---
title: "Contributing"
description: "How a change gets into opsinjs. The four routes in, what each one has to clear, and the one thing that will get a component proposal rejected fastest."
url: "https://opsinjs.pensievelabs.org/handbook/contributing"
source: "https://opsinjs.pensievelabs.org/handbook/contributing.md"
section: "Handbook"
kind: "handbook"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["how to contribute", "contribution guide", "submitting a change", "proposing something"]
---

> 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="handbook" />

## The short version [#the-short-version]

Four kinds of change, four different bars:

| Change               | Bar                                                | Where                                                |
| -------------------- | -------------------------------------------------- | ---------------------------------------------------- |
| A documentation fix  | Correctness. Open a pull request.                  | Any page's "Edit this page"                          |
| A token change       | A migration story, because a token is a public API | [Contributing tokens](./contributing-tokens.mdx)     |
| A new component      | A proposal answering the intake questions first    | [Proposing a component](./proposing-a-component.mdx) |
| A change to doctrine | A dated rationale and a named reviewer             | [Health](../../health/index.mdx)                     |

The fastest way to have a component proposal rejected is to open it with an API.
The intake questions are about what the component *asserts about somebody's
health* and what happens when it is wrong; a prop table does not answer either.

## How it works [#how-it-works]

### The three release phases [#the-three-release-phases]

Every component arrives the same way:

`planned` → `shipped` → `deprecated`

* **`planned`** is a full specification page: intent, when not to use it naming
  the alternative, the clinical contract, the proposed anatomy, the proposed API
  and the accessibility bar. Still no code. No id sits at `planned` today,
  because every catalogue row is implemented and installable.
* **`shipped`** means code exists, it installs, and the API may change in any
  release. Every component has been audited against WCAG 2.2 AA by its own
  authors, in a static source pass and a rendered pass, so it is audited but
  not independently reviewed, and no component has had a clinical review.
  `shipped` means the source installs and the authors audited it, and it does
  not mean an independent reviewer or a clinician signed it. Nothing here is
  for a production health surface until a clinician signs it. See [ADR
  0025](../../project/decisions/0025-the-audit-is-author-run.mdx).
* **`deprecated`** means the code still installs, a replacement is named, and a
  removal version is named.

What each phase promises is set out in [Release
phases](../../project/release-phases.mdx). The important consequence for a
contributor is that &#x2A;*the specification is the deliverable at `planned`**, and it
is reviewed as seriously as code.

### Review [#review]

Every change needs one review. Some need more:

* **Anything under `health/`** needs a clinical reviewer, and a `reviewed` date.
* **Anything that changes a colour token or a contrast pair** means the
  generated contrast report must be regenerated and committed.
* **Anything user-visible in a component** needs an accessibility review,
  recorded by setting `reviewed` and `reviewer: design` on the day it happened.
* **Any wording that appears on screen** needs a content review.

One pair of frontmatter fields carries all four, `reviewed` and `reviewer`, which
is what makes [Docs freshness](../../project/docs-freshness.mdx) able to report
expiry rather than guess at it. There is no second date field: a review is a
review whoever signed it.

### Generated files [#generated-files]

A large share of this site is generated. If your change touches a token, a type,
a keyboard table, a data attribute or the catalogue, the generated output must be
regenerated and committed in the same change. CI runs the generators and fails
on any diff. See [ADR
0006](../../project/decisions/0006-generated-not-authored.mdx).

## Do this [#do-this]

* **Open an issue before a large change.** A rejected component proposal after
  two weeks of work is a bad outcome for everybody, and the intake questions can
  be answered in an hour.
* **Read the page kind's template first.** The outline is a contract that
  `assert-ia` enforces, and writing to the template is faster than writing prose
  and re-cutting it. See [Documentation
  templates](./documentation-templates.mdx).
* **Say when something is your opinion.** `evidence: opinion` is always
  preferred to a citation that does not quite say what you needed it to.
* **Run `pnpm run check` before opening the pull request.** Four checks, in
  order: `check:generated` regenerates and fails on a diff, `check:ia` asserts
  the frontmatter, the section outlines and both link directions, `check:a11y`
  reads the component sources for a status carried by colour alone and the rest
  of the static accessibility rig, and `check:llms` proves the machine-readable
  surface. Freshness is not in it. `pnpm run check:freshness` is separate, and
  it reports rather than fails.
* **Keep the change one thing.** A token rename and a new component in one pull
  request cannot be reviewed by the people who need to review each.

## Not this [#not-this]

* **Never invent a citation, a statistic, a DOI or a date.** This is the one
  rule with no exception. A fabricated reference in a health document is the
  worst thing this repository could contain, and it is worse than an honest
  absence.
* **Do not copy NHS or other Crown-copyright text.** Cite it; write your own.
  See [Licence and attribution](../../project/licence-and-attribution.mdx).
* **Do not add a component because a competitor has one.** A recorded decision
  not to build something is a real outcome, and saying so in a proposal costs
  nothing.
* **Do not hand-edit a generated file.** It will be overwritten and the diff
  will fail CI.
* **Do not add a page without a `kind`**, or a component page without a
  `status`. The build will reject either, and the rejection is deliberate.

## Gotchas [#gotchas]

* **`assert-ia` checks headings against the page kind.** An extra H2 fails the
  build even if it is a good heading, because the outline is what keeps
  hundreds of pages navigable.
* **Aliases are globally unique.** They are declared in
  `registry/catalogue.ts`; a page that invents one will collide eventually.
* **Links in MDX are relative and resolved by `createRelativeLink`.** An absolute
  site link fails the IA check, and there is no longer an exception.
* **British spelling in prose, American in code.** A page named
  `colour-independence.mdx` documenting a `ColorScale` is correct. See
  [Naming conventions](../naming-conventions.mdx).
* **A clean clone needs `pnpm install` before `typecheck`**, because `.source/`
  is generated by `postinstall` and gitignored.

## Related [#related]

* [Proposing a component](./proposing-a-component.mdx) has the intake questions.
* [Component checklist](./component-checklist.mdx) is the definition of done.
* [Documentation templates](./documentation-templates.mdx) has the page outlines,
  published verbatim.
* [Community](../../project/community.mdx) is where to ask before you build.
