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

# Optimisation / Recommendation Rules (O-series)

**6 rules** · Checker: `suite.checks.optimizations` (AST-based)

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

```python
# conformance: ignore[O001] intentional: stdlib json required here
```

**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 |
|---|---|---|---|---|---|---|
| [O001](#o001) | `OrjsonOverStdlibJson` | `warn` | `both` | `canonical-dependency` | — | 0.3.0 |
| [O002](#o002) | `LegacyAssetSerialization` | `warn` | `app` | `asset-mapper` | — | 0.8.0 |
| [O003](#o003) | `UntypedAssetMapperReturn` | `warn` | `app` | `asset-mapper` | — | 0.8.0 |
| [O004](#o004) | `LegacyPyatlanAssetImport` | `warn` | `app` | `asset-mapper` | — | 0.8.0 |
| [O005](#o005) | `UnresolvedAppNamePlaceholder` | `warn` | `both` | `dag-write-path` | — | 0.18.0 |
| [O006](#o006) | `DirectRocksdictImport` | `warn` | `app` | `canonical-dependency` | — | 0.18.0 |

---

## O001 — `OrjsonOverStdlibJson` {#o001}

**Tier:** `warn` · **Scope:** `both` · **Category:** `canonical-dependency` · **Autofixable:** — · **Since:** 0.3.0

> json.dumps()/json.loads() — prefer orjson (a core SDK dependency, ~10x faster)

**Rationale:** orjson is already a core SDK dependency — zero incremental cost — and on hot paths the
~10x throughput advantage compounds at fleet scale. WARN (not block) because orjson
returns bytes not str and has a different option API, so each site needs human judgment
before migrating.

`orjson` is already a core dependency of the application SDK, so it is available to
every app, and it is generally *at least* 10x faster than the stdlib `json` module.
Prefer `orjson.dumps` / `orjson.loads` for serialisation on any hot path.

Only `json.dumps` and `json.loads` call sites are flagged, and only when `json` resolves
to the stdlib module (an `import json` / `import json as …` / `from json import
dumps|loads` binding).  Bare `.json()` attribute calls (e.g. `response.json()`) are
never flagged. `json.JSONDecodeError` handling, `json.dump`/`json.load` (file-object
APIs orjson does not provide), and custom `JSONEncoder` subclasses are out of scope.

NOT autofixable: `orjson` is not a drop-in replacement.  `orjson.dumps` returns `bytes`
(not `str`), has no `indent=` / `sort_keys=` / `default=` keyword surface (use
`option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS` and the `default` positional), and
rejects some inputs stdlib accepts.  A blind `json.`→`orjson.` swap silently changes
`str`→`bytes` and breaks callers — each site needs human judgement.

---

## O002 — `LegacyAssetSerialization` {#o002}

**Tier:** `warn` · **Scope:** `app` · **Category:** `asset-mapper` · **Autofixable:** — · **Since:** 0.8.0

> Asset serialised with .dict() — prefer the v9 asset.to_nested_bytes() API

**Rationale:** The asset-mapper pattern serialises pyatlan assets to JSONL with the v9 API —
asset.to_nested_bytes() — which emits the nested-entity wire shape the platform expects.
Serialising an asset with the pydantic .dict() method produces a flat dict that still
needs hand-conversion and drifts from the SDK's recommended pipeline (BLDX-1492;
docs/upgrade-guide-v3.md). WARN/recommendation because .dict() is name-anchored — it can
also belong to a non-asset pydantic model — so the call needs a human glance.

Flags a `.dict()` method call in a module that imports pyatlan asset models.  The
asset-mapper pattern writes assets with the v9 serialisation API —
`asset.to_nested_bytes()` — not the pydantic `.dict()` form (`docs/upgrade-guide-v3.md`
explicitly says 'use the v9 serialisation API instead of .dict()').

Coverage limits (biased to low false-positives at WARN): only `.dict()` is matched (not
`.json()`, which is overwhelmingly `response.json()` on HTTP clients), and only in files
that import asset models.  A `.dict()` on a *non-asset* pydantic model in such a file is
a known false-positive — suppress with `# conformance: ignore[O002] <reason>`.

---

## O003 — `UntypedAssetMapperReturn` {#o003}

**Tier:** `warn` · **Scope:** `app` · **Category:** `asset-mapper` · **Autofixable:** — · **Since:** 0.8.0

> Function builds a pyatlan asset but has no return annotation — annotate it with the asset type

**Rationale:** The asset-mapper pattern's value is end-to-end typing: a mapper function constructs a
pyatlan asset and returns it, so the return annotation documents which asset it produces
and lets pyright check the call site. A mapper that builds an asset but declares no
return type loses that guarantee (BLDX-1492; reference app atlan-openapi-app).
WARN/recommendation because adding the annotation is a safe, mechanical nudge.

Flags a function that constructs a pyatlan asset (instantiates a class imported from
`pyatlan_v9.model.assets` / `pyatlan.model.assets`) and **returns that asset**, but
carries no `-> <Asset>` return annotation. The asset-mapper pattern is typed end-to-end
— each `map_<entity>` function declares the pyatlan asset it produces (see
`atlan-openapi-app`).

Keyed on actually returning the constructed asset (`return Table(...)` or `asset =
Table(...); ... return asset`), not just a `map_` name — so a helper that builds an
asset as a side effect and returns something else (e.g. `return record.id`) is not
flagged, and the suggested annotation always matches the real return.  Suppress with `#
conformance: ignore[O003] <reason>` when an untyped return is intentional.

---

## O004 — `LegacyPyatlanAssetImport` {#o004}

**Tier:** `warn` · **Scope:** `app` · **Category:** `asset-mapper` · **Autofixable:** — · **Since:** 0.8.0

> Imports pyatlan.model.assets (non-v9) — prefer pyatlan_v9.model.assets

**Rationale:** pyatlan_v9 is the SDK's go-forward asset surface for the asset-mapper pattern; the
legacy pyatlan.model.assets classes are the memory-heavy DataFrame/transformer-era
serialization path, kept only for connectors still on the built-in AtlasTransformer
(which B001 steers off). pyatlan_v9 ships inside the existing pyatlan>=9 dependency, so
the switch adds nothing to resolve. A below-the-bar recommendation (O-series, WARN): the
v9 models differ in attributes and serialization (to_nested_bytes vs .dict()), so each
site needs human judgement — never a blind name swap.

Flags app code that imports asset model classes from the legacy `pyatlan.model.assets`
package, in any of the three import forms: `from pyatlan.model.assets import X`, `import
pyatlan.model.assets`, or `from pyatlan.model import assets`.  Detection is
import-anchored (an asset class is only imported in order to construct it), so the rare
fully-qualified `pyatlan.model.assets.X(...)` form with no matching import is out of
scope.  New connectors should build assets from `pyatlan_v9.model.assets` — the
optimized v9 surface the asset-mapper pattern is built on (BLDX-1492; see
`docs/guides/sql-application-guide.md` and `docs/upgrade-guide-v3.md`).

Scope is deliberately narrow — only `pyatlan.model.assets` is matched, never the rest of
`pyatlan`: enums and helpers that legitimately have no v9 equivalent (e.g. `from
pyatlan.model.enums import AtlanConnectorType`) are out of scope.

NOT autofixable: the v9 models are not a drop-in rename — attribute names and the
serialization API differ (use `asset.to_nested_bytes()` rather than `.dict()`), so each
construction site needs review. Suppress with `# conformance: ignore[O004] <reason>`
when a connector is intentionally pinned to the legacy `AtlasTransformer` surface.

---

## O005 — `UnresolvedAppNamePlaceholder` {#o005}

**Tier:** `warn` · **Scope:** `both` · **Category:** `dag-write-path` · **Autofixable:** — · **Since:** 0.18.0

> Hardcoded '{app_name}' left unsubstituted in a plain string literal

**Rationale:** A plain string literal carrying an unsubstituted '{app_name}' token freezes the literal
token into whatever it's assigned to instead of the real app name — the exact shape that
shipped a task queue no worker polls, hanging dbt:process to its 24h heartbeat backstop
(CONNECT-183). The substitution was independently hand-rolled at least four times
(Heracles, native-migration-app, atlan-local-marketplace-app CONNECT-191/#539,
atlan-hightouch-app ARUN-1039), and one of those shipped a double prefix (DISTR-834). A
canonical helper — application_sdk.common.task_queue (derive_task_queue,
resolve_manifest_tokens) — lands in the SDK release that ships FND-195, so remediation
then has a single target; this rule still checks the unresolved shape rather than a
missing import, because the writers most worth catching are hand-authored templates that
import nothing. WARN (not BLOCK): a template legitimately resolved by a caller in a
different file cannot be seen from here, so each hit needs a human glance rather than an
automatic fail.

Flags a string `ast.Constant` containing the literal substring `{app_name}` when the
token can actually **reach a value** — including the pieces of an escaped-brace
f-string: `f"atlan-{{app_name}}-prod"` is *not* interpolated, its runtime value is the
literal `atlan-{app_name}-prod`, so it freezes the token exactly like a plain literal. A
token that only ever appears in prose or in a diagnostic cannot freeze into an
identifier, and flagging it just teaches people to suppress the rule.

Not flagged:

* part of a *resolving* f-string (`f"...{app_name}..."` interpolates   at parse time and
no `{app_name}` text survives into its pieces), * the receiver of a `.format(...)` call
whose keywords include   `app_name=` (a proper, already-resolving substitution site), *
**documentation** — the value of any bare string expression statement.   This covers
module/class/function docstrings *and* PEP 257 attribute   docstrings, which are not the
first statement of their class body, * **diagnostic text** — inside the arguments of a
logging call,   `warnings.warn(...)`, or a `raise`; reporting on the token requires
quoting it, * **a token sentinel or message constant** — bound to an `ALL_CAPS`   name
where the literal is exactly the token (the token's own   definition, e.g.
`APP_NAME_TOKEN`) or the name ends in a prose   segment (`_MESSAGE`, `START_MESSAGE`,
`VALIDATION_RATIONALE`).

The exclusions stay narrow: an `ALL_CAPS` name holding a real queue template
(`TASK_QUEUE = "atlan-{app_name}-prod"`) is still flagged — and so is one whose name
merely *contains* a prose fragment without a trailing boundary (`MESSAGE_QUEUE`,
`HELP_QUEUE`) — as are keyword arguments, values at any depth in a DAG literal, and
returned templates.

Detection is shape-anchored rather than import-anchored, because the writers most worth
catching are hand-authored templates outside the SDK that import nothing at all.
Remediation: an f-string or `.format(app_name=...)` when the name is in scope; the
shared `application_sdk.common.task_queue` helper (`derive_task_queue` /
`resolve_manifest_tokens`) lands in the SDK release that ships FND-195 and is the
canonical target once available.

NOT autofixable: the correct fix depends on where `app_name` is actually available in
scope — sometimes an f-string is right, sometimes the value needs threading in from a
caller first. Suppress with `# conformance: ignore[O005] <reason>` for a template
resolved by a caller in a different file than the one being scanned.

---

## O006 — `DirectRocksdictImport` {#o006}

**Tier:** `warn` · **Scope:** `app` · **Category:** `canonical-dependency` · **Autofixable:** — · **Since:** 0.18.0

> Imports rocksdict directly — prefer the SDK's SpillableDict (pickles values, no hand-rolled serialize/deserialize step)

**Rationale:** SpillableDict (application_sdk.common.spillable_dict) already wraps rocksdict.Rdict as a
MutableMapping and pickles values directly, so it carries none of the
hand-rolled-serialization risk a from-scratch wrapper does. Two connectors
(atlan-thoughtspot-app, atlan-aws-smus-app) independently hand-rolled the same
RocksDB-backed DiskLookup with an asymmetric JSON serialize/deserialize step — put()
special-cased str, get() unconditionally ran json.loads() — so a stored string that was
also valid bare JSON (a numeric-looking name, 'true', 'null') silently came back as
int/bool/None instead of str (CNCT-80, CNCT-191). WARN (not block) because a
from-scratch wrapper may have a deliberate reason (custom RocksDB Options, a key type
outside str/int/float/bool/bytes) that needs a human glance before migrating.

Flags app code that imports the `rocksdict` package directly, in either import form:
`from rocksdict import Rdict` or `import rocksdict`.  Detection is import-anchored (a
direct `rocksdict` import is the unambiguous signal — nothing else pulls that dependency
in).

The SDK ships `application_sdk.common.spillable_dict.SpillableDict` — a
`MutableMapping`-compatible, disk-backed dict built on the same `rocksdict.Rdict`, which
pickles values directly rather than hand-rolling a serialize/deserialize step.  It
exists specifically so connector apps stop reinventing this wrapper.

Motivating incident: `atlan-thoughtspot-app` and `atlan-aws-smus-app` each independently
wrote a `DiskLookup` class directly on `rocksdict.Rdict` with the identical bug — a
value's `str` type was silently lost on a round-trip through JSON when the string
happened to also be valid bare JSON.  Neither connector's hand-rolled wrapper was
calling anything the SDK had a fleet-wide signal for at the time; this rule is that
signal going forward.

NOT autofixable: `SpillableDict`'s key type is restricted to `str | int | float | bool |
bytes` and it has no equivalent to a custom `rocksdict.Options` tuning surface, so each
call site needs review before migrating.  Suppress with `# conformance: ignore[O006]
<reason>` when a from-scratch wrapper is deliberate (e.g. custom RocksDB tuning, or
association-list output like `rocks_backed_dict.py`'s `append_to_key` that
`SpillableDict` does not provide).

---
