---
title: "Machine-readable schemas"
description: "The catalogue, the token set, the status vocabulary and the glossary as JSON at stable paths, so a tool can resolve a fact instead of parsing prose."
url: "https://opsinjs.pensievelabs.org/agents/machine-readable-schemas"
source: "https://opsinjs.pensievelabs.org/agents/machine-readable-schemas.md"
section: "Agents & automation"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["json api", "catalogue json", "schemas", "structured data"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="guide" />

## Overview [#overview]

Prose is for people. A build script, a linter, a validator or an agent with a
narrow question should not have to read a paragraph to learn that `urgent` is a
valid status and `critical` is not.

Four things are published as JSON at stable paths, and each of them is the same
source the site itself renders from. None of them is an export, none is a copy,
and none is a periodically refreshed mirror. If the site and the JSON ever
disagree, the JSON is not the one that is wrong.

## The four surfaces [#the-four-surfaces]

| Path               | Contains                                                                                                            | Generated from          |
| ------------------ | ------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| `/r/registry.json` | The shadcn-spec catalog: every item, its type, title, description and dependencies                                  | `registry/catalogue.ts` |
| `/r/index.json`    | Name, type, status, category, links and aliases per item, which make up the opsinjs view rather than the shadcn one | `registry/catalogue.ts` |
| `/r/<name>.json`   | One registry item, resolved at the default base and style                                                           | `registry/catalogue.ts` |
| `/r/docs.json`     | The documentation corpus, versioned and size-capped. See [the offline bundle](./offline-docs-bundle.mdx)            | the page tree           |

Two more are files in the repository rather than routes, because their consumers
are build tools rather than clients:

| File                   | Contains                                                                |
| ---------------------- | ----------------------------------------------------------------------- |
| `tokens/*.json`        | The authored token source: colour, material, motion, type, space, shape |
| `tokens/glossary.json` | Every clinical term with its plain-English replacement                  |

## The status vocabulary [#the-status-vocabulary]

Three values, closed, and used identically by a component page's frontmatter,
the catalogue and the registry:

```ts
type Status =
  | "planned"     // specified, not implemented. No component sits here today
  | "shipped"     // installable source. The API may change in any release
  | "deprecated"  // installable source, on its way out, with a named replacement
```

`shipped` covers every component id today. It means installable source and a
changing API, and it makes no claim about clinical safety, because no opsinjs
component has had a clinical review. Every shipped component has been audited
against WCAG 2.2 AA by its own authors, in a static source pass and a rendered
pass, with findings fixed in the same change; that is not an independent
accessibility review, so `status` still makes no claim of accessibility
conformance. See [the audit is author-run](../project/decisions/0025-the-audit-is-author-run.mdx).
`planned` is a specification you may read and must not generate against. Handle all three
anyway: the vocabulary is closed, not fixed, and a client that only knows the
values it saw once will misread the day one of them moves.

`status` appears on a `kind: component` page and on a catalogue row, and nowhere
else. A documentation page carries no status at all, so a tool that reads one
from a page about colour or consent is reading a field that does not exist.

The clinical status levels are a **different, unrelated** four-value vocabulary,
and the four are `steady`, `watch`, `attention` and `urgent`. Confusing the two
is a real failure mode worth guarding against in any tool you write. One
describes whether a component's code exists; the other describes a person's health
data. The clinical vocabulary also has a fifth token stem, `unknown`, which is
the absence of an assertion rather than a fifth level; a schema that ranks it
alongside the four has already got it wrong.

## The catalogue [#the-catalogue]

`registry/catalogue.ts` is the single source of truth for component identity:
id, title, plain-English description, category, status, `since`, aliases and the
reviewing discipline. It has no field for an independent accessibility review
date or a clinical review date, because neither has happened; the WCAG 2.2 AA
audit that has happened was run by the authors and is recorded in
[the audit is author-run](../project/decisions/0025-the-audit-is-author-run.mdx),
not in this file. Every id in it resolves somewhere. That
rule is enforced by `scripts/assert-ia.mts` rather than by convention.

It is also the sole owner of the **alias namespace**. Aliases are search
synonyms, and `lab result` finds `result-card`. Every alias is required to be
globally unique across the corpus, which is only achievable if one file declares
them all. A documentation page references an alias; it never invents one.

## The glossary [#the-glossary]

`tokens/glossary.json` pairs a clinical term with a plain-English replacement
and a short definition written for a reader with no medical training. It drives
`<Term>`, `<PlainLanguage>` and the filterable A to Z on
[Plain-English A to Z](../content/plain-english-a-z.mdx).

<Callout>
  Every definition in it is original prose. Public health bodies publish excellent
  plain-language glossaries and most of them are under a licence that does not
  permit reuse. The NHS A to Z is Crown copyright. opsinjs cites such sources and
  never copies them, and any contribution that pastes one will be rejected. See
  [Licence and attribution](../project/licence-and-attribution.mdx).
</Callout>

## Using them well [#using-them-well]

<Steps>
  ### Resolve, do not guess [#resolve-do-not-guess]

  If your tool needs to know whether `symptom-picker` exists, fetch
  `/r/index.json` and look. Do not infer it from a URL pattern and do not ask a
  model.

  ### Check `status` and `implemented` on every read [#check-status-and-implemented-on-every-read]

  Every catalogue row is implemented and installable, and every one has been
  audited against WCAG 2.2 AA by its own authors and none has had an independent
  accessibility review or a clinical review. `status` tells you whether code
  exists, not whether it is safe; `implemented`
  tells you whether there is source to install, which is the question
  most tools are actually asking. A tool that ignores either will behave, next
  year, as though a specification were a component, or it will refuse to install
  a component that ships.

  ### Treat aliases as input, not as output [#treat-aliases-as-input-not-as-output]

  Match a user's phrasing against aliases. Never emit an alias as an identifier.
  `lab result` is not importable.

  ### Cache with the version [#cache-with-the-version]

  `/r/docs.json` carries `docsVersion` and `generatedAt` in its body. A markdown
  twin carries the same version on its `x-opsinjs-docs-version` response header
  and not in its frontmatter, so a twin saved to a file loses it. Record it as
  you fetch. Cache the version alongside the data either way, so a stale answer
  can be identified as stale.
</Steps>

## Verify it worked [#verify-it-worked]

```bash
curl -s https://opsinjs.pensievelabs.org/r/index.json | head -30
```

You should get JSON with one entry per catalogue row, each carrying a `status`
and an `implemented` flag. Every entry reports `implemented: true` today. Confirm
you read the flag rather than inferring it from the URL: the first `planned` row
after this sentence will report `implemented: false`, and that is the field a
tool must check.

## Troubleshooting [#troubleshooting]

**`/r/registry.json` is missing items I can see in the sidebar.** The registry
lists distributable items. Documentation pages that describe doctrine rather
than code have no registry entry, by design.

**Aliases in the JSON differ from the ones on a page.** They cannot, unless the
build is stale. Both come from the catalogue. Rebuild, and report it if it
persists.

**The token JSON does not match the CSS.** `pnpm run generate` has not run. The
CSS is generated from the JSON, and `--opsin-tokens-generated` will say
`placeholder` when it has not.

**I want a schema for the frontmatter.** It is
`content/_templates/frontmatter.schema.json`, and `source.config.ts` is the copy
the build actually enforces.

## Next [#next]

* [Offline docs bundle](./offline-docs-bundle.mdx) covers everything above in
  one request.
* [Registry overview](../registry/index.mdx) says what the `/r/` surface is and
  why it follows someone else's spec.
* [Writing docs for agents](./writing-docs-for-agents.mdx) shows how a page
  becomes well-formed structured data.
