Design Note: Artifact Claims And Facets

This note records the first claim and facet schema slice from ADR 0002. It is limited to JSON-compatible persistence, validation, and documentation. It does not implement reader dispatch, writer registration, converters, or optional scientific readers.

Chosen Shape

Artifact claims use one open envelope with a kind field:

{
  "kind": "interface",
  "name": "bytes",
  "namespace": "ogcat.core",
  "version": "1",
  "evidence": "validated",
  "confidence": "validated",
  "metadata": {}
}

Core recognizes three standard claim kinds from ADR 0002:

  • data_type for source or external forms such as NetCDF, CSV, service, or collection pattern.

  • representation for storage or encoding shapes such as file, directory, archive, text, or Zarr store.

  • interface for access contracts such as bytes, text, directory listing, archive members, table, xarray dataset, manifest, or collection.

The kind vocabulary is intentionally open. Plugins may persist their own claim kinds, names, and metadata without changing core schema. The Python API exposes ArtifactClaim plus small convenience constructors: DataTypeClaim, RepresentationClaim/Representation, and InterfaceClaim.

Facets use the same envelope, but they describe structured facts rather than type or interface claims:

{
  "kind": "stat",
  "name": "size",
  "namespace": "ogcat.core",
  "version": "1",
  "evidence": "probed",
  "confidence": "validated",
  "metadata": {"bytes": 12345}
}

The Python API exposes ArtifactFacet and the alias Facet.

Required Fields

Normalized claims and facets always contain:

  • kind: non-empty category string.

  • name: non-empty namespace-local name.

  • namespace: non-empty owner namespace, defaulting to ogcat.core.

  • version: non-empty schema version string, defaulting to 1.

  • evidence: one supported vocabulary term.

  • confidence: one supported vocabulary term, defaulting to evidence.

  • metadata: JSON-compatible dictionary for structured details.

Existing artifact descriptors remain readable. Raw dict claims with missing namespace, version, evidence, confidence, or metadata are normalized with defaults. Raw dict facets may also omit name; in that case name defaults to kind so earlier payloads such as {"kind": "image", "format": "png"} remain readable. Unknown top-level fields are folded into metadata during normalization.

Claims still require both kind and name. Facets still require kind. Required identifier fields must be strings; None, non-string values, and empty strings are rejected. Invalid evidence, confidence, metadata, list, or item shapes raise ValueError or TypeError during ArtifactDescriptor and CatalogRecord construction, including repository load.

Evidence And Confidence

The first vocabulary is:

  • declared: supplied by caller, writer, plugin, or source metadata.

  • inferred: cheap inference such as locator text, suffix, or directory shape.

  • probed: inspected cheaply without proving domain validity.

  • validated: checked by a validator or reader-specific capability.

  • stale: previously true or declared evidence that may no longer describe the artifact.

  • failed: attempted evidence collection or validation failed.

Suffix-only detection should use evidence="inferred" and usually confidence="inferred". Details such as source and suffix list belong in metadata:

{
  "kind": "representation",
  "name": "netcdf",
  "namespace": "ogcat.core",
  "version": "1",
  "evidence": "inferred",
  "confidence": "inferred",
  "metadata": {"source": "suffix", "suffixes": [".nc"]}
}

This makes the difference between “looks like NetCDF” and “validated as NetCDF” explicit.

Namespaces

ogcat.core is reserved for core claim and facet names. Plugin-owned claims and facets should use a stable namespace such as a package name, plugin id, or reverse-DNS string, for example pydata.xarray or org.unidata. The version field is scoped to that namespace and name. Core validates only the envelope and JSON compatibility, not plugin-specific schemas.

Cheap Core Examples

Core can represent common cheap facts without importing optional reader libraries:

  • interfaces: bytes, text, directory-listing, archive-members, manifest.

  • representations: path, file, directory, archive, compressed-file, text, manifest.

  • facets: stat/size, stat/mtime, checksum/sha256, suffix/suffixes, archive/member-summary, manifest/entries.

This slice provides the schema and constructors. It does not automatically generate these claims and facets during ingest, and it does not use them for reader dispatch yet.

Small query helpers in ogcat.artifact_claims support the next registry slice without duplicating normalization logic: iter_claims(), has_claim(), claim_key(), iter_facets(), has_facet(), and facet_key().

Relationship To Capability Selection

The #119 capability registry uses these claims and facets as dispatch facts. It should inspect the ArtifactDescriptor attached to the artifact being read, written, or converted; it should not use CatalogRecord.record_type as an I/O dispatch key.

Selection is explicit. A single artifact may legitimately advertise several interfaces at the same time, for example bytes, text, table, and JSON. A caller that asks only to “read” such an artifact has not supplied enough information. The registry should raise an ambiguity error until the caller requests a specific interface or output claim such as interface=text or interface=table.

Facets make otherwise similar claims precise. Text readers can use encoding facets, delimited-table readers can use dialect facets, archive readers can use member facets, and collection readers can use member-pattern facets. These facts are plugin-readable metadata; they do not require core to import optional scientific libraries.

Capability selection treats claim metadata as descriptive. Claims match by their namespace/kind/name/version envelope. Facets match by the same envelope plus required metadata as a subset, so dispatch-significant details such as encoding values, delimiters, archive members, or local path requirements should be modeled as facets.

Worked Examples

These examples are design targets for writers, inspectors, and future readers. They are not automatic output in this schema slice.

Zarr Directory Store

Issue #121 and PR #122 made add_file() intentionally support managed directory artifacts such as example.zarr. A Zarr store is a directory-shaped artifact, but it is one dataset artifact, not a collection. The useful claims separate storage shape, data type, and optional interfaces:

{
  "claims": [
    {"kind": "representation", "name": "directory", "namespace": "ogcat.core"},
    {"kind": "data_type", "name": "zarr", "namespace": "zarr.dev"},
    {"kind": "interface", "name": "directory-listing", "namespace": "ogcat.core"},
    {"kind": "interface", "name": "zarr-group", "namespace": "zarr.dev"},
    {"kind": "interface", "name": "xarray-dataset", "namespace": "pydata.xarray"}
  ],
  "facets": [
    {"kind": "suffix", "name": "suffixes", "metadata": {"suffixes": [".zarr"]}}
  ]
}

The Zarr and xarray claims do not mean core imports those libraries. They are capability claims that a plugin or optional reader can satisfy later.

NetCDF Member Collection

A directory of NetCDF files opened with a glob is different from a Zarr store. It is a directory representation with explicit collection semantics:

{
  "claims": [
    {"kind": "representation", "name": "directory", "namespace": "ogcat.core"},
    {"kind": "interface", "name": "collection", "namespace": "ogcat.core"},
    {"kind": "interface", "name": "xarray-dataset", "namespace": "pydata.xarray"}
  ],
  "facets": [
    {
      "kind": "collection",
      "name": "members",
      "metadata": {
        "pattern": "*.nc",
        "member_format": "netcdf",
        "member_suffixes": [".nc"],
        "reader_hint": "xarray.open_mfdataset"
      }
    }
  ]
}

This is the claim/facet shape that issue #109 can use for managed collection writes. A reader can dispatch on the collection or xarray-dataset interface and then use the member facet to decide how to open the directory.

CSV-Like Table

CSV-like data can stay dependency-light in core by recording text and table interfaces without importing pandas:

{
  "claims": [
    {"kind": "representation", "name": "text", "namespace": "ogcat.core"},
    {"kind": "data_type", "name": "csv", "namespace": "iana.media-types"},
    {"kind": "interface", "name": "text", "namespace": "ogcat.core"},
    {"kind": "interface", "name": "table", "namespace": "ogcat.core"}
  ]
}

Single NetCDF File

A single NetCDF-like artifact has file representation and can advertise both cheap byte access and optional scientific interfaces:

{
  "claims": [
    {"kind": "representation", "name": "file", "namespace": "ogcat.core"},
    {"kind": "data_type", "name": "netcdf", "namespace": "org.unidata"},
    {"kind": "interface", "name": "bytes", "namespace": "ogcat.core"},
    {"kind": "interface", "name": "xarray-dataset", "namespace": "pydata.xarray"}
  ]
}

Grouped NetCDF/HDF5 File

Groups are structured facts or interfaces, not a reason for a core NetCDF dependency:

{
  "claims": [
    {"kind": "data_type", "name": "netcdf", "namespace": "org.unidata"},
    {"kind": "interface", "name": "netcdf-groups", "namespace": "org.unidata"}
  ],
  "facets": [
    {
      "kind": "netcdf",
      "name": "groups",
      "namespace": "org.unidata",
      "metadata": {"groups": ["/", "/observations", "/metadata"]}
    }
  ]
}

Directory Stores Versus Collections

PR #122 clarified an important ADR distinction. Directory-backed artifacts are not automatically collections. A managed .zarr directory added with add_file() is one dataset artifact whose representation is directory. A managed directory of monthly .nc files is a collection only when a caller, writer, or plugin explicitly adds collection claims/facets such as member pattern, member format, and member suffixes.

This keeps storage shape separate from logical dataset shape:

  • representation=directory says how bytes are stored.

  • data_type=zarr or data_type=netcdf says what external format or data type is claimed.

  • interface=collection says member traversal is part of the artifact contract.

  • collection facets say which members participate and how a reader should combine them.

Writer Result Open Loop

Writers are the natural source of produced artifact claims/facets because they know what they materialized. This branch defines the schema, but it does not define the merge path from writer output into descriptors. Issue #117 should define that path, preserve existing None-returning writers, and decide how writer results interact with OperationContext, rollback, storage plans, audit metadata, and derived classification metadata.

Relationship To Classification Metadata

derived_metadata.classification remains the current home for cheap inferred facts such as artifact_kind, format, archive_format, inner_format, collection_pattern, member_format, member_suffixes, and reader_hint. This slice does not replace that namespace and does not automatically mirror classification into facets.

Future migration can mirror selected classification facts into inferred claims/facets while preserving the existing derived metadata search behavior. For now, the bridge is documentation and compatibility: classification facts remain readable and searchable, and new descriptors may add explicit inferred claims/facets when a writer or plugin has enough context.