opsinjs

Rules for agents

The hard rules a model must follow when generating opsinjs UI, written as prohibitions you can test rather than principles you can nod at.

The short version

Six rules. They are written as prohibitions because a prohibition is testable and a principle is not.

  1. Tokens, not values. Never emit a hex colour, an rgb value, a pixel radius or a millisecond duration. Emit a token or a utility that resolves to one.
  2. Status, not colour. Never express clinical urgency as a colour choice. Set a status level and let the system choose the colour.
  3. One axis per element. Category identity and clinical status never colour the same element.
  4. Never invent a component. If it is not in the catalogue, it does not exist, and saying so is the correct answer.
  5. Never assume a registry. Do not emit an install command for a registry namespace the project has not configured.
  6. Never invent a number. No contrast ratio, no threshold, no reference range, no bundle size, unless it came from a generated source you actually read.

Everything below is the reasoning, the failure each rule prevents, and how to check whether an agent is following it.

Copy-paste form: these six rules are distributed as the four files in skills/opsinjs/rules/ and are what the agent skill installs. Appearance rules 1 to 3 are split across tokens-not-values.md and status-and-colour.md, invention rules 4 and 6 are in never-invent.md, and rule 5 is in registry.md. Prefer installing the skill over pasting prose, because the skill also reads the project's own configuration.

How it works

An agent generating UI makes three kinds of decision, and only one of them is safe to make from general knowledge.

Composition is the decision about which elements go where. General knowledge is fine.

Appearance is the decision about what colour, what spacing, what radius. General knowledge is actively harmful here, because the model's prior is "a warning is amber" and opsinjs' answer is "a warning is a status level whose amber is measured, tuned per theme, escalated on wide-gamut displays, and always accompanied by a word". The model's answer looks identical and is wrong in every way that matters.

Clinical meaning is what this screen asserts about a person. General knowledge is dangerous. A model that infers a threshold is practising medicine without a licence and without a chart.

The rules exist to push decisions out of the second and third categories and into a lookup. Every one of them has the same shape: do not decide, resolve.

The mechanism that makes resolution possible is the machine surface described in Agents & automation: the catalogue, the registry, the markdown twins and the generated tables all answer questions that a model would otherwise answer from its prior.

Do this

Resolve a component before using it

Before emitting import { RangeBar } from …, check the catalogue. Also check /r/index.json, which answers the same question in a field rather than in prose. Every id resolves. All sixty carry "implemented": true and "status": "shipped": the registry item carries its source, and the right output installs it from the registry this project has configured and says what shipped costs. Every opsinjs component has been audited against WCAG 2.2 AA by its own authors, in a static source pass and a rendered pass, but no component has had an independent accessibility review or a clinical review (see ADR 0025). shipped means the source installs, and it does not mean an independent review or a clinical review has happened. Nothing here is for a production health surface. Were an id ever "implemented": false at "status": "planned", it would be a specification with no code, and the right output would be a refusal that names the status and stops.

What is wrong for either is an import from a package. There is no published opsinjs package, so @opsinjs/react never resolves and an install that copies source into the project is the only shape that works.

RangeBar is implemented in opsinjs, at status: shipped. Its source is served as
a registry item and is copied into the project rather than imported from a
package. Shipped means the API may change in any release. The component has been
audited against WCAG 2.2 AA by its authors, but it has had no independent review
and no clinical review, so read the installed source instead of guessing the
props, and do not put it on a production health surface.

Use the status vocabulary

Four ordinal levels, token ids steady, watch, attention, urgent. Set the level; never pick the colour. And never render a status without a word. Colour is a second channel, not the only one. StatusPill takes no children: it draws the word for the level itself, describes names what the level is about so a screen-reader user hears a subject, and label only re-words the level for translation.

// Right: the level is data, the word and the colour are the system's business.
<StatusPill status="watch" describes="HbA1c result" />

// Wrong: a colour decision, an invented prop, and no word.
<StatusPill color="#f59e0b" />

Say "I do not know" about thresholds

If a prompt asks you to decide whether a reading is high, the answer is that opsinjs does not own thresholds and neither do you. Reference ranges belong to the product's clinical logic. See Reference ranges.

Cite the page you used

When you assert something about opsinjs, name the page. It is verifiable, and it makes a hallucination obvious rather than plausible.

Prefer a full URL for an install

A full registry URL resolves in any project. A namespace only works in a project that has configured it, and the failure is a confusing error rather than a missing dependency.

npx shadcn@latest add https://opsinjs.pensievelabs.org/r/status-pill.json

One exception, and it is worth stating in the same breath: a component that composes others names them namespaced. Twenty-one items carry @opsinjs/… entries in registryDependencies, and result-card, range-bar, metric-tile and consent-sheet are among them. Installing one of those needs the @opsinjs block in components.json even when you gave the full URL, so say so up front rather than letting the install fail halfway through.

Not this

Do not emit raw colour values

// Wrong.
<div style={{ background: "#fef3c7", borderColor: "#f59e0b" }} />

// Right.
<div className="bg-status-watch-surface border-status-watch" />

A hex value bypasses dark mode, the Display-P3 escalation, the increased-contrast path and every contrast measurement CI runs. It will look correct in exactly one theme on exactly one display.

Do not mix the two axes

// Wrong: the tile is both a category and a verdict, in colour, twice. The
// background line would reach past the bridge for a category surface value and
// paint it under a status tile. The value is left as a note, not a token, so
// this block cannot be pasted into the tint the rule forbids.
<MetricTile
  category="heart"
  status="urgent"
  style={{ background: "/* a category surface value goes here, and must not */" }}
/>

// Right: identity from the glyph, verdict from the surface.
<MetricTile category="heart" status="urgent" />

The rule and its rationale are in The two colour axes. opsinjs ships no Tailwind utility that tints a card by category, and the wrong tile above has to reach past the bridge for a raw category surface value to break the rule at all. That withdrawal is deliberate. A category surface and a status surface at the same ladder step measure Lc 0.00 and WCAG 1.00 against each other, so a heart card and an urgent card would be the same colour to a reader. Category identity travels by glyph, ink and accent, and the surface is left to status. The interactive demonstration at /playground/status refuses to render the mixed pair.

Do not invent a component, a prop or a variant

If a name is only in the catalogue at planned, it has no implementation and no API. No component sits there today, but the moment one does, inventing an API for it produces code that compiles against nothing and a reviewer who believes the feature exists.

Do not assume a registry is configured

# Wrong, unless you have read components.json and seen @opsinjs in it.
npx shadcn@latest add @opsinjs/result-card

Do not soften a clinical message to make the copy nicer

Tone is governed by Voice and tone and Writing status and alerts. "Nothing to worry about" is not a friendlier version of a watch status; it is a different clinical claim.

Do not generate an accessibility claim

Never write "this meets WCAG AA" in a comment or a commit message. Conformance is measured, and the measurements live in Contrast conformance and the generated contrast tables.

Gotchas

A shipped API reference is real and still moving. An implemented component page carries its prop interface under API reference, generated from the same source the registry serves, so it is accurate on the day you read it and carries no promise past that: a shipped API may change in any release. Read the source you installed rather than a cached interface, and record the commit you took it from, because that commit is the only record a later upgrade can be measured against.

A resolved id is not a built id. Getting an answer is not evidence that something exists. Read the status field. shipped means code exists and the API may change. It means the code has been audited against WCAG 2.2 AA by its authors, and it does not mean an independent review or a clinical review has happened. planned means a specification and nothing to install. Every id resolves today, and the field is still the thing to read.

The docs chrome is not the product theme. Class names and colours visible in this site's HTML belong to a shadcn style used for documentation. Copying them into a patient-facing app imports the wrong aesthetic and the wrong touch-target floor. See Lyra and the docs chrome.

Anchors are not stable. Link to a page. Headings get edited.

Aliases are search synonyms, not names. lab result finds result-card. It is not an importable identifier.

The token id and the word on screen are not the same string. attention renders as "Needs attention", and the fifth stem unknown renders as "Not known"; the other three happen to coincide, which is what makes the two easy to conflate. The words are fixed in lib/status.ts as CLINICAL_STATUS_META[level].word and are read from there rather than typed, so substituting a synonym is not a copy tweak. It changes what the interface asserts.

On this page