Distribution Contract: authoring and consuming the distribution-contract aspect

The built-in distribution-contract aspect describes how a distribution can be accessed or queried: what kind of resource it is, its protocol and endpoint, where its native specification and documentation are, what authentication the source expects, a curated list of useful operations with their scalar parameters, request/response metadata (with links to data-dictionary entities), pagination hints and what the source reports it supports. It records provenance and review state throughout.

The aspect is optional and additive: distributions without it render exactly as before. When it is present, the distribution page shows a read-only How to use tab (protocol/access mode, endpoint, documentation/specification links, authentication description, source capabilities, operations with purposes and parameters, request/response entities linked to the Structure tab, pagination hints, provenance/review state and last verification time). It sits next to Structure, below the existing previews, and is shown even when the distribution cannot be previewed. Link to it with #how-to-use on the distribution page URL.

Automatic producers (harvesting from OpenAPI, ArcGIS, OGC/STAC, …, tracked in #3814) are not required: contracts can be authored manually today, and manual/custodian input remains a first-class source once producers exist.

The contract is descriptive, not execution authority

Minimal payloads

A downloadable file needs very little:

{
  "schemaVersion": "1.0",
  "resourceRole": "download",
  "accessMode": "public-download",
  "protocol": "HTTP",
  "endpointUrl": "https://example.org/data.csv",
  "provenance": { "method": "manual", "reviewStatus": "reviewed" }
}

An API with one curated operation:

{
  "schemaVersion": "1.0",
  "resourceRole": "data-service",
  "accessMode": "public-query",
  "protocol": "REST",
  "endpointUrl": "https://example.org/api",
  "documentationUrl": "https://example.org/docs",
  "specification": {
    "type": "openapi",
    "url": "https://example.org/openapi.json",
    "version": "3.1.0"
  },
  "authentication": { "type": "none" },
  "operations": [
    {
      "id": "search-occurrences",
      "sourceIdentifier": "searchOccurrences",
      "label": "Search occurrence records",
      "purpose": "Search records by taxon",
      "interactionType": "query",
      "method": "GET",
      "path": "/occurrences/search",
      "endpointUrl": "https://example.org/api/occurrences/search",
      "parameters": [
        {
          "name": "q",
          "location": "query",
          "type": "string",
          "required": true
        },
        {
          "name": "pageSize",
          "location": "query",
          "type": "integer",
          "default": 10,
          "maximum": 50
        },
        {
          "name": "offset",
          "location": "query",
          "type": "integer",
          "default": 0
        }
      ],
      "response": {
        "mediaTypes": ["application/json"],
        "recordsPath": "/occurrences",
        "dictionaryEntity": "occurrence-record",
        "pagination": {
          "type": "offset-limit",
          "limitParameter": "pageSize",
          "offsetParameter": "offset",
          "totalPath": "/total"
        }
      }
    }
  ],
  "provenance": { "method": "manual", "reviewStatus": "unreviewed" }
}

Only schemaVersion is required at the top level. An operation requires a non-empty id. A parameter requires a non-empty name, location (path, query, header or cookie) and type (a data-dictionary logical type such as string, integer, number, boolean, date, datetime; use "unknown" when no useful type is known). Everything else is optional. Vocabularies (resourceRole, accessMode, protocol, interactionType, pagination type, …) are open, and unknown additional properties are allowed and must be ignored by consumers.

Identities, endpoints and references

Provenance and review state

Provenance works exactly as in the Data Dictionary and uses the same helpers. It is inherited, nearest wins (no merging):

  1. propertyProvenance[<property>] on the node (for a single property, e.g. an operation’s purpose or a parameter’s description);
  2. the node’s own provenance (parameter → operation);
  3. the contract-level provenance.

method is one of authoritative-import, harvested, manual, agent-generated, inferred (open vocabulary); reviewStatus is one of unreviewed, reviewed, custodian-approved, rejected. Every provenance object must state method and/or reviewStatus, so an empty or detail-only object can’t hide inherited (possibly protected) provenance.

Use property-level provenance for mixed-origin metadata, e.g. a harvested operation with a custodian-written purpose:

{
  "id": "list-sites",
  "sourceIdentifier": "listSites",
  "method": "GET",
  "path": "/sites",
  "purpose": "Browse every monitoring site; start here to find site codes",
  "propertyProvenance": {
    "purpose": { "method": "manual", "reviewStatus": "custodian-approved" }
  }
}

Record what the contract was built from and when it was checked:

Rules for anything that writes the aspect, including future automatic producers and agents:

Populating a contract with mgd

The generic aspect commands work on distribution records; no contract-specific command is needed:

# create or replace the whole contract
mgd dataset aspect set <distribution-id> distribution-contract @distribution-contract.json

# read it back (machine-readable)
mgd dataset aspect get <distribution-id> distribution-contract --json

mgd dataset aspect patch performs a server-side JSON merge. Objects are merged key by key, but arrays are combined, not matched by id: patching operations or parameters appends changed elements alongside the old ones and produces duplicates (which validateDistributionContract() reports). Use patch only for top-level objects such as provenance, specification or authentication:

mgd dataset aspect patch <distribution-id> distribution-contract '{"lastVerified":"2026-10-03T00:00:00Z"}'

For a targeted edit inside operations (e.g. one operation’s purpose) send an RFC 6902 JSON Patch to the registry instead, addressing the element by index (read the aspect first to find it):

cat > patch.json <<'EOF'
[
    { "op": "add", "path": "/operations/0/purpose", "value": "Browse every monitoring site; start here to find site codes" },
    { "op": "add", "path": "/operations/0/propertyProvenance", "value": { "purpose": { "method": "manual", "reviewStatus": "custodian-approved" } } }
]
EOF
mgd api request PATCH /v0/registry/records/<distribution-id>/aspects/distribution-contract --body-file patch.json

The registry validates aspect data against the JSON schema only when its validateJsonSchema option is enabled (the normal Helm deployment enables it; local/custom deployments may not), so validate before writing.

No protocol-specific mgd execution command is implied by this authoring path: the contract describes how to call a source, and mgd only reads and writes the metadata.

Validating and consuming programmatically

import { validateDistributionContract } from "@magda/typescript-common/dist/distribution-contract/validate.js";
import { normalizeDistributionContract } from "@magda/typescript-common/dist/distribution-contract/normalize.js";
import {
  getEffectiveAuthentication,
  getParameterIdentity
} from "@magda/typescript-common/dist/distribution-contract/contract.js";
import {
  getEffectiveProvenance,
  isAdvisoryProvenance,
  isProtectedProvenance
} from "@magda/typescript-common/dist/distribution-contract/provenance.js";

Machine clients and agents should derive tool descriptions from the normalized contract, not raw aspect JSON: the distribution id + operation id give a stable tool identity, and interactionType, parameters (with getParameterIdentity()), request/response entities (resolved through the data-dictionary), recordsPath and pagination hints describe the call deterministically. Executing a call still requires an approved adapter that resolves the endpoint, credentials (through a delegated-auth system, never from this aspect), network policy and request/response limits.