DataShield Ontology · documentation

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.

On this page

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:

SurfaceEntry pointNotes
MCP umbrella data_catalogscripts/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.tsVerb table, door floors and arg validation DERIVED from the same wire contract + COMMAND_AUTH (route header lines 6-17)
Data Catalog appapp/app/data-catalog/ (dashboard + 11 sections) and app/data-catalog/lineage/page.tsxSection 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

CommandDoesRequired paramsFloorREST
provider_createRegisters 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_refkind, name (+ one identity)devPOST
provider_listLists providers you may see; filters kind, role; paginated (default 20, max 200 — LIST_LIMITS, config:4022)devGET
provider_getOne provider; detail minimal/compact(default: row + capabilities + summary tiles)/full(+ identity status)provider_iddevGET
provider_updatePatches name/role/cadences/sample_policy/resolver/notification; null clears to family/platform defaultprovider_iddev (owner or admin)POST
provider_retireSoft-delete (deleted_at), disables its crawlers, history keptprovider_id, confirm: truedev (owner-scoped; command-auth.config.ts:563)POST
provider_testProbes the identity source and PERSISTS the resulting healthprovider_iddevPOST

2.2 Crawlers and scans

CommandDoesRequired paramsFloorREST
crawler_createCreates a collection job over a provider. purposediscover | 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, namedevPOST
crawler_listCrawlers of one providerprovider_iddevGET
scan_runEnqueues 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_iddevPOST

2.3 Reading the estate

CommandDoesRequired paramsFloorREST
assets_listObjects 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_iddevGET
objects_searchCross-provider object browse; VALUE-FREE by SELECT list (drops attrs) — STANDARD-009 (wire.ts comment above the verb)devGET
asset_getOne object with the per-axis quality tableasset_iddevGET
columns_listColumns of ONE object incl. stored samples/profile documents; acknowledge_plain_samples governs whether plain samples are returned in the clearasset_iddevGET
columns_searchCross-object column browse; VALUE-FREE; q matches column name OR qualified object namedevGET
profile_getCurrent AnalysisContract profile of an object; sections[] to restrict; acknowledge_plain_samples as aboveasset_iddevGET
flow_summaryCollected → profiled → bound → mastered map, "every declared zero disclosed"; RFC 3339 from/to bound only the alerts legdevGET

2.4 Tags

CommandDoesRequired paramsFloorREST
asset_tag_setAdds 1–32 tags; already-present tags are a no-op; returns the FULL resulting setasset_id, tags[]dev (owner)POST
asset_tag_removeRemoves tags; absent tags a no-op; returns remaining setasset_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

CommandDoesRequired paramsFloorREST
terms_materializeRebuilds the glossary from MDM entity types + validation rules (+ product concepts per TERMS.defaultMode, modes entity_fields_only | entity_fields_plus_free, config:2774)confirm: trueadmin (command-auth.config.ts:553)POST
terms_listOne row per SOURCE entry; filters q, source (entity_field | product_concept | manual, config:2776), entity_type_id, include_retireddevGET
terms_grouped_listOne row per CANONICAL term (case-insensitive, whitespace-trimmed grouping, config/data-catalog-browse.config.ts); pages over TERMS, not entriesdevGET

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).

CommandDoesRequired paramsFloorREST
bind_inboundBinds an OWNED object to an entity type with field_map {field → {column, transform?, pk?}}; schedule default PT6H (config:2812); remaster_policyauto | agent_review | steward_review (config:2789); supersede:true versions a changed contractasset_id, field_map, one of entity_type_id/entity_typeadminPOST
bindings_listFilter by provider/asset/entity type(s) — entity_type_ids[] answers many types in ONE readdevGET
binding_getOne binding incl. last run and next extractbinding_iddevGET
unbindRetires the binding, deactivates its MDM mapping; history keptbinding_id, confirm: trueadminPOST
remasteractionrun | skip | rescope (config:2799); mode full/delta; a run writes MDM statebinding_id, action, confirm: trueadminPOST

2.7 REST-endpoint capture and enrichers

CommandDoesRequired paramsFloorREST
capture_payloadExecutes 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_iddevPOST
asset_register_enricherThin 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_idadmin (command-auth.config.ts:562)POST
helpKind labels, what each kind implements this release, governance sources, standards citeddevGET

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_DECLARATIONSPROVIDER_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.

KindFamilyIdentityPhasePathdiscoverprofileextractLine
postgresqlrelationalconnectionDC-1Ayesyesyes229
snowflakewarehouseconnectionDC-2Ayesyesyes254
databrickswarehouseconnectionDC-3A (@databricks/sql)yesyesyes279
bigquerywarehouseconnectionDC-3A (@google-cloud/bigquery)yesyesyes303
sqlserverrelationalconnectionDC-3Ayesyesyes314
mysqlrelationalconnectionDC-4A (mysql2)yesyesyes347
iceberg_restlakehouseconnectionDC-3B350
salesforcesaasconnectionDC-3Byes380
rest_apisaasconnectionDC-3Byesyes (reads a CAPTURED body)403
dynamics365saasconnectionDC-3B (Dataverse Web API, OAuth2 client-credentials)yes428
kafkastreamconnectionDC-2B (by ruling)434
s3object_storeconnectionDC-3Ayes443
azure_blob, gcs, sftp, ftp, remote_storageobject_storeconnectionDC-3ingest462-466
imapmailboxconnectionDC-4ingest467
socrata, tyler, ckan, arcgis, opendatasoft, dcat, otherportalcrawler_projectDC-2ingest469-474, 476
filesystemobject_storecrawler_projectDC-2ingest475

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_unrecordeddispatch.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.tsxDataCatalogDashboard.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 idLabelFeed verbWritesNeeds providerRole floor
overviewOverviewprovider_listnoREAD
estateEstateprovider_list (providers → objects → columns, each a filter for the next; app/app/data-catalog/lib/estate.ts)noREAD
providersProvidersprovider_listprovider_create/update/test/retirenoREAD
collectionsObjectsassets_listyesREAD
columnsColumnscolumns_searchnoREAD
tilesSensitivityprovider_getprovider_updateyesREAD
flowProcessing flowflow_summarynoREAD
scansScanscrawler_listcrawler_create, scan_runyesREAD
glossaryGlossaryterms_grouped_listadmin: terms_materializenoDEVELOPER
bindingsBindingsbindings_listadmin: bind_inbound/unbind/remasternoDEVELOPER
acknowledgementsAcknowledgementsprovider_getprovider_update, scan_run (+ SamplePolicyConsent.tsx)noDEVELOPER

Lineage pageapp/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.tsDATA_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.

CodeMeaningFix hint
provider_not_found / asset_not_found / crawler_not_found / binding_not_found / term_not_foundId 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_foundREST endpoint id absent/foreignrest.endpoint_list
connection_not_foundVault connection not yours/sharedlist your connections
provider_identity_required / provider_identity_mismatchZero or two identities, or wrong one for the kindconnection-backed kinds need connection_id; portal kinds need crawler_project_id
provider_kind_unknown / provider_kind_not_implemented / provider_not_scannableNot a declared kind / declared for a later release / kind lacks this purposehelp lists kinds and which implement discover/profile/extract
provider_existsOne provider per identity source (or crawler name taken)provider_list to find it
provider_in_useRetire refused while bindings feed masteringunbind {confirm:true} first
sample_policy_acknowledgement_requiredplain requested without acknowledgementadd acknowledge_plain_samples: true (SAMPLE_VALUE_POLICY.acknowledgementFixWhy)
invalid_cadenceNot ISO 8601 duration / RFC 5545 RRULEe.g. PT6H, P1D
invalid_tagTag breaks the grammarskeleton asset_tag_set {tags:["example-tag"]}; rule sentence in fix.why
scan_in_progressAnother scan holds the provider's harvest lockprovider_get, retry after it finishes
scan_budget_exhaustedObject budget reached, stopped cleanlyre-run to resume, or narrow scope
invalid_sweep_acknowledgement / sweep_acknowledgement_rejectedReason too short / acknowledgement expired, foreign crawler, or foreign principalre-issue scan_run {acknowledge_sweep:true, reason} (not job.retry)
credential_expired / cleartext_refused / credential_shape_invalid / host_unresolvableConnection-side defects; nothing about the credential was provenrecreate/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_unclassifiedClassified dial outcomes from provider_test/scanauth: 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_largeOAuth2 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_malformedEndpoint answered but not the expected metadata JSON / over byte ceilingcheck the endpoint
asset_not_profiled / profile_count_unavailableNo profile yet / row count undeterminablerun a profile crawler; profiling on an unknown denominator is refused
binding_dataset_requiredBinding masters the FULL extract datasetprofile crawler with scope.profiling_mode:"M3", scan_run, then bind
binding_exists / binding_contract_changed / binding_pk_unsupportedOne ACTIVE binding per (direction, asset, entity type) / contract differs / composite PKreuse or retire / supersede:true / mark exactly one pk:true on a unique column
entity_type_not_foundBad entity_type(_id)pass a registered entity type by id or exact name
remaster_not_pending / run_in_progress / automation_stoppedNothing pending / a run already queued / MDM pause or kill flagbinding_get / job.list_mine / mdm.control_clear
asset_not_endpoint / enricher_refused / endpoint_unbound_not_catalogableWrong asset kind for an enricher / MDM writer refused / endpoint has no bound connectionassets_list {kind:"endpoint"} / read context.reason / bind the endpoint to a connection you own
strategy_unavailableauth_broker resolver not usable for this connection type yetuse local_vault
driver_unavailableThis 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_unresolvableCaller identity resolves to no accountverify your key (writes never create NULL-owner rows)
provider_readonly_violation / write_dialer_not_declared / write_shape_required / write_shape_lane_mismatchCatalog sessions are READ-only at the connection boundary; no write grammar declared this releasenot actionable by callers this release (write:false for every kind, §3)
governance_read_unavailableRequested governance source not servable for this kindhelp → governance_control
param_retiredProject-level sampling_config is deaddataset_admin.settings_update
self_database_undeterminablePlatform cannot prove the provider is not itself; fails closedoperator action

10. Trust notes

Measured against the release named in the text. Raw Markdown: /ontology/documentation/data-catalog.md. This page is also served as raw Markdown at the same URL with a .md suffix, for agents and search tools that prefer plain text. Every capability statement cites the source file and line it was measured from.

You've seen the proof

Ready for a number? Scope your deployment and we'll price it against your own economics.

Get your quote →