---
title: "Docs freshness"
description: "Every page has a review cadence and a reviewer discipline. An expired page is reported by CI, not discovered by a reader."
url: "https://opsinjs.pensievelabs.org/project/docs-freshness"
source: "https://opsinjs.pensievelabs.org/project/docs-freshness.md"
section: "Project"
kind: "project"
reviewed: "2026-09-02"
reviewer: "engineering"
aliases: ["review cadence", "stale docs", "out of date pages"]
---

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

## Why a documentation site needs an expiry date [#why-a-documentation-site-needs-an-expiry-date]

Guidance rots more quietly than code. A component that no longer compiles is
found in minutes; a paragraph that stopped being true two releases ago reads
exactly as authoritative as it did when it was written, and in a health system it
is read by somebody making a decision about how to present a number to a patient.

So every page here carries three fields in its frontmatter, and a script checks
them:

| Field         | What it means                                                                                     |
| ------------- | ------------------------------------------------------------------------------------------------- |
| `reviewed`    | The ISO date somebody last **re-read** the page against the system                                |
| `reviewer`    | Which discipline owes the **next** read-through: `design`, `engineering`, `clinical` or `content` |
| `reviewEvery` | How long the last read-through stands before it expires: `3m`, `6m`, `12m` or `never`             |

`<Reviewed />` prints the date and the cadence at the foot of the page, so a
reader can see the age of what they are reading without opening a file.

**It does not print `reviewer`, and it must not be made to.** That field is an
assignment and not an attestation. Twenty pages name `clinical`, twelve of them
safety patterns including the alert escalation ladder and the result disclosure
rules, and no clinician has read any of them. A footer saying "last reviewed by
clinical" would be the single most dangerous sentence this corpus could print.
When a real sign-off exists it needs a field that records that it happened,
because this one records who is next.

There were two more fields here until recently, `owner` and `a11yDate`. Both
are gone: `owner` asked the same question as `reviewer` without a closed list
of answers and collected six spellings for four disciplines, and `a11yDate`
held the same stamped date on seventy-seven pages, which records a bulk edit
and not a review.

## The cadence, by page kind [#the-cadence-by-page-kind]

`reviewEvery` is optional in frontmatter. When it is absent, the default for the
page's `kind` applies. Those defaults live in one place, `DEFAULT_CADENCE` in
`scripts/check-freshness.mts`, and that is the authority: if the table below ever
disagrees with it, the script is right and this page is the thing to correct.

| Kind               | Default cadence | Usual reviewer | Why                                                                                                                                               |
| ------------------ | --------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `health`           | 6 months        | `clinical`     | Doctrine that shapes what a patient is told                                                                                                       |
| `accessibility`    | 6 months        | `engineering`  | Claims that are tested and can regress                                                                                                            |
| `project`          | 6 months        | `engineering`  | Policy and status pages, which describe a system that is still moving                                                                             |
| `content`          | 12 months       | `content`      | Language guidance changes slowly                                                                                                                  |
| `component`        | 12 months       | `design`       | The implementation moves, but a component page moves with it in the same commit                                                                   |
| `foundation`       | 12 months       | `design`       | Token changes arrive through the generated tables, which the drift gate checks every build                                                        |
| `pattern`          | 12 months       | `design`       | Patterns outlive the components implementing them                                                                                                 |
| `recipe`, `screen` | 12 months       | `design`       | Compose components, so they rot when components change                                                                                            |
| `handbook`         | 12 months       | `engineering`  | Mechanics, tied to releases rather than the calendar                                                                                              |
| `guide`            | 12 months       | `engineering`  | Installation instructions age fastest of all, which is why the ones that matter override this down to 3 months rather than relying on the default |
| `reference`        | never           | `engineering`  | Bodies are generated and are checked by the drift gate on every build, so a calendar review would report an age the gate has already disproved    |

The `reference` exemption is the one that surprises people. It is not a claim
that those pages cannot be wrong; it is that a date is the wrong instrument for
them. `pnpm run check:generated` regenerates every one of them and fails on a
single changed byte, which is a stronger guarantee than a human re-reading a
table of two hundred rows twice a year. The hand-written half above each
generated marker is not covered by that, and a reference page whose prose needs
a cadence sets `reviewEvery` explicitly. Both
[contrast](../reference/generated/contrast.mdx) and
[catalogue](../reference/generated/catalogue.mdx) do this.

**Pages that override the default downwards to 3 months** are the ones where
being wrong is most expensive. They include the emergency and crisis health
pages, the accessibility statement and the two contrast pages, the six `start/`
guides a newcomer follows literally, the measured
[contrast](../reference/generated/contrast.mdx) table, the
[catalogue](../reference/generated/catalogue.mdx), the [roadmap](./roadmap.mdx)
and [state of the system](./state-of-the-system.mdx). The six guides are quick
start, installation, the Next guide, `components.json`, the first component and
troubleshooting.

`reviewEvery: never` exists for pages whose content is fixed by history, such as
a dated changelog entry or a decision record that has been superseded. Use it
rarely; it is the one setting that cannot be reported as stale.

## What the check actually does [#what-the-check-actually-does]

`scripts/check-freshness.mts` reads every page, adds either the page's
`reviewEvery` or its kind's default to its `reviewed` date, and sorts the corpus
into three states. It prints one summary line and then up to three tables,
headed `OVERDUE`, `DUE WITHIN 30 DAYS` and `NO REVIEW DATE`; a table is omitted
when it would be empty. Each row is one page: its slug, its `kind`, its
`reviewer`, its `reviewed` date and its due date, with the overdue rows ordered
by how far past due they are so the worst is at the top. `--json` prints the
same three sets plus the totals, and `--strict` exits non-zero when anything is
overdue.

The tables group by state and by nothing else, so do not expect a per-team
digest from this script. State is what somebody clearing a backlog needs. If you
want the digest, group the `--json` output by `reviewer` with `jq`.

It runs both places, differently. On a pull request it is advisory and
`continue-on-error`, because a page expiring is not a reason to block somebody
else's change. In the nightly job it runs with `--strict` and blocks, which is
the only thing that makes `reviewed:` and `reviewEvery:` mean anything.

A page with `kind: health` and no `reviewed` date is a different matter: that
fails `assert-ia.mts` on every build. Health doctrine without a review date is
not a stale page, it is an unsourced claim.

## What a review is, and is not [#what-a-review-is-and-is-not]

**A review is re-reading the page against the system as it is today** and
changing what has stopped being true. If a page needed no change, the reviewer
still updates `reviewed`. That is a real signal, and it is the whole point of
separating "when it was written" from "when it was last checked".

**Bumping the date without re-reading the page is the one prohibited operation
here.** It converts the only honest freshness signal on the site into decoration.
A page that is out of date and says so is more useful than a page that is out of
date and claims to have been checked last week.

This repository has done it, twice. Of the 343 pages that carry a `reviewed`
date, 255 share one date and another 81 share a second, so two bulk edits
account for 336 of them. The `a11yDate` field deleted alongside `owner` was the
same story on a smaller scale, one stamped date across seventy-seven pages. So
read a `reviewed` date here as the weaker claim it is: somebody passed over the
page on that day. When the next one comes round, re-read the page rather than
repeating the edit that made this paragraph necessary.

## If you are reading a stale page [#if-you-are-reading-a-stale-page]

The review date at the foot of the page is the truth. Cross-check against the
[changelog](./changelog/index.mdx): if a release landed after the review date and
touched what you are reading, believe the changelog. Right now the changelog is
itself behind, because the component layer landed without an entry.
[State of the system](./state-of-the-system.mdx) is therefore the more current of
the two and is the one to check first. Then tell us: report it as a docs issue
naming the page path, and [Community](./community.mdx) says where to send it.
There is no report-a-problem control at the foot of the page; a `<Feedback>`
component is written and nothing renders it, which [Community](./community.mdx)
also records. A stale page is worth reporting anyway, and it is the report this
section exists to ask for.

<LastUpdated />

<Reviewed />
