Files
agentic-dev-template/docs/guides/adding-a-library.md
Danijel Martinek 603104ca97 docs(guides): add adding-a-library.md human reading-room guide
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>
2026-05-14 05:52:08 +00:00

12 KiB
Raw Blame History

Adding a library

Human reading-room guide for the library evaluation policy. For decision-record density, see ADR-022. For the agent runbook, see .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 above).

2. Check for an existing trace

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:

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


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