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

# Contract-Toolkit Conformance Rules (K-series)

**14 rules** · Checker: `suite.checks.legacy_contract` (K001–K002, pkl-source regex, scans ``contract/**/*.pkl``), `suite.checks.generated_freshness` (K003–K005, scans ``contract/PklProject``, ``contract/PklProject.deps.json``, ``atlan.yaml``, ``app.yaml``, and ``app/generated/**``), `suite.checks.manifest_contract` (K006, cross-references ``app/generated/**/manifest.json`` against Python ``Output`` contracts)

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

```python
// conformance: ignore[K001] intentional: phased migration tracked in BLDX-XXXX
```

| ID | Name | Tier | Scope | Category | Autofixable | Since |
|---|---|---|---|---|---|---|
| [K001](#k001) | `ContractAmendsLegacyModule` | `warn` | `app` | `contract-toolkit` | — | 0.9.0 |
| [K002](#k002) | `LegacyContractApi` | `warn` | `app` | `contract-toolkit` | — | 0.9.0 |
| [K003](#k003) | `ContractLockDrift` | `warn` | `app` | `contract-toolkit` | — | 0.9.0 |
| [K004](#k004) | `MissingGeneratedArtifact` | `warn` | `app` | `contract-toolkit` | — | 0.9.0 |
| [K005](#k005) | `GeneratedArtifactBannerStripped` | `warn` | `app` | `contract-toolkit` | — | 0.9.0 |
| [K006](#k006) | `ManifestContractFieldMismatch` | `warn` | `app` | `contract-toolkit` | — | 0.13.0 |
| [K007](#k007) | `ToolkitVersionOutdated` | `warn` | `app` | `contract-toolkit` | — | 0.12.0 |
| [K008](#k008) | `ToolkitSourceNonCanonical` | `warn` | `app` | `contract-toolkit` | — | 0.12.0 |
| [K009](#k009) | `UnresolvedScaffoldPlaceholder` | `block` | `app` | `contract-toolkit` | — | 0.12.0 |
| [K010](#k010) | `E2EScaffoldingMissing` | `warn` | `app` | `contract-toolkit` | — | 0.12.0 |
| [K011](#k011) | `AppIdMissingFromContract` | `block` | `app` | `contract-toolkit` | — | 0.14.0 |
| [K012](#k012) | `GeneratePoeTaskMissing` | `block` | `app` | `contract-toolkit` | — | 0.14.0 |
| [K013](#k013) | `ManifestNodeAppNameMisattributed` | `warn` | `app` | `contract-toolkit` | — | 0.18.0 |
| [K014](#k014) | `ReleaseModelUndeclared` | `warn` | `app` | `contract-toolkit` | — | 0.18.0 |

---

## K001 — `ContractAmendsLegacyModule` {#k001}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.9.0

> Contract amends NativeApp.pkl or NativeAppBundle.pkl — migrate to App.pkl

**Rationale:** App.pkl (contract-toolkit v0.10.0+) is the canonical single entry point that supersedes
NativeApp.pkl (single-entrypoint) and NativeAppBundle.pkl (multi-entrypoint bundle).
Legacy modules are frozen and slated for hard removal at toolkit v1.0.  Renovate version
bumps + regeneration break contracts that still amend the legacy base, and the generated
artifacts (atlan.yaml, manifest.json) may silently carry stale or incorrectly structured
fields.  Migrating to App.pkl before v1.0 removes the blast radius of the hard cutover
and aligns every app with the one supported workflow for contract evolution (BLDX-1479).

The `contract/app.pkl` file (or any `contract/**/*.pkl` file) contains an `amends` line
pointing at `NativeApp.pkl` or `NativeAppBundle.pkl` instead of the canonical `App.pkl`.

`App.pkl` (contract-toolkit v0.10.0+) is the single entry point for all Atlan native
apps — both single-entrypoint and multi-entrypoint (bundle) contracts.  The legacy
modules are frozen (no new features) and are scheduled for hard removal at v1.0.

**Migration steps:**

1. Change the `amends` line to `amends "@app-contract-toolkit/App.pkl"`.

2. Resolve `workflowType`.  NativeApp.pkl paired a PascalCase `workflowType` with an
optional `workflowTypeOverride` and applied automatic kebab-casing.  App.pkl takes a
**verbatim** string — set it to the exact string the legacy contract would have emitted
(apply PascalCase→kebab-case manually if needed), then drop `workflowTypeOverride`.
Omit `workflowType` entirely when it would equal the `name` field (App.pkl defaults to
kebab-casing `name`).

3. Make `connector` nullable if it is a utility app with no connector type
(NativeApp.pkl required `connector`; App.pkl allows `null`).

4. For `NativeAppBundle.pkl` migrations: move each per-entrypoint contract into
App.pkl's typed `entrypoints` block.  Each child contract file that also amends
`NativeApp.pkl` will produce its own K001 finding and must be migrated separately.

5. Run `pkl eval -m . contract/app.pkl` (or `uv run poe generate`) to regenerate
`app/generated/**` and `atlan.yaml`.  Never hand-edit generated artifacts — K004/K005
and the generated-artifact freshness gate catch staleness.

**Suppress** with `// conformance: ignore[K001] <reason>` on the `amends` line or the
comment-only line directly above it when a deliberate delay is justified (e.g. phased
migration tracked in a follow-on ticket).

---

## K002 — `LegacyContractApi` {#k002}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.9.0

> Contract uses NativeApp-only APIs (flatManifestArgs, workflowTypeOverride, or legacy imports) removed in App.pkl

**Rationale:** Several properties and imports that exist in NativeApp.pkl were intentionally dropped
when App.pkl was designed: flatManifestArgs and manifestMetadataArgs (App.pkl always
emits flat top-level args), workflowTypeOverride (App.pkl takes a verbatim
workflowType), and explicit imports of Config.pkl/Credential.pkl/Renderers.pkl (Argo-era
modules App.pkl no longer uses).  Their presence in a contract that claims to amend
App.pkl (or that will be migrated to App.pkl) indicates that the migration is
incomplete.  When any of these knobs remain after the amends line is changed, pkl eval
fails, blocking CI (BLDX-1479).

The `contract/**/*.pkl` file contains one or more NativeApp-only properties or imports
that do not exist in `App.pkl`:

* `flatManifestArgs` / `manifestMetadataArgs` — control how workflow params are nested
in the manifest `args` object.  App.pkl always emits flat top-level args; these flags
have no counterpart and must be removed.

* `workflowTypeOverride` — companion to NativeApp.pkl's PascalCase `workflowType` field.
App.pkl takes a verbatim string; resolve the final kebab-cased value and set it as
App.pkl's `workflowType`, then remove `workflowTypeOverride`.

* `import "…Config.pkl"` — Config.pkl provides widget types for NativeApp.pkl.  App.pkl
re-exports all widget types as typealiases (`TextInput`, `UIConfig`, etc.) — remove the
import and switch `Config.UIConfig` → `UIConfig`, `Config.TextInput` → `TextInput`, etc.

* `import "…Credential.pkl"` and `import "…Renderers.pkl"` (Argo-era modules) — no
longer used by App.pkl; remove both.

Note: `import "…Connectors.pkl"` is **not** flagged — the Connectors registry is still
imported explicitly by App.pkl consumers (App.pkl imports it internally and types
`connector` as `Connectors.Type` but does not re-export the constants), so the import is
required, not legacy.

Note: a contract that **amends** `Credential.pkl` (a credential-config sub-contract, not
an App.pkl entrypoint) is **not** flagged for its `import "…Config.pkl"` line.
Credential.pkl uses `Config.*` internally but, unlike App.pkl, does not re-export the
widget types as unqualified typealiases, so such a contract genuinely needs the import
for `pkl eval` to resolve `Config.TextInput` etc. — flagging it would be a false
positive.  The exemption covers `Config.pkl` only: a credential contract carrying
`import "…Renderers.pkl"` or `import "…Credential.pkl"` is still legacy and still fires.

**Note:** if the contract also has a K001 finding (still amending a legacy module),
address K001 first — many K002 knobs disappear automatically when the module changes,
because App.pkl simply lacks those properties.

After editing, run `pkl eval -m . contract/app.pkl` (or `uv run poe generate`) to
regenerate `app/generated/**` and `atlan.yaml`.  Never hand-edit generated artifacts —
K004/K005 and the generated-artifact freshness gate catch staleness.

**Scanner limitation:** the checker is not string-literal aware.  A property name that
appears only inside a string literal (e.g. `description = "flatManifestArgs is removed
in App.pkl"` on a single line) may be flagged.  Use `// conformance: ignore[K002]
<reason>` to suppress false positives; that directive is the intended workaround for any
case where the pattern matches non-code content.

**Suppress** with `// conformance: ignore[K002] <reason>` on the violating line or the
comment-only line directly above it.

---

## K003 — `ContractLockDrift` {#k003}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.9.0

> contract/PklProject pin does not match the resolved version in PklProject.deps.json — re-resolve the lock

**Rationale:** contract/PklProject pins each pkl dependency (e.g. app-contract-toolkit) at an exact
@<version>; contract/PklProject.deps.json is the resolved lock that records the version
pkl actually fetched, with its checksum.  When someone bumps the pin in PklProject but
does not re-resolve, the lock stays behind: pkl eval regenerates from the OLD toolkit,
so the committed generated artifacts silently reflect a version the contract no longer
claims.  The self-hosted Renovate runner keeps these two in sync on bot bumps
(regenerating the lock and artifacts in the same PR via postUpgradeTasks), but a manual
pin edit bypasses it entirely.  Comparing the two files is a pure, deterministic text
check that needs no pkl toolchain, so it catches the drift the moment it lands
(BLDX-1414).

A dependency pinned in `contract/PklProject` resolves to a different version in
`contract/PklProject.deps.json` (or the lock file is missing / does not contain the
dependency at all).  The lock is stale relative to the pin.

`pkl eval` generates `app/generated/**` and `atlan.yaml` from whatever the lock resolves
to — so a stale lock means the committed artifacts were generated from a toolkit version
the contract no longer pins.  A bump to the `@<version>` in `PklProject` must be paired
with a re-resolve.

**Fix:** re-resolve the Pkl project so the lock matches the pin, then regenerate:

    pkl project resolve   # rewrites contract/PklProject.deps.json     pkl eval -m .
contract/app.pkl   # regenerates the artifacts

On a `renovate/**` branch the self-hosted Renovate runner does this automatically in the
same PR (via postUpgradeTasks); on a manual bump run the commands above (or `uv run poe
generate` where the app defines it).

The version match is prefix-aware: a broad pin such as `@0` is satisfied by any resolved
`0.y.z` and is never flagged — only a fully-specified pin (`@0.16.0`) that disagrees
with the lock, or a lock that lacks the dependency, is a finding.

**Suppress** with `// conformance: ignore[K003] <reason>` on the `uri` line in
`contract/PklProject` (or the comment-only line directly above it) when a deliberate lag
is justified.

---

## K004 — `MissingGeneratedArtifact` {#k004}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.9.0

> contract/app.pkl exists but an expected generated artifact (atlan.yaml / manifest.json / _input.py) is missing — regenerate

**Rationale:** An app that defines contract/app.pkl commits the pkl eval outputs (atlan.yaml,
app/generated/manifest.json, app/generated/_input.py) so that deployment and CI consume
them without a pkl toolchain.  When one of those outputs is absent while the contract
exists, the app was never generated (or the artifact was deleted): the platform reads a
manifest that does not exist, and the app fails to deploy or register.  File existence
is a fully deterministic check that needs no pkl (BLDX-1414).

The app defines `contract/app.pkl` but one or more of the artifacts `pkl eval` is
expected to produce is absent:

* `atlan.yaml` * `app/generated/manifest.json` * `app/generated/_input.py`

These are the outputs the deployment pipeline and the SDK read at runtime; a missing one
means the contract was never generated (or an output was deleted).

**Fix:** regenerate from the contract —

    pkl eval -m . contract/app.pkl

(or `uv run poe generate` where the app defines it) and commit the result.

**Suppress** with `// conformance: ignore[K004] <reason>` on the `amends` line of
`contract/app.pkl` when an output is legitimately not produced for this app.

---

## K005 — `GeneratedArtifactBannerStripped` {#k005}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.9.0

> A generated text artifact (atlan.yaml / app/generated/*.py) is missing its AUTO-GENERATED provenance banner — likely hand-edited

**Rationale:** Every text artifact the contract toolkit emits carries a provenance banner in its first
lines — 'AUTO-GENERATED from contract/app.pkl — DO NOT EDIT MANUALLY' (or the '… via
contract-toolkit. DO NOT EDIT' variant).  A generated file that is MISSING that banner
has almost always been hand-authored or hand-edited in place, which means it will
silently diverge from the contract on the next regeneration.  This is a heuristic proxy,
not a proof: a hand-edit that preserves the banner is invisible to a static scanner
(only the CI regenerate-and-diff gate catches that).  Because a deliberately
hand-maintained app legitimately strips the now-untrue banner, K005 stays WARN and is
suppressed per file rather than ever graduating to BLOCK (BLDX-1414).

A file the contract toolkit is expected to generate (`atlan.yaml`, `app.yaml`, or a
`.py` file under `app/generated/` other than `__init__.py`) does not carry the
provenance banner the toolkit stamps into the first lines of every output it writes:

    # AUTO-GENERATED from contract/app.pkl — DO NOT EDIT MANUALLY.

(or the `# Generated from contract/app.pkl via contract-toolkit. DO NOT EDIT.` variant).
A missing banner means the file was authored or edited by hand and will diverge from the
contract the next time `pkl eval` runs.

`.json` artifacts (`manifest.json` etc.) are exempt — JSON has no comment syntax to
carry a banner — as is the empty `app/generated/__init__.py`.

**Fix:** regenerate from the contract (`pkl eval -m . contract/app.pkl`) so the file is
re-emitted with its banner.

**Limitation:** this rule cannot see content-level hand-edits that leave the banner
intact; the CI generated-artifact freshness gate (regenerate-and-diff) is the check that
proves full freshness.

**Suppress** with `# conformance: ignore[K005] <reason>` on the first line of the file
(or the line above it) for an app that deliberately hand-maintains this artifact.

---

## K006 — `ManifestContractFieldMismatch` {#k006}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.13.0

> app/generated/**/manifest.json references an $.extract.outputs.<field> the entrypoint's Output contract does not declare

**Rationale:** App.pkl's pipeline nodes (e.g. PublishNode) unconditionally wire a downstream node's
args to the entrypoint's runtime output via a JSONPath such as
$.extract.outputs.publish_state_prefix. Pkl compiles this before any Python runs and has
zero visibility into the app's Output model; the B005 contract-ledger checker only knows
a field 'was tracked and disappeared,' with no knowledge of the manifest's JSONPath
requirements. An app can therefore silently delete a field the manifest depends on, and
nothing static catches it — only a rarely-run, non-deterministic full-DAG e2e run
against a real Automation Engine does. This is exactly what happened when
OpenAPIConnectorOutput lost publish_state_prefix and current_state_prefix in an
unrelated conformance-cleanup PR and went undetected for about 12 days (BLDX-1527). K006
closes the loop with a structural manifest-vs-contract diff, computed once both
artifacts exist, without either layer needing visibility into the other's language.

A `$.extract.outputs.<field>` JSONPath reference in a committed
`app/generated/**/manifest.json` DAG node's `inputs.args` names a field that the
corresponding entrypoint's Python `Output` contract does not declare — not directly, and
not via any inherited base class or SDK mixin.

The Automation Engine resolves this JSONPath at runtime against the object the
entrypoint's workflow actually returned. A missing field means the reference never
resolves, and the dependent pipeline step (most commonly the default `publish` step)
fails at runtime with an unresolved-JSONPath error — the one signal that would have
caught this is a rarely-run, opt-in-labeled, non-deterministic full-DAG e2e test.

**Fix:** declare the missing field(s) on the entrypoint's `Output` model, or mix in the
SDK contract base that already supplies them. For the publish-state fields specifically
(`connection_qualified_name`, `transformed_data_prefix`, `publish_state_prefix`,
`current_state_prefix`), mix in `application_sdk.contracts.base.PublishInputMixin`
rather than hand-declaring each field — it also derives the values correctly from
`connection_qualified_name`.

**Never hand-edit** `app/generated/manifest.json` to work around a finding — it is a
`pkl eval` output (K004/K005 and the generated-artifact freshness gate catch a
hand-edited manifest). If the referenced field is genuinely not needed (e.g. the
pipeline step that consumes it should not be enabled), remove or reconfigure that step
in `contract/app.pkl` and re-run `pkl eval -m . contract/app.pkl` instead.

**Suppress** with `# conformance: ignore[K006] <reason>` on the `Output` class
definition (or the comment-only line directly above it) when a mismatch is understood
and deliberately deferred.

---

## K007 — `ToolkitVersionOutdated` {#k007}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.12.0

> app-contract-toolkit dependency resolves to a version below the latest published one — bump and regenerate

**Rationale:** The app-contract-toolkit ships fixes and new contract capabilities in every release; an
app pinned to an old version regenerates stale artifacts, misses schema changes, and is
the usual root cause of leftover scaffold placeholders (K009) and legacy-API drift
(K002). The latest published version is read from the baked-in toolkit baseline
(data/toolkit_baseline.json), regenerated from the toolkit's own PklProject and guarded
against drift in CI, so the check stays correct offline inside any consumer repo.

The `app-contract-toolkit` dependency in `contract/PklProject` resolves (per
`contract/PklProject.deps.json`) to a version older than the latest the SDK publishes.

The check uses the resolved lock as the ground truth for the version actually in use; a
missing or stale lock is K003's concern, so K007 stays quiet in that case rather than
guessing from a broad pin.

**Fix:** bump the `@<version>` in `contract/PklProject` to the latest, run `pkl project
resolve` to refresh the lock, then regenerate with `pkl eval -m . contract/app.pkl`. On
a `renovate/**` branch the self-hosted Renovate runner does this automatically via
postUpgradeTasks.

**Suppress** with `// conformance: ignore[K007] <reason>` on the `uri` line (or the
comment-only line directly above it) when a deliberate lag is justified.

---

## K008 — `ToolkitSourceNonCanonical` {#k008}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.12.0

> app-contract-toolkit is sourced from a non-canonical base URI (fork / local path / wrong host)

**Rationale:** Every app must consume the app-contract-toolkit from the single SDK-published package so
the whole fleet generates from the same contract semantics and receives version bumps
through the standard renovate flow. A dependency pointed at a fork, a local file path,
or a different host silently diverges — it can pin behavior the SDK no longer supports
and is invisible to the version floor (K007), which only compares against the canonical
package. The canonical base URI is read from the baked-in toolkit baseline.

The `app-contract-toolkit` dependency in `contract/PklProject` is pointed at a base URI
other than the canonical SDK-published package
(`package://atlanhq.github.io/application-sdk/contracts/app-contract-toolkit`).

The dependency is identified by its `["app-contract-toolkit"]` mapping key (falling back
to any URI whose path segment is `app-contract-toolkit`), so a fork under the canonical
key is still caught.

**Fix:** point the dependency at the canonical package
`package://atlanhq.github.io/application-sdk/contracts/app-contract-toolkit@<latest>`
and run `pkl project resolve`.

**Suppress** with `// conformance: ignore[K008] <reason>` on the `uri` line (or the
comment-only line directly above it).

---

## K009 — `UnresolvedScaffoldPlaceholder` {#k009}

**Tier:** `block` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.12.0

> Generated artifact contains an unresolved single-brace scaffold placeholder ({app_name}, {name}, …) — upgrade the toolkit and regenerate

**Rationale:** A committed generated artifact that still contains a single-brace scaffold token such as
{app_name} is shipping template text where a rendered value belongs — the current
toolkit resolves {app_name} to the app's literal name, so a leftover means the artifact
was generated by an outdated toolkit (or a legacy NativeApp.pkl base) and is objectively
wrong on the wire, not merely stylistically stale. Because it is never a false positive
(the {{...}} double-brace runtime tokens are excluded, and the one legitimate
single-brace token {deployment_name} is not in the flagged set), this is a BLOCK-tier
rule rather than the usual land-as-WARN default: the only correct resolution is to
upgrade to the latest app-contract-toolkit and regenerate.

A committed generated artifact (`atlan.yaml`, `app.yaml`, or a file under
`app/generated/`) contains a single-brace scaffold placeholder token — `{app_name}`,
`{name}`, `{app-name}`, `{entrypoint_name}`, `{connection_name}`, and related scaffold
vars.

These are filled in by `pkl eval`; a literal leftover means the artifact was generated
by an outdated toolkit (or a legacy `NativeApp.pkl` base) that never rendered the field.
The current toolkit resolves `{app_name}` to the app's literal name, so this artifact is
wrong as shipped — hence BLOCK, not WARN.

Intentional `{{credential}}` / `{{connection}}` double-brace E2E runtime-substitution
tokens are excluded, as is the legitimate `{deployment_name}` deploy-time token the
current toolkit still emits verbatim; none of these are ever flagged.

**Fix (required):** upgrade `app-contract-toolkit` to the latest published version (see
K007), migrate off any legacy `NativeApp.pkl` base (see K001), and regenerate with `pkl
eval -m . contract/app.pkl`. Never hand-edit the artifact to delete the token.

**Suppress** with `# conformance: ignore[K009] <reason>` on the placeholder's line (or
the comment-only line directly above it) — available for text artifacts; a placeholder
in a `.json` output has no comment syntax to suppress and must be regenerated.

---

## K010 — `E2EScaffoldingMissing` {#k010}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.12.0

> Single-entrypoint contract/app.pkl exists but generated app/generated/_e2e_base.py is missing

**Rationale:** The toolkit emits app/generated/_e2e_base.py for every single-entrypoint app, and the
SDK's E2E framework imports it; a missing file means the contract was never generated
(or the output was deleted), so the app's E2E tests cannot resolve their typed base.
Multi-entrypoint bundles emit E2E scaffolding into per-entrypoint subfolders instead, so
the rule only applies when the contract declares no entrypoints block.

A single-entrypoint `contract/app.pkl` exists but its generated E2E scaffolding
`app/generated/_e2e_base.py` is absent. The toolkit emits this module unconditionally
for single-entrypoint apps, and `application_sdk.testing.e2e` imports it as the typed
E2E base.

Contracts that declare an `entrypoints` block (multi-entrypoint bundles) are out of
scope — their E2E scaffolding lands in per-entrypoint subfolders, not the
single-entrypoint path.

**Fix:** regenerate with `pkl eval -m . contract/app.pkl` and commit the result.

**Suppress** with `// conformance: ignore[K010] <reason>` on the `amends` line of
`contract/app.pkl` when the app legitimately ships no E2E scaffolding.

---

## K011 — `AppIdMissingFromContract` {#k011}

**Tier:** `block` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.14.0

> atlan.yaml is present but declares no top-level app_id — the marketplace publish will 404

**Rationale:** app_id is the stable identity that maps the app to its Global Marketplace record. The
release publish step (build-and-publish-app.yaml -> .github/scripts/parse_atlan_yaml.py)
reads the top-level app_id out of the generated atlan.yaml and POSTs it to the GM; an
empty value makes the GM return 404 creating the version, so the GitHub Release is cut
and the image is pushed to GHCR but the version never appears in the marketplace. This
is a silent, release-time-only failure: nothing in the app's own PR CI notices, and it
stays broken across every subsequent release until someone reads the failed publish log.
It regressed on a live connector when atlan.yaml became fully pkl-generated and the
app_id, previously hand-carried in the file, was dropped because the pkl metadata block
never declared it. BLOCK-tier because the only outcome of shipping without it is a
broken release.

The committed `atlan.yaml` has no top-level `app_id:` key. `app_id` is the app's Global
Marketplace identity; the release publish step POSTs it to the GM, and an empty value
returns a 404 so the release never reaches the marketplace even though the tag, the
GitHub Release, and the GHCR image all succeed.

`atlan.yaml` is generated from `contract/app.pkl` and must not be hand-edited (K005
guards its provenance banner). Restore `app_id` at its source, in the pkl `metadata`
block — its entries are emitted as top-level `atlan.yaml` keys, exactly as
`release_model` already is:

    metadata {       ["release_model"] = "semver"       ["app_id"] = "<your-app-uuid>"
}

Find `<your-app-uuid>` in the Global Marketplace admin UI (the app URL —
`/admin/#/apps/<app_id>/versions`) or in the `app_id` of a prior successful publish log.
Then regenerate with `pkl eval -m . contract/app.pkl` (or `uv run poe generate`) and
commit the updated `atlan.yaml`.

**Suppress** with `# conformance: ignore[K011] <reason>` on the first line of
`atlan.yaml` (or the line above) — but a suppression is almost never correct here: a
semver app with no `app_id` cannot publish. It exists only for the rare non-published
app that still ships an `atlan.yaml`.

---

## K012 — `GeneratePoeTaskMissing` {#k012}

**Tier:** `block` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.14.0

> pyproject.toml defines no [tool.poe.tasks.generate] task — the SDK Certify step will abort the publish

**Rationale:** The SDK build-and-publish Certify step runs `uv run poe generate` to prove the committed
generated artifacts match what regenerating the contract today would produce, and it
hard-fails (exit 1) when the task does not exist -- `Error: Unrecognized task
'generate'`. That aborts Certify before the publish job runs, so a missing task blocks
every marketplace release. Apps whose contract generation lived only in a Makefile
target (`make generate`) had no poe task of that name, so the check passed locally yet
the release died in CI. A one-line poe alias mirroring the Makefile target closes the
gap. BLOCK-tier because, like K011, the only outcome of the missing piece is a broken
release rather than degraded quality.

`pyproject.toml` has a contract (a `contract/` directory exists) but `[tool.poe.tasks]`
defines no `generate` task. The SDK Certify step (`build-and-publish-app.yaml`) runs `uv
run poe generate` and fails the whole publish run with `Unrecognized task 'generate'`
when the task is absent.

Add a `generate` poe task that regenerates the contract artifacts, mirroring the repo's
`Makefile` target — for the common single-contract layout:

    [tool.poe.tasks]     generate = "pkl eval --project-dir contract -m .
contract/app.pkl"

Include every `.pkl` the `Makefile` evaluates (e.g. a credential contract such as
`contract/csa-connectors-objectstore.pkl`) so `poe generate` and `make generate` stay
equivalent. Verify with `uv run poe generate` — it must succeed and leave the generated
tree unchanged.

**Suppress** with `# conformance: ignore[K012] <reason>` on the `[tool.poe.tasks]`
header line (or the line above). Only justified for an app that is genuinely never
published through the marketplace pipeline.

---

## K013 — `ManifestNodeAppNameMisattributed` {#k013}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.18.0

> Generated manifest DAG node declares an app_name that disagrees with the app its workflow type or task queue says runs it

**Rationale:** A DAG node's app_name is the identity its logs, metrics and failures are filed under:
the SDK tags log records with it and the tenant's Workflow Center reads them back by it,
so a wrong value makes the step's logs unreachable rather than merely mislabelled -- the
step shows 'No error logs available for this pod' even though logging worked. Nothing
else catches it, because the node still runs correctly: task_queue governs dispatch, so
a misattributed node executes in the right place and only its telemetry goes to the
wrong app. That is why the drift survived across the connector fleet unnoticed (CNCT-24,
CNCT-129). Two signals establish the owning app independently. Automation Engine hosts
none of the toolkit-owned workflows, so QueryIntelligenceWorkflow paired with app_name
'automation-engine' is impossible rather than merely suspect -- the signature of a
contract that hand-wrote a raw DAGNode instead of the matching node class and inherited
the default. Separately, a task queue naming a known system app says which worker polls
the node whatever its workflow type. Both are exact-match checks against closed sets, so
neither guesses.

A node in a committed generated `manifest.json` declares an `app_name` that disagrees
with the app actually running it. Two independent signals are checked, each against a
closed set:

**1. The workflow type.** Automation Engine hosts none of the toolkit-owned workflows --
each has its own worker:

    QueryIntelligenceWorkflow -> query-intelligence     PublishWorkflow           ->
publish     LineageWorkflow           -> lineage     PopularityWorkflow        ->
popularity     NotificationWorkflow      -> notification-app

One of these paired with `app_name: automation-engine` is drift by construction: the
contract hand-wrote a raw `DAGNode` instead of the matching node class and inherited the
default. An `app_name` set to some *other* value is left alone -- that may be a bespoke
worker, which is the author's call.

**2. The task queue.** A queue of the form `atlan-<system-app>-...` names the worker
that polls the node, and so the app that runs it, whatever its workflow type. A
disagreeing `app_name` is misattributed. Only the *app* segment is matched, against the
known system apps -- the suffix (`-production`, `-{deployment_name}`, a tenant name) is
not interpreted.

Why nothing else catches it: `task_queue` governs dispatch, so a misattributed node
still executes in the right place. Only its telemetry goes astray -- the logs are
written under one identity and read back under another, so the step shows `No error logs
available for this pod` even though logging worked.

**Fix:** in `contract/app.pkl`, either replace the hand-written `DAGNode` with the
matching built-in node class (`QueryIntelligenceNode`, `PublishNode`, `LineageNode`,
`PopularityNode`, `NotificationNode`), which sets `appName` and `taskQueue` together, or
set `appName` explicitly to the app named in the finding. Then regenerate with `pkl eval
-m . contract/app.pkl`.

**Change `appName` only.** The rule never asks for a `taskQueue` change: the queue is
the routing decision, and in these manifests it is generally already right. Rewriting it
would move where the node runs.

**No suppression is available.** The finding is anchored on a generated `.json`
artifact, which has no comment syntax to carry a directive -- as with K009, the only
resolution is to fix the contract and regenerate. Never hand-edit `manifest.json`.

---

## K014 — `ReleaseModelUndeclared` {#k014}

**Tier:** `warn` · **Scope:** `app` · **Category:** `contract-toolkit` · **Autofixable:** — · **Since:** 0.18.0

> atlan.yaml declares no top-level release_model, so the app silently inherits the 'cd' default and auto-publishes on merge

**Rationale:** release_model decides whether merging to main publishes the app to every tenant. The
publish step reads it out of the committed atlan.yaml and defaults a missing key to 'cd'
(.github/scripts/parse_atlan_yaml.py), so an app that never declares it auto-publishes
to channel='all' on every merge -- not because anyone chose continuous delivery, but
because the key was absent. That default is silent by construction: the app builds,
tests and publishes successfully, so no gate has any reason to speak up, and the release
model an app is actually running is invisible in review. A fleet sweep found 36 of 75
connectors in exactly that state, none of them deliberately. This rule does not prefer
either model -- 'cd' is a legitimate choice -- it only requires that the choice be
written down, so it is reviewable and cannot be inherited by accident.

The committed `atlan.yaml` has no usable top-level `release_model:` key, or declares a
value outside the allowed set.

`release_model` selects how the app reaches tenants:

    cd      every merge to main publishes to channel='all'     semver  merges build
only; a GitHub Release publishes to all

A missing key is read as `cd` (`.github/scripts/parse_atlan_yaml.py`), so omitting it
opts the app into fleet-wide publish-on-merge by default. Nothing else reports this: the
omission breaks no build and fails no test, and the effective model never appears in a
diff.

**This rule takes no side between `cd` and `semver`.** It fires only on an *undeclared*
or *invalid* value. Declaring `release_model: cd` explicitly satisfies it.

`versioned` is a deprecated alias for `semver`, normalised on read; it is reported so it
can be migrated.

**Fix -- where the key goes depends on whether the contract emits atlan.yaml.**
Determine that by running `pkl eval -m <out> contract/app.pkl` and checking whether
`atlan.yaml` appears in the output. Do not infer it from the presence of
`contract/app.pkl` or from which template the contract `amends` -- both are unreliable,
and a `NativeApp.pkl` contract can still emit `atlan.yaml` through its own
`additionalOutputFiles` block.

*The contract emits it* -- declare it at the source, in the pkl `metadata` mapping,
whose entries are emitted as top-level `atlan.yaml` keys (the same untyped hatch K011
prescribes for `app_id`), then regenerate:

    metadata {       ["release_model"] = "semver"     }

If the contract instead builds `atlan.yaml` itself via an inline
`additionalOutputFiles["atlan.yaml"]` mapping, add the key to that mapping. Either way,
never hand-edit a generated `atlan.yaml` -- K005 guards its provenance banner and the
next toolkit bump reverts the edit.

*The contract does not emit it* -- `atlan.yaml` is hand-owned; add `release_model:` to
it directly.

**Suppress** with `# conformance: ignore[K014] <reason>` on the first line of
`atlan.yaml` (or the line above the key). A suppression is rarely the right answer:
declaring the value is one line and is the entire point of the rule.

---
