Skip to main content

Registry Contract: Resolve, Card, Values, History

The Field Registry as a source of truth for agents and apps: stable ids under curation, name resolution, named maturity levels, the concept card, cross-document values with provenance, curation history and field.* events.

These reads make the Field Registry safe to build on: an id you stored keeps working after a merge, a name resolves to the live concept, every read names its maturity level, and curation is announced on the event feed so a cached vocabulary can invalidate instead of polling.

Maturity levels

Every /v1/fields* response carries maturity beside the frozen numeric tier. Treat it as the contract for what you may rely on.

Levels

coretier 1Universal, fully trusted. Safe to bind schemas, dashboards and downstream models to. Demotion is hysteresis-guarded and always emits `field.demoted`.
proventier 2Recurring and useful. Stable id; the name may still be groomed, announced by `field.renamed` / `field.merged`.
candidatetier 3Newly discovered. May be merged, renamed or expire with no notice beyond the event feed. Do not hard-code.

GET /v1/fields?maturity=core|proven|candidate filters by level. The list hides rows merged into another concept unless include_superseded=true, so a cached vocabulary never carries two ids for one concept.

Stable identity under curation

When curation merges a concept into a survivor, the absorbed row stays readable with superseded_by: { id, links.self }GET /v1/fields/{id} never 404s a stored id. Pass follow_redirects=true to receive the survivor instead; the hops are listed in redirected_from so you can rewrite your keys.

GET/v1/fields/resolve

Query parameters

name*stringThe field name, in any spelling you hold: `Invoice No`, `invoice_number`, `Rechnungsnummer`.
follow_redirectsbooleanWalk `superseded_by` to the survivor. Default: true

Lookup order, first hit wins: exact canonical_name → the registry match_key fold (separator, case, accent and plural insensitive — the same fold the resolver binds with) → a merge alias → exact synonyms[] containment (names folded in by merges and cleanup) → case-insensitive canonical name → case-insensitive synonym. The first four arms are index lookups; the last two scan the tenant registry and run only when everything indexed missed. The response says which arm matched; no match is 404 unknown_field.

Response

{
  "name": "Invoice No",
  "matched_by": "synonym",
  "redirected_from": ["b7c1…"],
  "field": { "id": "a1b2…", "canonical_name": "invoice_number", "maturity": "proven", "tier": 2, "synonyms": ["inv_no", "invoice no"], "superseded_by": null, "links": { "card": "/v1/fields/a1b2…/card" } }
}

The concept card

GET/v1/fields/{id}/card

Response sections

definitionobject`description`, `instruction` (the master extraction instruction), `variance_notes`, `synonyms[]`, `aliases[]`.
identityobject`pinned`, `source`, `link_key`, `superseded_by`, `absorbed[]` (concepts merged INTO this one that still exist as redirect rows).
occurrenceobject`occurrence_count`, `document_count`, `occurrence_rate`, `first_seen_at`, `last_seen_at`, `promoted_at`, `document_type_distribution`.
valuesobjectSources-IAM filtered distribution over the concept and its absorbed redirects: `total`, `distinct_count`, `top[]` (value, count, share), `examples[]`.
usageobject`schema_count`, `schema_field_count` — how many Specs bind the concept.

Values across documents

GET/v1/fields/{id}/values

Query parameters

document_idstringOnly occurrences on this document.
valuestringCase-insensitive contains filter on the value text.
include_redirectedbooleanInclude occurrences still bound to concepts merged into this one (`provenance.via_redirect: true`). Default: true
limit / cursor / orderstringCursor pagination, newest first by default (max 100 per page).

One row per bound occurrence — a per-cell table column is several rows sharing document_id. Each row carries value, confidence, and provenance (raw_field_name, verbatim source_text, the resolved_by band, needs_confirmation). Rows and pagination.total are Sources-IAM filtered as the key's minting user.

Curation history and events

GET/v1/fields/{id}/history

Event kinds: tier_changed (with from_maturity / to_maturity and direction), merge_log_absorbed / merge_log_merged_into, absorbed, superseded, renamed, rewritten, split, deleted, alias_added. Never follows redirects — the trail belongs to the id asked for.

The same changes arrive live on GET /v1/events (and to webhook / delivery bindings) as field.graduated, field.promoted, field.demoted, field.merged and field.renamed. Every payload names the concept by stable field_id; field.merged carries merged_into_id and mode (redirect keeps the absorbed row, deleted removes it). Events are published only after the curation write has committed.

Caching the vocabulary? Store id, read maturity, subscribe to field.*, and re-resolve through GET /v1/fields/resolve when a name stops matching. Never key on canonical_name alone.

Frequently asked questions

A field id I stored now returns superseded_by. Did I lose data?+
No. The absorbed row stays readable and its occurrences move to the survivor. Read `superseded_by.id` (or call with `follow_redirects=true`) and rewrite your key; `GET /v1/fields/{id}/values` on the survivor includes the not-yet-repointed occurrences with `provenance.via_redirect: true`.
Which maturity level is safe to hard-code?+
`core`. A `proven` concept keeps its id but its name may still be groomed; a `candidate` may be merged or expire. Subscribe to the `field.*` events either way so a cache never goes stale silently.
How do I go from a name in a user prompt to the right concept?+
Call `GET /v1/fields/resolve?name=` with the wording you have. It tries the exact canonical name, the registry spelling fold, merge aliases and synonyms in that order (then case-insensitive fallbacks), follows redirects, and tells you which arm matched — then read `/card` to confirm the definition and example values before querying.