Translates ADR-022 from decision-record density into an onboarding narrative: why the policy exists, the tier trigger, the four enforcement layers, a step-by-step walkthrough, and worked approved/rejected examples (clsx pass, trpc-to-openapi named-consumer fail). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
171 lines
12 KiB
Markdown
171 lines
12 KiB
Markdown
# Adding a library
|
||
|
||
Human reading-room guide for the library evaluation policy. For decision-record density, see [ADR-022](../decisions/adr-022-library-evaluation-policy.md). For the agent runbook, see [`.claude/skills/evaluate-library/SKILL.md`](../../.claude/skills/evaluate-library/SKILL.md).
|
||
|
||
---
|
||
|
||
## Why this exists
|
||
|
||
This repo ships with a deliberately narrow third-party surface. Every feature package carries the same six runtime dependencies — `@repo/core-shared`, `@trpc/server`, `inversify`, `payload`, `reflect-metadata`, `zod` — and nothing else. That uniformity isn't an accident; it's the result of unstated discipline that the boundary-tag system and the manifest-first ordering silently reward.
|
||
|
||
The discipline was not codified. Before ADR-022, anyone — human or agent — could run `pnpm add <pkg>` with no checkpoint between intent and lockfile. Three signals made the gap undeniable:
|
||
|
||
1. **The 2026-05-14 OpenAPI near-miss.** An exploratory session nearly added `trpc-to-openapi`, `zod-to-json-schema`, and a build-time generator before someone asked "who calls this code path?" The honest answer was nobody — all callers were TypeScript using `createCaller`. The library would have shipped ~30 lines of `.meta({...})` annotations per router and a `superjson`-incompatible HTTP handler in exchange for zero consumers. Pure carrying cost, caught by a chance question, not by a system.
|
||
|
||
2. **Post-hoc ADRs don't prevent bad choices.** ADR-002 (Inversify), ADR-014 (Sentry), and ADR-017 (OpenTelemetry) were all written after adoption. By the time the record existed, the lockfile already held the dep. No mechanism existed to catch a bad choice before it became a migration project.
|
||
|
||
3. **No audit for unintended adoption.** `pnpm fallow` audits dead code; `pnpm conformance` audits manifest drift; `pnpm coverage:diff` audits change coverage. There was no equivalent for "did we just adopt a library nobody asked for?"
|
||
|
||
A fourth pressure: this template is EU-resident and GDPR-bound. A library that defaults to a US-only SaaS endpoint silently moves user data out of the EU the moment it's imported and configured with defaults. The old process had no point where that was caught.
|
||
|
||
ADR-022 codifies the de-facto discipline, formalises the enforcement stack, and makes rejection records first-class so future agents don't re-evaluate libraries that were already rejected for known reasons.
|
||
|
||
---
|
||
|
||
## When the policy applies (tier trigger)
|
||
|
||
The policy maps onto the workspace boundary-tag system already enforced by ESLint and Turborepo. No new mental model required.
|
||
|
||
| Where the dep lands | Process required | Companion record |
|
||
| -------------------------------------------- | ------------------- | ---------------- |
|
||
| `apps/<x>` (any app) | Author's call; none | – |
|
||
| `devDependencies` in any package | Exempt | – |
|
||
| `feature` package (e.g. `packages/auth`) | Trace required | – |
|
||
| `core` package (e.g. `packages/core-shared`) | Trace required | ADR required |
|
||
| New optional-core category | Trace required | ADR required |
|
||
|
||
**App-tier** additions (Next.js, Payload CMS, TanStack Start) are at the author's discretion. Apps have clear blast-radius bounds; the conformance system doesn't govern their `node_modules`.
|
||
|
||
**Dev-deps** (linters, test runners, type-only packages) are exempt. They never run in production, never move user data, and are already bounded by the tooling packages (`core-eslint`, `core-typescript`).
|
||
|
||
**Feature- and core-tier** runtime deps require a trace — this is where the boundary-tag rules govern what can be imported, and where a bad choice propagates across all apps that consume the feature.
|
||
|
||
---
|
||
|
||
## Four enforcement layers
|
||
|
||
The policy enforces at four latencies, mirroring the shape of the conformance system (ADR-012):
|
||
|
||
| Layer | Latency | What it catches |
|
||
| ------------------------------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| **Claude `PreToolUse`/`PostToolUse` hook** | Inline (before `pnpm add` runs) | Agent skipping the skill before editing `package.json` or running `pnpm add`. Injects a `<system-reminder>` directing the agent to the skill — does **not** auto-deny, because app-tier and devdep additions are common and exempt. |
|
||
| **`/evaluate-library` skill** | Seconds | The decision itself: walks the eight filters, writes the trace file to `docs/library-decisions/`, and returns pass/fail. |
|
||
| **Git pre-commit hook** | Pre-commit | Humans or agents who ran `pnpm add` without invoking the skill. The hook detects a new runtime dep in a feature/core `package.json` without a corresponding trace file and blocks the commit. |
|
||
| **Sandcastle reviewer prompt** | Per-slice | Bypasses that slipped past pre-commit (e.g. direct lockfile edits). The reviewer checks for unevaluated deps before approving a slice. |
|
||
|
||
The hook is a reminder-injector, not a blocker — false positives (app-tier, devdeps) are common. The pre-commit hook is the deterministic gate for humans. The skill is the natural path both layers point to.
|
||
|
||
---
|
||
|
||
## How to add a library
|
||
|
||
### 1. Check whether the policy applies
|
||
|
||
Is the dep going into a `feature` or `core` package as a runtime dependency? If yes, continue. If it's going into an `apps/<x>` directory or as a devDependency, you can skip the evaluation (see [tier trigger](#when-the-policy-applies-tier-trigger) above).
|
||
|
||
### 2. Check for an existing trace
|
||
|
||
```bash
|
||
ls docs/library-decisions/ | grep <package-name>
|
||
```
|
||
|
||
If a trace already exists and is marked `decision: rejected`, read it before proceeding. The rejection reasoning is the permanent record — if circumstances have changed (e.g. a named consumer now exists), you can re-evaluate and write a new trace. Otherwise, the existing rejection stands.
|
||
|
||
### 3. Invoke the evaluate-library skill
|
||
|
||
```
|
||
/evaluate-library <package-name> --tier <feature|core> --target <package-path>
|
||
```
|
||
|
||
The skill walks the eight filters in collect-cheap-skip-expensive order:
|
||
|
||
- **Cheap filters** (always complete, even on failure): `license`, `types`, `shadow-check`, `boundary-fit`
|
||
- **Expensive filters** (short-circuit on first failure): `maintenance`, `cve-scan`, `eu-residency`, `named-consumer`
|
||
|
||
It then answers the three discussion prompts and writes a trace file to `docs/library-decisions/<YYYY-MM-DD>-<package-name>.md`.
|
||
|
||
### 4. Read the result
|
||
|
||
The skill returns `approved` or `rejected` with the filter that caused rejection (if any). The trace file is the permanent record either way.
|
||
|
||
- **Approved** → the trace is written; proceed with `pnpm add`.
|
||
- **Rejected** → the trace records the failure; do not add the library. If you believe the rejection was wrong, re-evaluate with new evidence rather than bypassing.
|
||
|
||
### 5. Add the library and commit
|
||
|
||
After an `approved` trace:
|
||
|
||
```bash
|
||
pnpm add <package-name> --filter <target-package>
|
||
```
|
||
|
||
Include the trace file in the same commit as the `package.json` and lockfile changes. The pre-commit hook verifies this pairing.
|
||
|
||
### 6. For core-tier additions: write the ADR
|
||
|
||
If the dep lands in a `core` package or new optional-core, write an ADR before or alongside the trace. The `alternatives-considered` section of the trace is duplicated into the ADR. Cite the ADR in the trace's `adr:` frontmatter field.
|
||
|
||
---
|
||
|
||
## Worked example: approved (`clsx`)
|
||
|
||
**Scenario:** Adding `clsx` to `packages/navigation` for conditional class-name composition in UI components.
|
||
|
||
**Tier:** `feature` (packages/navigation is a feature package).
|
||
|
||
**Evaluation summary:**
|
||
|
||
| Filter | Result |
|
||
| -------------- | ------------------------------------------------------------------------ |
|
||
| license | MIT — on allowlist |
|
||
| types | Native `.d.ts` — pass |
|
||
| maintenance | Last release 15 months ago, active PR triage — pass |
|
||
| boundary-fit | Pure string utility, zero transitive deps, no vendor SDK — pass |
|
||
| shadow-check | No existing class-composition utility in workspace — pass |
|
||
| eu-residency | No network calls, no vendor endpoint — n/a |
|
||
| cve-scan | 0 vulnerabilities — clean |
|
||
| named-consumer | `navigation-menu.tsx` + `mobile-nav.tsx` both blocked on this — **pass** |
|
||
|
||
**Decision: approved.** Named consumer exists today (not hypothetically), migration cost is mechanical (swap back to ternaries), alternatives evaluated (`classnames`, inline ternaries, `tailwind-merge`).
|
||
|
||
Full trace: [`.claude/skills/evaluate-library/EXAMPLES/approved-example.md`](../../.claude/skills/evaluate-library/EXAMPLES/approved-example.md)
|
||
|
||
---
|
||
|
||
## Worked example: rejected (`trpc-to-openapi`)
|
||
|
||
**Scenario:** Exposing the tRPC router surface as a REST API for potential external consumers.
|
||
|
||
**Tier:** `core` (would land alongside the tRPC router configuration in a core package).
|
||
|
||
**Evaluation summary:**
|
||
|
||
| Filter | Result |
|
||
| -------------- | ------------------------------------------------------------------- |
|
||
| license | MIT — pass |
|
||
| types | Native — pass |
|
||
| maintenance | Active — pass |
|
||
| boundary-fit | Imports `@trpc/server` and `zod` (both workspace must-haves) — pass |
|
||
| shadow-check | No existing OpenAPI generator in workspace — pass |
|
||
| eu-residency | Pure in-process, no vendor endpoint — n/a |
|
||
| cve-scan | Clean — pass |
|
||
| named-consumer | **No named consumer — FAIL** |
|
||
|
||
**Decision: rejected.** Seven of eight filters passed. The failure was `named-consumer`. All current API callers are TypeScript using `createCaller`; no HTTP REST clients exist, and no external consumers are blocked waiting for an OpenAPI spec. Speculative future partners do not count as consumers.
|
||
|
||
This trace is the permanent record. The next agent considering `trpc-to-openapi` finds it in `ls docs/library-decisions/` in under a second and does not re-litigate. If a concrete integration is later planned, re-open the evaluation with the integration as the named consumer.
|
||
|
||
Full trace: [`.claude/skills/evaluate-library/EXAMPLES/rejected-trpc-to-openapi.md`](../../.claude/skills/evaluate-library/EXAMPLES/rejected-trpc-to-openapi.md)
|
||
|
||
---
|
||
|
||
## Cross-links
|
||
|
||
- [ADR-022 — Library evaluation policy](../decisions/adr-022-library-evaluation-policy.md) — the authoritative decision record with full rationale, alternatives considered, and consequences
|
||
- [`docs/library-decisions/_template.md`](../library-decisions/_template.md) — trace file template; the skill uses this shape automatically, but it's useful when writing or reviewing a trace manually
|
||
- [`.claude/skills/evaluate-library/SKILL.md`](../../.claude/skills/evaluate-library/SKILL.md) — agent runbook; the authoritative guide for agents running the evaluation
|
||
- [ADR-006](../decisions/adr-006-vertical-feature-packages.md) — vertical feature packages (the boundary-tag system the tier trigger maps to)
|
||
- [ADR-010](../decisions/adr-010-turbo-boundaries.md) — Turborepo boundaries (enforcement substrate)
|
||
- [ADR-017](../decisions/adr-017-opentelemetry-vendor-isolation.md) — vendor isolation pattern (motivates the boundary-fit and EU-residency filters)
|
||
- [`docs/glossary.md`](../glossary.md) — entries for **Library trace** and **Pre-shipped trace**
|