opsinjs
RoadmapDecisions

ADR 0006. Generated, never authored

Source code, tables and every measured number are produced by scripts, committed as MDX, and checked for drift in CI.

Status

Accepted. 2026-09-02.

Context

Documentation rots unevenly. Prose about intent ages slowly and can be reviewed on a cadence. Facts about a system age at the speed of the system, and the facts that age fastest are the ones that look most authoritative: prop tables, token lists, data attributes, CSS variables, keyboard tables, contrast ratios, bundle sizes.

A stale prop table is not a small problem. It is a table of things that do not exist, indistinguishable from a table of things that do, sitting on a page that a program is about to read and generate code from.

There is a sharper version of the problem in this project. Nothing is built, so every measured number would have to be either omitted or invented. A plausible fake row in a reference table is indistinguishable from a real one, and it poisons exactly the audience the site is designed for.

Amendment, after the component layer shipped. One clause above is stale and is left standing rather than rewritten: "Nothing is built" was true on the day this record was accepted and is not true now. Sixty components are implemented and every catalogue row reads shipped, and the first consequence is that the generators finally have something to read. lib/generated/props.ts carries a prop table extracted from the exported interfaces under registry/bases/ for every component and every exported part, and the pages that show them still carry no prop row of their own, because they render <PropsTable> and nothing else. The argument does not weaken with the fact; it moves. The sharper version of the problem is no longer "every number would have to be invented", it is that an empty table on a system that visibly has components is far more tempting to fill in by hand than an empty table on a system that has none. Two tables are in exactly that position today: Keyboard and Data attributes both still render an honest empty state naming scripts/build-reference.mts. The reason is that the extraction is unwritten, not that the components emit nothing. Rule 3 below is what holds that line, and it is load-bearing now in a way it could not have been while nothing existed.

Decision

Anything that can be derived from the system is generated by a script, committed as MDX, and verified in CI. That covers component source, props tables, token tables, data-attribute tables, CSS-variable tables, keyboard tables, APCA and WCAG measurements, bundle sizes, catalogue rows, the glossary and eval scores.

Three rules make it work:

  1. Generated artefacts are committed, as real MDX under reference/generated. They are therefore indexed by the site's search exactly like written pages, which is the only change needed to make search cover generated content.
  2. CI regenerates and diffs. pnpm run check:generated runs the generators and fails on any change, so a committed table cannot drift from its source.
  3. An empty source renders an honest empty state, naming the script that will fill it. Never a sample row, never an ellipsis, never a plausible example.

Consequences

  • You cannot fix a wrong number by editing the page. The next generate run erases it and CI fails the pull request that contained it. You fix the source. This is the intended friction.
  • Every generated page needs a committed placeholder so that a clean clone typechecks and builds before anything has been generated. Whoever owns a directory writes that placeholder.
  • A hand-written header survives on every generated page. Above the marker comment sits a short block naming the source and the script. It is there because "generated from tokens/color.json by build-tokens.mts" is itself a fact a reader needs, and no generator can be trusted to explain its own provenance well.
  • Several tables are empty today, and say so. That is the decision working correctly, not a gap in the documentation.
  • Generated directories are excluded from the formatter, so that a formatting pass cannot create a diff that the drift check then reports as a source change.
  • Numbers become checkable claims. The contrast figures on this site were computed from shipped token values on a dated run, and a regression fails the build rather than being noticed by a reader.

Alternatives considered

Generate at runtime, from an API route. Rejected: the search index would not cover it, the .md twins would not contain it, and the offline bundle would be missing exactly the machine-readable facts it exists to carry.

Hand-written tables with a review cadence. Rejected. This is the industry default and it is the failure mode being avoided; a cadence catches a stale table months after somebody has already generated code from it.

Generate but do not commit. Rejected: a clean clone would not typecheck, CI could not diff for drift, and there would be no way to review a change to a generated artefact in a pull request. That review is often exactly where a token mistake becomes visible.

Revisiting this

Revisit only if committed generated output becomes unmanageable in review. That is a plausible future once there are hundreds of components. The mitigation would be narrower generated files, not hand-authored tables.

Edit this page

Last read through against the system on 2026-09-20. Due for review every 12 months; expiry is reported by pnpm run check:freshness.

On this page