# 02 — Data Catalog

*Measured from the codebase at v0.22.188 (2026-09-22). Every claim cites `file:line`; where a claim is inferred rather than read, it says so. Audience: developers, governance teams, analysts and AI agents driving the `data_catalog` umbrella.*

## 1. What the Data Catalog is

The Data Catalog is the platform's **system-object estate map**: registered *providers* (a database, warehouse, SaaS API, storage bucket or open-data portal), the *objects* (tables, views, schemas, endpoints…) a scan discovered under them, the *columns* of those objects with their type/class/constraints, optional *profiles*, business-term *glossary*, and *bindings* that feed catalogued objects into MDM mastering.

It is exposed on three parity surfaces built from ONE contract:

| Surface | Entry point | Notes |
|---|---|---|
| MCP umbrella `data_catalog` | `scripts/mcp-server/umbrellas/data_catalog.ts` (111 lines; kernel-only imports, "no logic in the umbrella", header lines 1-11) | Zod union `DATA_CATALOG_COMMAND_SCHEMA` in `lib/data_catalog/wire.ts:40`; executor `dispatchDataCatalog` in `lib/data_catalog/dispatch.ts` |
| REST bridge `/api/app/data_catalog/<verb>` | `app/api/app/data_catalog/[verb]/route.ts` | Verb table, door floors and arg validation DERIVED from the same wire contract + `COMMAND_AUTH` (route header lines 6-17) |
| Data Catalog app | `app/app/data-catalog/` (dashboard + 11 sections) and `app/data-catalog/lineage/page.tsx` | Section registry is data: `config/data-catalog-nav.config.ts:181-212` |

Tier floor: the umbrella is a **developer-tier** addition (`config/mcp-tiers.config.ts:186-187`, in `DEVELOPER_TIER_ADDITIONS` declared at line 174). Per-command floors above that are in `config/command-auth.config.ts:551-564`; ownership gates (owner-or-admin, per-account rows) are in `config/authorization-policy.config.ts:556` (`UMBRELLA_COMMAND_GATES.data_catalog`) and run BEFORE the switch (umbrella header line 9).

## 2. Command reference

Verbs are derived, never hand-listed (`DATA_CATALOG_VERBS`, `wire.ts:328`). 29 commands (counted from the discriminated union; `DATA_CATALOG_VERB_METHODS` carries the same 29 keys — 15 GET, 14 POST). "Floor" = developer tier unless `admin` is stated (`command-auth.config.ts:551-564`). REST method per verb from `DATA_CATALOG_VERB_METHODS` (`wire.ts:411-419`).

### 2.1 Providers

| Command | Does | Required params | Floor | REST |
|---|---|---|---|---|
| `provider_create` | Registers a provider over exactly ONE identity source: `connection_id` (vault connection; database/warehouse/storage/SaaS/stream kinds) or `crawler_project_id` (portal kinds). Optional `role` (source/target/both, default source), `scan_cadence`/`extract_cadence` (ISO 8601 duration or RFC 5545 RRULE), `sample_policy`, `resolver_strategy` (local_vault/auth_broker), `notification_channel_ref` | `kind`, `name` (+ one identity) | dev | POST |
| `provider_list` | Lists providers you may see; filters `kind`, `role`; paginated (default 20, max 200 — `LIST_LIMITS`, config:4022) | — | dev | GET |
| `provider_get` | One provider; `detail` minimal/compact(default: row + capabilities + summary tiles)/full(+ identity status) | `provider_id` | dev | GET |
| `provider_update` | Patches name/role/cadences/sample_policy/resolver/notification; `null` clears to family/platform default | `provider_id` | dev (owner or admin) | POST |
| `provider_retire` | Soft-delete (`deleted_at`), disables its crawlers, history kept | `provider_id`, `confirm: true` | dev (owner-scoped; command-auth.config.ts:563) | POST |
| `provider_test` | Probes the identity source and PERSISTS the resulting health | `provider_id` | dev | POST |

### 2.2 Crawlers and scans

| Command | Does | Required params | Floor | REST |
|---|---|---|---|---|
| `crawler_create` | Creates a collection job over a provider. `purpose` ∈ `discover` \| `profile` \| `extract` \| `rescan` (`CRAWLER_PURPOSES`, config:910). Scope JSON: `schemas[]`, `include_glob`, `exclude_glob`, `object_kinds[]`, `sample_policy`, `profiling_mode` (M0–M3, config:1002), `max_entities`. `schedule_cron` (5-field) or null = manual. Tunables: `concurrency` 1–10 (default 2), `rate_limit_rps` 0.1–10 (default 1), `max_requests_per_run` ≤ 50000 (default 500) | `provider_id`, `purpose`, `name` | dev | POST |
| `crawler_list` | Crawlers of one provider | `provider_id` | dev | GET |
| `scan_run` | Enqueues one scan for a crawler project. Optional `acknowledge_sweep: true` + `reason` (12–500 chars) to ratify a sweep the blast-radius guard refused (see §5) | `crawler_project_id` | dev | POST |

### 2.3 Reading the estate

| Command | Does | Required params | Floor | REST |
|---|---|---|---|---|
| `assets_list` | Objects IN one provider; filters `kind`, `parent_id` (null = top level), `q`, `include_deleted`. Carries per-object `attrs` document plus `column_count`, `child_count`, `tags`, `quality` (`assets.ts:83`) | `provider_id` | dev | GET |
| `objects_search` | Cross-provider object browse; VALUE-FREE by SELECT list (drops `attrs`) — STANDARD-009 (`wire.ts` comment above the verb) | — | dev | GET |
| `asset_get` | One object with the per-axis quality table | `asset_id` | dev | GET |
| `columns_list` | Columns of ONE object incl. stored samples/profile documents; `acknowledge_plain_samples` governs whether `plain` samples are returned in the clear | `asset_id` | dev | GET |
| `columns_search` | Cross-object column browse; VALUE-FREE; `q` matches column name OR qualified object name | — | dev | GET |
| `profile_get` | Current AnalysisContract profile of an object; `sections[]` to restrict; `acknowledge_plain_samples` as above | `asset_id` | dev | GET |
| `flow_summary` | Collected → profiled → bound → mastered map, "every declared zero disclosed"; RFC 3339 `from`/`to` bound only the alerts leg | — | dev | GET |

### 2.4 Tags

| Command | Does | Required params | Floor | REST |
|---|---|---|---|---|
| `asset_tag_set` | Adds 1–32 tags; already-present tags are a no-op; returns the FULL resulting set | `asset_id`, `tags[]` | dev (owner) | POST |
| `asset_tag_remove` | Removes tags; absent tags a no-op; returns remaining set | `asset_id`, `tags[]` | dev (owner) | POST |

Tag grammar (ONE sentence, `ASSET_TAG_RULE_SENTENCE` at config:807-809, rendered from `ASSET_TAGS`, config:781-792): lowercase, 1–64 chars, starts with a letter or digit, may contain `.`, `:`, `-`, `_` inside (e.g. `pii:reviewed`, `team.finance`). The DB CHECK `asset_tags_tag_check` (migration 0235) retypes the same regex; `audit:catalog-config-agreement` binds the two (config:795-797).

### 2.5 Glossary

| Command | Does | Required params | Floor | REST |
|---|---|---|---|---|
| `terms_materialize` | Rebuilds the glossary from MDM entity types + validation rules (+ product concepts per `TERMS.defaultMode`, modes `entity_fields_only` \| `entity_fields_plus_free`, config:2774) | `confirm: true` | **admin** (command-auth.config.ts:553) | POST |
| `terms_list` | One row per SOURCE entry; filters `q`, `source` (`entity_field` \| `product_concept` \| `manual`, config:2776), `entity_type_id`, `include_retired` | — | dev | GET |
| `terms_grouped_list` | One row per CANONICAL term (case-insensitive, whitespace-trimmed grouping, `config/data-catalog-browse.config.ts`); pages over TERMS, not entries | — | dev | GET |

### 2.6 Bindings and remaster (MDM seam)

These reach the same writers the `mdm` umbrella admin-floors, so they are admin-floored too ("a second door must not be a lower floor than the first", `command-auth.config.ts:554-558`).

| Command | Does | Required params | Floor | REST |
|---|---|---|---|---|
| `bind_inbound` | Binds an OWNED object to an entity type with `field_map` {field → {column, transform?, pk?}}; `schedule` default `PT6H` (config:2812); `remaster_policy` ∈ `auto` \| `agent_review` \| `steward_review` (config:2789); `supersede:true` versions a changed contract | `asset_id`, `field_map`, one of `entity_type_id`/`entity_type` | **admin** | POST |
| `bindings_list` | Filter by provider/asset/entity type(s) — `entity_type_ids[]` answers many types in ONE read | — | dev | GET |
| `binding_get` | One binding incl. last run and next extract | `binding_id` | dev | GET |
| `unbind` | Retires the binding, deactivates its MDM mapping; history kept | `binding_id`, `confirm: true` | **admin** | POST |
| `remaster` | `action` ∈ `run` \| `skip` \| `rescope` (config:2799); `mode` full/delta; a run writes MDM state | `binding_id`, `action`, `confirm: true` | **admin** | POST |

### 2.7 REST-endpoint capture and enrichers

| Command | Does | Required params | Floor | REST |
|---|---|---|---|---|
| `capture_payload` | Executes a saved `rest` endpoint (must be bound to a connection) and CAPTURES its body as a catalogued payload; optional `dataset_id` attaches the file for ingest. Envelope keys (`CAPTURE_PAYLOAD_KEYS`, config:856-863): `http_response_code`, `catalog_enrolment` (provider_id, crawler_project_id, job_id, created flags), `scan_status` (the ENQUEUED discover scan's state — never the HTTP response's) | `endpoint_id` | dev | POST |
| `asset_register_enricher` | Thin wrapper over `mdm.enricher_create`: an `endpoint`-kind asset becomes a `rest_lookup` enricher for an entity type (`input_fields`, `output_attrs`, `spec:{var_map,json_path}`, `stage`, `failure_policy` skip/null/fail/flag, `exec_order` default 100) | `asset_id`, `name`, `input_fields`, `output_attrs`, `spec`, one of `entity_type`/`entity_type_id` | **admin** (command-auth.config.ts:562) | POST |
| `help` | Kind labels, what each kind implements this release, governance sources, standards cited | — | dev | GET |

### 2.8 Typical sequence

```
provider_create {kind, name, connection_id}      → provider_id
provider_test   {provider_id}                    → health persisted (dial/credential classified)
crawler_create  {provider_id, purpose:"discover", name, scope?} → crawler_project_id
scan_run        {crawler_project_id}             → job enqueued (job.* to follow it)
assets_list     {provider_id}                    → asset_id …
asset_get / columns_list {asset_id}
crawler_create  {purpose:"profile", scope:{profiling_mode:"M3"}} → scan_run → profile_get {asset_id}
bind_inbound    {asset_id, entity_type, field_map}   (admin; needs an M3 extract dataset — see errors)
```

## 3. Provider kinds — what each implements today

Source: `PROVIDER_KIND_DECLARATIONS` → `PROVIDER_KINDS` (`config/data-catalog.config.ts:227-477`, derivation at 483); 26 declared kinds. "Identity" below is the row's `auth_source` field (`config:119`). `path` follows STANDARD-010 §1: **A** = native dial with a declared driver module, **B** = REST + vault credential, **ingest** = reached through a pre-existing ingest/crawler lane (not the catalog's own dial). `implemented` flags are `{discover, profile, extract, write, stream}`; `write`/`stream` are `false` for every kind in this release.

| Kind | Family | Identity | Phase | Path | discover | profile | extract | Line |
|---|---|---|---|---|---|---|---|---|
| `postgresql` | relational | connection | DC-1 | A | yes | yes | yes | 229 |
| `snowflake` | warehouse | connection | DC-2 | A | yes | yes | yes | 254 |
| `databricks` | warehouse | connection | DC-3 | A (`@databricks/sql`) | yes | yes | yes | 279 |
| `bigquery` | warehouse | connection | DC-3 | A (`@google-cloud/bigquery`) | yes | yes | yes | 303 |
| `sqlserver` | relational | connection | DC-3 | A | yes | yes | yes | 314 |
| `mysql` | relational | connection | DC-4 | A (`mysql2`) | yes | yes | yes | 347 |
| `iceberg_rest` | lakehouse | connection | DC-3 | B | — | — | — | 350 |
| `salesforce` | saas | connection | DC-3 | B | yes | — | — | 380 |
| `rest_api` | saas | connection | DC-3 | B | yes | — | yes (reads a CAPTURED body) | 403 |
| `dynamics365` | saas | connection | DC-3 | B (Dataverse Web API, OAuth2 client-credentials) | yes | — | — | 428 |
| `kafka` | stream | connection | DC-2 | B (by ruling) | — | — | — | 434 |
| `s3` | object_store | connection | DC-3 | A | yes | — | — | 443 |
| `azure_blob`, `gcs`, `sftp`, `ftp`, `remote_storage` | object_store | connection | DC-3 | ingest | — | — | — | 462-466 |
| `imap` | mailbox | connection | DC-4 | ingest | — | — | — | 467 |
| `socrata`, `tyler`, `ckan`, `arcgis`, `opendatasoft`, `dcat`, `other` | portal | crawler_project | DC-2 | ingest | — | — | — | 469-474, 476 |
| `filesystem` | object_store | crawler_project | DC-2 | ingest | — | — | — | 475 |

A declared-but-unbuilt kind is admitted by the enum but `provider_create`/`scan_run` refuse with `provider_kind_not_implemented` / `provider_not_scannable` (§9). Harvest lanes on disk: `lib/data_catalog/harvest/{postgres,snowflake,databricks,bigquery,sqlserver,mysql,rest_api,s3,s3Lake,saas,salesforce,dynamics365}.ts`; profile lanes: `lib/data_catalog/profile/{postgres,snowflake,databricks,bigquery,sqlserver,mysql}.ts`; extract lanes: `lib/data_catalog/extract/{postgres,rest_api}.ts` plus `extractDialects.ts` (other SQL engines share the dialect table — inferred from filenames, not read).

## 4. Sample-value policy ladder

`SAMPLE_VALUE_POLICIES = ["plain","masked","omit"]` (config:1110). Default **`omit`** — no source values stored. Standards cited on the wire: GDPR Art. 5(1)(c), Art. 25, HIPAA 45 CFR 164.502(b) (config:1135). Detection (PII pattern label, `lib/data_catalog/piiLabel.ts`) ALWAYS runs; only PERSISTING values is gated (config:1107-1108).

Resolution ladder, most specific wins (`SAMPLE_VALUE_POLICY.ladder`, config:1128): **column → scope (crawler) → provider → platform (`admin.settings_set` key `data_catalog.sample_value_policy`) → config default**. Resolver: `resolveSamplePolicy` (`lib/data_catalog/policyLadder.ts:72`) returns `{policy, source}` so every read can say which rung decided.

Acknowledgement: `plain` is in `requiresAcknowledgement` (config:1133). Setting it at `provider_create`/`provider_update` or in a crawler `scope` REQUIRES `acknowledge_plain_samples: true`; the acknowledgement is written to the job log as an audit row. Reading stored plain values back (`columns_list`, `profile_get`) again requires `acknowledge_plain_samples: true` on the read. The UI's **Acknowledgements** section (§7) lists providers holding values in the clear and the recorded decision.

## 5. Scan purposes and the blast-radius sweep guard

Purposes (config:910): `discover` (harvest objects/columns), `profile` (column statistics into an AnalysisContract, modes M0–M3), `extract` (read a relation's ROWS into a dataset for mastering), `rescan`. Handlers: `lib/data_catalog/scan/{run,handlers}.ts`; applier `lib/data_catalog/harvest/apply.ts`.

**Sweep guard** (`SWEEP_GUARD`, config:3066-3069): after a harvest, objects no longer seen would be soft-deleted ("swept"). The applier REFUSES the sweep (`sweep_refused`, `apply.ts:263-271`) when (a) the harvest saw ZERO objects while the estate still held live ones (`zero_objects`), or (b) the estate before had ≥ 5 live objects (`minEstateForFractionGuard`) and the delete would exceed 50 % of it (`maxFractionDeleted`). Reasons vocabulary `SWEEP_GUARD_REASONS = ["zero_objects","magnitude"]` (config:3063). Contract violations (harvester spelled `scanned_schemas` in an undeclared vocabulary) are a SEPARATE, non-ratifiable vocabulary (config:3072-3080).

**Acknowledgement** (`SWEEP_ACKNOWLEDGEMENT`, config:3189+): `scan_run {acknowledge_sweep:true, reason}` — reason 12–500 chars; persisted under `catalog.scans.scope.acknowledge_sweep`; **consumed once**; **expires** after the declared ISO 8601 TTL `PT1H` (`ttl`, config:3203); **bound** to the crawler project AND to the principal the job is attributed to; **not carried through `job.retry`** (discloses `acknowledgement_not_carried`). An expired/foreign/malformed carried acknowledgement is refused with `sweep_acknowledgement_rejected` — never silently ignored (`wire.ts` scan_run describe).

## 6. Quality scoring at read time

`quality_basis` is ALWAYS `derived_at_read` (`QUALITY_BASIS`, `config/asset-quality.config.ts:253`; wire `wire.ts:373`): the headline `overall` (0–100 or null) is projected from stored per-axis measurements and the profile's sampling denominator by `projectAssetQuality` (`lib/data_catalog/profile/qualityProjection.ts:140`) — a stored headline is never served, and a 0-row profile reads as unmeasurable estate-wide with no backfill. `assets_list` returns the compact HEADLINE projection only — no prior contract, so no trend on a list page (`assets.ts:104`); `asset_get` carries the per-axis table and compares against the prior profile for trend (`assets.ts:163-164`). When no honest headline exists, `quality_unavailable_reason` names why (`QUALITY_UNAVAILABLE_REASONS`, asset-quality.config.ts:183, e.g. `extract_quality_in_dataset_store`, `extract_backing_dataset_unrecorded` — `dispatch.ts:744`). Privacy runs first: `withholdUnmeasuredContractScores` (`dispatch.ts:777-816`) strips producer-side scores the kernel did not measure.

## 7. The Data Catalog app

Route `/app/data-catalog` (`app/app/data-catalog/page.tsx` → `DataCatalogDashboard.tsx`). Sections are data in `config/data-catalog-nav.config.ts:181-212` (order IS the nav) and bound to components in `app/app/data-catalog/sections/index.ts:35-47`; `checkSectionRegistry` fails at module load if any declared section lacks a component. Each row declares its `feed` verb, non-admin `writes`, `adminWrites` (⊆ the admin-floored commands) and `needsProvider`; `test:data-catalog-shell` L6 asserts those against the live config.

| Section id | Label | Feed verb | Writes | Needs provider | Role floor |
|---|---|---|---|---|---|
| `overview` | Overview | `provider_list` | — | no | READ |
| `estate` | Estate | `provider_list` (providers → objects → columns, each a filter for the next; `app/app/data-catalog/lib/estate.ts`) | — | no | READ |
| `providers` | Providers | `provider_list` | provider_create/update/test/retire | no | READ |
| `collections` | Objects | `assets_list` | — | yes | READ |
| `columns` | Columns | `columns_search` | — | no | READ |
| `tiles` | Sensitivity | `provider_get` | provider_update | yes | READ |
| `flow` | Processing flow | `flow_summary` | — | no | READ |
| `scans` | Scans | `crawler_list` | crawler_create, scan_run | yes | READ |
| `glossary` | Glossary | `terms_grouped_list` | admin: terms_materialize | no | DEVELOPER |
| `bindings` | Bindings | `bindings_list` | admin: bind_inbound/unbind/remaster | no | DEVELOPER |
| `acknowledgements` | Acknowledgements | `provider_get` | provider_update, scan_run (+ `SamplePolicyConsent.tsx`) | no | DEVELOPER |

**Lineage page** — `app/data-catalog/lineage/page.tsx` (segment `LINEAGE_ROUTE_SEGMENT = "lineage"`, label "Lineage", `config/lineage-view.config.ts:348-351`; nav row at `config/app-nav.config.ts:464`). It is anchored on one asset (`?start=<type>:<id>`, parsed by `lib/server/lineage/contract.ts`) and draws the derivation tree + stored relationships via `components/lineage/LineageCanvas`. Its **honest-empty state** is deliberate data, not a defect (`LINEAGE_EMPTY_COPY`, lineage-view.config.ts:368-377): `noAnchor` — "Pick something to trace"; `noLayers` — "No derived files and no snapshots": a typical sub-1GB CSV genuinely has one original file and no derivation layer; layers appear when something derives from it — `dataset_snapshot.create` writes a snapshot, and the declarative JSON ingest writes a transform step per pipeline operation. Rendered through `components/flow/ui` `HonestEmpty`. The config self-check refuses a view with no enabled `VIEW_SOURCES` (line 881). The catalog's own dependency edges from harvest (`dependency_edges` in `ApplyStats`, `apply.ts:51`, written at `apply.ts:489`) are recorded, but whether they feed this canvas was not measured here.

## 8. REST bridge

`app/api/app/data_catalog/[verb]/route.ts` is the single generic dispatcher (header lines 6-17): verb table from `_bridge/registry.ts` ← `DATA_CATALOG_VERB_METHODS` (`wire.ts:411-419`); door floor per verb from `COMMAND_AUTH.data_catalog` + tier config; args parsed by the same Zod union; executed by the same `dispatchDataCatalog` inside a request scope carrying the REAL principal (`resolveCatalogPrincipal`; the DC-2 fix that removed a constant `isAdmin: true`, lines 18-27). Success: `200 { data }`. Refusal: RFC 9457 problem+json (`lib/server/problem.ts`) whose `code` is the registry key and status from `REFUSAL_HTTP_STATUS`; door refusals are explicit 403 with their own `type`. Usage metered as `web:data_catalog.<verb>`.

GET verbs: `help, provider_list, provider_get, crawler_list, assets_list, asset_get, columns_list, columns_search, objects_search, profile_get, terms_list, terms_grouped_list, bindings_list, binding_get, flow_summary`. POST verbs: every writer (`provider_create/update/test/retire, crawler_create, scan_run, terms_materialize, bind_inbound, unbind, remaster, asset_tag_set/remove, capture_payload, asset_register_enricher`).

## 9. Common errors and their fix hints

From the ONE registry `lib/mcp/dataCatalogErrors.ts` (envelope `{ok:false, error:{code, message, retryable, fix:{tool, command?, args, why}}}`, line 9). The registry declares 58 codes; the table below covers all 58. Hints are the registry's own `fix.why`, condensed.

| Code | Meaning | Fix hint |
|---|---|---|
| `provider_not_found` / `asset_not_found` / `crawler_not_found` / `binding_not_found` / `term_not_found` | Id absent — or foreign; foreign ≡ absent (not-found parity) | `provider_list` / `assets_list {provider_id}` / `crawler_list` / `bindings_list` / `terms_list` to obtain a valid id |
| `endpoint_not_found` | REST endpoint id absent/foreign | `rest.endpoint_list` |
| `connection_not_found` | Vault connection not yours/shared | list your connections |
| `provider_identity_required` / `provider_identity_mismatch` | Zero or two identities, or wrong one for the kind | connection-backed kinds need `connection_id`; portal kinds need `crawler_project_id` |
| `provider_kind_unknown` / `provider_kind_not_implemented` / `provider_not_scannable` | Not a declared kind / declared for a later release / kind lacks this purpose | `help` lists kinds and which implement discover/profile/extract |
| `provider_exists` | One provider per identity source (or crawler name taken) | `provider_list` to find it |
| `provider_in_use` | Retire refused while bindings feed mastering | `unbind {confirm:true}` first |
| `sample_policy_acknowledgement_required` | `plain` requested without acknowledgement | add `acknowledge_plain_samples: true` (`SAMPLE_VALUE_POLICY.acknowledgementFixWhy`) |
| `invalid_cadence` | Not ISO 8601 duration / RFC 5545 RRULE | e.g. `PT6H`, `P1D` |
| `invalid_tag` | Tag breaks the grammar | skeleton `asset_tag_set {tags:["example-tag"]}`; rule sentence in `fix.why` |
| `scan_in_progress` | Another scan holds the provider's harvest lock | `provider_get`, retry after it finishes |
| `scan_budget_exhausted` | Object budget reached, stopped cleanly | re-run to resume, or narrow scope |
| `invalid_sweep_acknowledgement` / `sweep_acknowledgement_rejected` | Reason too short / acknowledgement expired, foreign crawler, or foreign principal | re-issue `scan_run {acknowledge_sweep:true, reason}` (not `job.retry`) |
| `credential_expired` / `cleartext_refused` / `credential_shape_invalid` / `host_unresolvable` | Connection-side defects; nothing about the credential was proven | recreate/re-store the connection (https, declared shape, future `expires_at`) |
| `provider_dial_auth_failed` / `provider_dial_timeout` / `provider_dial_unreachable` / `provider_dial_config_rejected` / `provider_dial_failed_unclassified` | Classified dial outcomes from `provider_test`/scan | auth: fix stored credential (retry changes nothing); timeout/unreachable: retry, check host/egress; config: check database/catalog/bucket name; unclassified: `details.driver_code` |
| `token_acquire_failed` / `token_endpoint_unreachable` / `credential_rejected` / `token_response_too_large` | OAuth2 grant failures (Path B kinds) | retry later / verify token URL / fix client id, secret, scope, audience / RFC 6749 §5.1 response too large |
| `provider_response_malformed` | Endpoint answered but not the expected metadata JSON / over byte ceiling | check the endpoint |
| `asset_not_profiled` / `profile_count_unavailable` | No profile yet / row count undeterminable | run a `profile` crawler; profiling on an unknown denominator is refused |
| `binding_dataset_required` | Binding masters the FULL extract dataset | profile crawler with `scope.profiling_mode:"M3"`, `scan_run`, then bind |
| `binding_exists` / `binding_contract_changed` / `binding_pk_unsupported` | One ACTIVE binding per (direction, asset, entity type) / contract differs / composite PK | reuse or retire / `supersede:true` / mark exactly one `pk:true` on a unique column |
| `entity_type_not_found` | Bad `entity_type(_id)` | pass a registered entity type by id or exact name |
| `remaster_not_pending` / `run_in_progress` / `automation_stopped` | Nothing pending / a run already queued / MDM pause or kill flag | `binding_get` / `job.list_mine` / `mdm.control_clear` |
| `asset_not_endpoint` / `enricher_refused` / `endpoint_unbound_not_catalogable` | Wrong asset kind for an enricher / MDM writer refused / endpoint has no bound connection | `assets_list {kind:"endpoint"}` / read `context.reason` / bind the endpoint to a connection you own |
| `strategy_unavailable` | `auth_broker` resolver not usable for this connection type yet | use `local_vault` |
| `driver_unavailable` | This deploy does not carry the kind's declared driver module (Path A dial) | a DEPLOYMENT state, not a credential one — install the release's dependencies (`npm ci`); other kinds unaffected |
| `owner_unresolvable` | Caller identity resolves to no account | verify your key (writes never create NULL-owner rows) |
| `provider_readonly_violation` / `write_dialer_not_declared` / `write_shape_required` / `write_shape_lane_mismatch` | Catalog sessions are READ-only at the connection boundary; no write grammar declared this release | not actionable by callers this release (`write:false` for every kind, §3) |
| `governance_read_unavailable` | Requested governance source not servable for this kind | `help → governance_control` |
| `param_retired` | Project-level `sampling_config` is dead | `dataset_admin.settings_update` |
| `self_database_undeterminable` | Platform cannot prove the provider is not itself; fails closed | operator action |

## 10. Trust notes

- Everything above was read from source; nothing was executed against the live database, so counts (e.g. glossary size "18,223 entries" quoted in `wire.ts` comments) are as of the comment, not measured now.
- Unmeasured here: the full `QUALITY_UNAVAILABLE_REASONS` vocabulary, the per-kind governance-source table (`help`), and whether harvest `dependency_edges` reach the lineage canvas.
- Standards applied on this surface: ISO 8601 durations / RFC 5545 RRULE for cadences, RFC 3339 for `flow_summary` bounds, RFC 9457 for REST refusals (MCP surface uses the compact envelope), RFC 9110 §15 for `http_response_code`, GDPR Art. 5/25 and HIPAA 164.502(b) for the sample-value default.
