---
title: "ADR 0010. Shared code ships inside every registry item"
description: "One substrate module, appended to every component's files list with its real content, so the same import path resolves in this repository and in a consumer's."
url: "https://opsinjs.pensievelabs.org/project/decisions/0010-shared-code-ships-with-every-item"
source: "https://opsinjs.pensievelabs.org/project/decisions/0010-shared-code-ships-with-every-item.md"
section: "Project"
kind: "project"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["shared code", "substrate module", "lib opsinjs", "shared module"]
---

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

## Status [#status]

**Accepted.** 2026-09-03. Verified end to end against shadcn 4.20.0 rather than
reasoned about: a registry item served from this repository installed into a
throwaway app outside it, the substrate landed at `lib/opsinjs.ts` and
`lib/status.ts`, a `registryDependencies` entry pulled its component in
alongside, and `tsc --noEmit` on the result exited 0.

## Context [#context]

[ADR 0002](./0002-shadcn-registry-distribution.mdx) makes the unit of distribution
a file of source text rather than a package. That works cleanly for a component
that imports nothing but React. It does not work at all for sixty components that
have to agree with each other about what `watch` means.

What they have to share is small, and none of it is decoration: the four clinical
levels and the words for them, the six health categories, the icon name per
level, the guard functions, the axis-conflict check, and the shapes that appear
in more than one interface, which are a reference range, a trend point and a
material rung. It already exists, in `lib/status.ts`, and it is already the thing
the documentation site, the machine routes and the token pipeline all read.

The failure mode of getting this wrong is unusually quiet. A component that
imports `@/lib/opsinjs` compiles here, because `apps/www/tsconfig.json` maps
`@/*` onto `./*`. In a consumer's repository the same import resolves only if a
file exists at their `@/lib/opsinjs`. And shadcn's installer skips, without a
message, any entry in an item's `files` list that has no `content`. A registry
item that names a file it does not carry therefore installs cleanly, reports
success, and leaves the consumer with a component that cannot compile and no
error text to search for.

The alternative to sharing is duplication, and duplication has a specific cost
here rather than a general one. Sixty private copies of the status vocabulary are
sixty places somebody can add a fifth level, rename `attention`, or quietly
reintroduce a word from the banned list. Every one of those copies would
typecheck.

## Decision [#decision]

**One substrate module at `apps/www/lib/opsinjs.ts`, and it ships inside every
component's registry item.**

* The module **re-exports `lib/status.ts` and never re-declares it**. The
  vocabulary is declared once, in the file the scripts and the site already read.
  A reference range, a trend point, a material rung, the warning channel and the
  example-data literal of [ADR 0012](./0012-synthetic-example-data.mdx) are needed
  only by components, and the substrate module declares them itself, once.
* **A component imports from `@/lib/opsinjs`, from `@/lib/utils`, from
  `@/registry/base-lyra/ui/<id>` when it composes another opsinjs component, and
  from nothing else in this repository.** `@/lib/utils` is safe because
  `shadcn init` writes `lib/utils.ts` into every consumer before anything is
  installed. The third one needs its own paragraph, below, because it is the
  specifier a reasonable person would not guess.
* **`scripts/build-registry.mts` appends the substrate files to every component
  item's `files` list**, each with `type: "registry:lib"` and real `content`.
  shadcn resolves `registry:lib` against the `aliases.lib` entry in
  `components.json`, which is `@/lib` here and `@/lib` in a default consumer, so
  `@/lib/opsinjs` resolves identically in both trees.
* **Nothing is rewritten on the way out.** The text a consumer receives is
  byte-for-byte the text `<ComponentSource>` displays and the text this
  repository reviews.

The substrate is library code. Component-to-component composition is a different
mechanism with two halves, and both are required. `registryDependencies` on the
catalogue row is how `result-card` names `status-pill` and `value`, which is what
makes `shadcn add` fetch and write them; it never inlines a copy of either. But a
catalogue row does not compile anything. The importing FILE still has to name its
dependency, and there is exactly one specifier that works in both trees:

```tsx
import { StatusPill } from "@/registry/base-lyra/ui/status-pill"
```

Neither obvious alternative survives. `@/components/ui/status-pill` resolves here
to `apps/www/components/ui/`, which is the lyra documentation chrome.
`components/ui/button.tsx` already lives there and is emphatically not the
opsinjs Button, so the collision is not hypothetical.
`@/registry/bases/base/status-pill` resolves here but shadcn rewrites it to
`@/components/base/status-pill`, a path nothing writes to, so the install reports
success and the consumer's build fails afterwards. That is the worst of the
available failures.

The form above resolves here because `apps/www/tsconfig.json` maps
`@/registry/base-lyra/ui/*` to `./registry/bases/base/*`, and it resolves there
because shadcn's import transform matches `/^@\/registry\/(.+)\/ui/` and
rewrites it to the consumer's `aliases.ui`. `base-lyra` is the style segment
shadcn's own convention expects in that position; it is not a directory on disk
and does not need to be.

## Consequences [#consequences]

* **Every item is self-sufficient**, so installation order does not matter and
  there is no item a consumer has to know to install first. `shadcn add
  @opsinjs/status-pill` into an empty project produces something that compiles.
* **A filename collision is possible, and a prompt is the only thing standing in
  the way.** `lib/status.ts` is a plausible name in somebody's repository. The
  `target` is set explicitly so shadcn resolves a real path and prompts before
  overwriting, but a consumer who accepts the prompt without reading it loses
  their file. That is a permanent cost of copy-in distribution, it is the same
  cost [ADR 0002](./0002-shadcn-registry-distribution.mdx) accepted for
  components, and the mitigation is documentation rather than machinery. See
  [Upgrades and diffs](../../registry/upgrades-and-diffs.mdx).
* **The substrate must never re-declare what it re-exports.** Two live
  declarations of `ClinicalStatus` is exactly the drift this decision exists to
  prevent, and it would typecheck in both trees while meaning two different
  things in each.
* **The substrate inherits the erasable-syntax constraint.** `scripts/*.mts` load
  it under plain Node 24 type stripping, so no `enum`, no parameter properties,
  no `namespace`, no JSX, and every relative import needs an explicit `.ts`
  extension. A component file has no such constraint; the substrate does.
* **Every item's payload grows, and a substrate change is a sixty-item diff.**
  `/r/registry.json` and each `/r/<name>.json` now carry the shared text,
  and editing one line of the substrate changes every published item. That is
  visible and reviewable, which is the intended trade against invisible drift.
* **One install writes the substrate once, however many components it pulls in.**
  The installer merges the whole resolved tree and dedupes by resolved target
  path before writing, so a component that names two others through
  `registryDependencies` does not produce three copies of the same file. Across
  separate installs the file is simply written again over an identical one.

## Alternatives considered [#alternatives-considered]

**A workspace npm package, `@opsinjs/core`.** `pnpm-workspace.yaml` lists
`packages/*`, and that directory exists on disk as a reserved and empty
placeholder, so the workspace glob is valid and no package ships from it yet.
Rejected on the deeper ground rather than on the state of that directory: a
package cannot travel through `/r/*.json`, because a registry item carries file
contents
and not a dependency resolver. Adopting one would mean a consumer runs an install
command *and* adds an npm dependency, and the npm half reintroduces exactly the
version coupling that [ADR 0002](./0002-shadcn-registry-distribution.mdx) exists
to avoid. That record draws a split where the registry is for components and the
packages are for functions, and it puts a vocabulary of unions and words firmly
on the registry side.

**The substrate as its own registry item, named in every `registryDependencies`.**
The mechanism is real: shadcn walks `registryDependencies` and merges the
resulting tree. Rejected because the item route is catalogue-gated. An id with no
catalogue row answers `unknown-item` with a 404 and a did-you-mean list. So
making it resolve means a sixty-first catalogue row, and a catalogue row is not a
cheap thing: it needs a documentation page or the build fails, an entry in
`components/meta.json`, a slot in the global alias namespace, and it appears on
the component roster and in `/r/index.json` alongside sixty components as though
it were a sixty-first. Paying all of that for a file of type unions is
the wrong shape.

**Rewrite imports at generation time**, emitting a relative `./status` into the
shipped copy. Rejected: the emitted file would then differ from the reviewed
file. `<ComponentSource>` would show one thing and the consumer would receive
another, so the source a reviewer approved would not be the source anybody runs.

**Duplicate the small parts into each component.** Rejected above, and worth
restating as the reason this record exists at all: the cost is not the bytes, it
is that a vocabulary with sixty copies is a vocabulary with no owner.

## Revisiting this [#revisiting-this]

Revisit when the substrate grows past what a consumer would sit down and read, or
when it acquires something algorithmic rather than declarative. The line is the
one [ADR 0002](./0002-shadcn-registry-distribution.mdx) already drew: deriving a
ramp, converting between unit systems and scoring a questionnaire are all
computing, and a module that computes belongs in a package that is imported, not
in a file that is copied. A module that declares belongs here.

<LastUpdated />

<Reviewed />
