docs: introduce library evaluation policy (ADR-022 + PRD)

- ADR-022 codifies the tiered library-evaluation policy: 8 hard
  auto-reject filters (license, types, maintenance, boundary-fit,
  shadow-check, EU residency, CVE scan, named consumer), 3
  discussion prompts, per-decision trace artifact at
  docs/library-decisions/, and a 4-layer enforcement stack
  (Claude PreToolUse/PostToolUse hook -> evaluate-library skill ->
  pre-commit hook -> sandcastle reviewer prompt). Mirrors the
  conformance-system latency pattern from ADR-012.
- PRD at docs/work/prds/2026-05-14-library-evaluation-policy.prd.md
  seeds the implementation epic; status: approved, ready for
  \`pnpm work decompose\`.
- Glossary gains "Library trace" + "Pre-shipped trace" entries
  referenced by both artifacts.

Catalyst: the 2026-05-14 grill session nearly adopted
trpc-to-openapi + zod-to-json-schema before someone asked who the
HTTP consumer was. Honest answer: none -- all callers are TS via
createCaller. This policy makes that question structurally
unavoidable for any future feature- or core-tier dep.
This commit is contained in:
2026-05-14 06:41:28 +02:00
parent 52af9f1fdd
commit 7f1a8d0212
4 changed files with 612 additions and 1 deletions

View File

@@ -0,0 +1,272 @@
# ADR-022 — Library evaluation policy
**Status:** Accepted
**Date:** 2026-05-14
**Related:** ADR-006 (vertical feature packages), ADR-010 (turbo boundaries), ADR-014 (Sentry observability), ADR-017 (OpenTelemetry + vendor isolation), ADR-019 (sandcastle agent orchestration), ADR-021 (release-please versioning)
**Companion guide:** `docs/guides/adding-a-library.md` (human reading-room)
**Companion skill:** `.claude/skills/evaluate-library/SKILL.md` (authoritative agent runbook)
## Context
This template ships with a deliberately narrow third-party surface. Every feature
package today 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 (ADR-006, ADR-010) and the manifest-first ordering
(ADR-012) silently reward.
The discipline is not codified. New dependencies get added by anyone — human or
agent — running `pnpm add <pkg>`, with no checkpoint between intent and lockfile.
Three recent signals show the gap:
1. An exploratory grill session on **2026-05-14** nearly added `trpc-to-openapi`
plus `zod-to-json-schema` plus a build-time generator to the repo before
stopping to ask "who calls this code path?" The honest answer was "nobody —
all callers are TypeScript via `createCaller`." The library would have shipped
~30 lines of `.meta({...})` annotations per router and a `superjson`-incompatible
HTTP handler in exchange for zero downstream consumers. Pure carrying cost,
caught by a chance question, not by a system.
2. **Three existing ADRs already record post-hoc library decisions**
ADR-002 (Inversify), ADR-014 (Sentry), ADR-017 (OpenTelemetry). Each notes
"we picked X over Y" but the records were written after adoption. By the time
the ADR existed the lockfile already held the dep. No mechanism existed to
catch a _bad_ choice before it became a migration project.
3. The repo's automation depends on the lockfile staying small and predictable.
`pnpm fallow` audits for dead code; `pnpm conformance` audits manifest drift;
`pnpm coverage:diff` audits change coverage. There is no equivalent audit for
"did we just adopt a library nobody asked for?"
A fourth pressure comes from this template being EU-resident and GDPR-bound.
A library that defaults to a US-only SaaS endpoint (telemetry, analytics, AI,
log aggregation) silently moves user data out of the EU as soon as it's imported
and configured with defaults. The current process has no point where that gets
caught.
The decision below codifies the de-facto discipline, formalizes the agent-loop
hook that prevents drift, and makes rejection records first-class so future
agents don't re-evaluate libraries that were already rejected for known reasons.
## Decision
Adopt a **tiered library-evaluation policy** with eight hard auto-reject filters,
three discussion prompts, a per-decision trace artifact, and a four-layer
enforcement stack.
### 1. Tiered trigger (by boundary tag)
| Tier the dep lands in | Process | Companion record |
| ------------------------------------ | ------------------------ | ---------------- |
| `apps/<x>` | Author's call; no policy | |
| `feature` (e.g. `packages/auth`) | Trace required | |
| `core` (e.g. `packages/core-shared`) | Trace required | ADR required |
| New optional-core category | Trace required | ADR required |
The trigger maps onto the workspace-tag boundary already enforced by ESLint
(`eslint-plugin-boundaries`) and Turborepo (`turbo boundaries`). No new mental
model — the policy is a corollary of an existing one.
### 2. Eight hard auto-reject filters
Failing any single filter is an automatic reject. Trace records the failure.
1. **License allowlist.** Only `MIT`, `Apache-2.0`, `BSD-*`, `ISC`, `MPL-2.0`.
Anything else (GPL family, AGPL, CC-BY-NC, custom EULAs) is a no.
2. **TypeScript-native or `@types/*` available.** The repo is strict TS;
un-typed JS libraries shift maintenance cost to the integrating feature.
3. **Not abandoned.** Last release < 18 months **AND** PR/issue activity
< 12 months. The triple-AND avoids killing finished-but-stable libraries
(`reflect-metadata`-style).
4. **Boundary-tag fit.** A dep added to a feature package cannot require
imports the boundary rules forbid (e.g. a Sentry SDK in a feature
ADR-017 §4 forbids this).
5. **Doesn't shadow an existing must-have.** Proposing `valibot` when `zod`
is locked, or `tsyringe` when Inversify is locked by ADR-002, is an
auto-reject; the replacement must be a separate ADR with consequences
analysis, not a parallel adoption.
6. **EU data residency for hosted/SaaS components.** If the package transmits
user data, telemetry, or business state to a vendor-controlled endpoint
by default, that vendor must offer an EU data region, and the integration
must be configured to use it. Self-hostable packages, on-device libraries,
and build-time-only tools are exempt.
7. **CVE scan clean.** `pnpm audit --audit-level=moderate` clean at adoption
time. Documented allowlist mechanism for accepted-risk advisories.
8. **Named consumer exists now, not hypothetically.** The integration must
answer "who calls this code path today, or who is blocked waiting for it?"
"Future code that might exist" is not a consumer. This filter is the
direct response to the 2026-05-14 OpenAPI near-miss.
### 3. Three discussion prompts
Filters that don't auto-reject but must be answered in the trace, either
direction acceptable with justification.
- **What does it replace?** New-and-old running in parallel is a smell.
- **Migration cost out.** What does ripping this back out look like 18 months
from now? Mechanical, hard, or impossible?
- **Alternatives considered.** Two named alternatives at minimum (or "none
with explanation"). Required for `feature`-tier; required _and_ duplicated
into the ADR for `core`-tier.
### 4. Trace artifact
Every decision approved or rejected emits a trace file at
`docs/library-decisions/<YYYY-MM-DD>-<package-name>.md`. Always written,
regardless of whether an accompanying ADR exists. Shape:
```markdown
---
package: <name>
version: "<semver range>"
tier: app | feature | core
decision: approved | rejected
date: <YYYY-MM-DD>
deciders: [<author>, ...]
adr: adr-NNN | null
filter-results:
license: <SPDX id>
types: native | "@types/<x>" | none
maintenance: active | dormant | abandoned
boundary-fit: pass | fail
shadow-check: pass | fail | "shadows <x>"
eu-residency: ok | n/a | self-hostable | fail
cve-scan: clean | "<advisory-id>" | fail
named-consumer: pass | fail
verification-commands:
- <literal command that produced each filter result>
---
## Filter: <name>
<prose>
## Prompt: <name>
<prose>
```
Frontmatter is the machine surface (greppable, schema-stable). Headings are
the human surface. Rejection traces are first-class the OpenAPI scenario,
had this policy existed, would have produced a permanent record so the next
agent considering `trpc-to-openapi` finds the prior reasoning in <1s of
`ls docs/library-decisions/`.
### 5. Four-layer enforcement stack
Mirrors the latency-tiered shape of the conformance system (ADR-012).
| Layer | Latency | Catches |
| -------------------------------------- | ---------- | ---------------------------------------------------------------- |
| Claude `PreToolUse`/`PostToolUse` hook | inline | Agent skipping the skill before `pnpm add` / `package.json` edit |
| `evaluate-library` skill | seconds | The decision itself + writes the trace |
| Git pre-commit hook | pre-commit | Humans or agents bypassing the skill |
| Sandcastle reviewer prompt | per-slice | Bypasses that slipped past pre-commit |
The Claude hook injects a `<system-reminder>` directing the agent to the skill
but does **not** auto-deny (false-positive paths like dev-deps and app-tier
additions are common). The pre-commit hook is the deterministic gate.
### 6. Composition with `pnpm turbo gen core-package`
The optional-cores generator emits **pre-shipped traces** one per direct
runtime dep of the new core pre-marked `decision: approved` and cited
against the relevant ADR (ADR-015 for events, ADR-016 for realtime,
ADR-018 for audit). Same frozen-snapshot discipline that the optional cores
already follow (`turbo/generators/__snapshots__/core-package/`). New optional
cores added later inherit this requirement.
### 7. Skill invocation
```
/evaluate-library <package-name> --tier <feature|core|app> --target <package-path>
```
The skill walks the eight filters in **collect-cheap-skip-expensive** order:
cheap structural filters (license, types, shadow-check, boundary-fit) run to
completion regardless of failure; expensive filters (CVE scan, EU residency
probe, maintenance signals) short-circuit after the first reject. The trace
records which filters ran and which were skipped, so a partial trace is still
useful evidence.
### 8. Backfill at policy adoption
Every existing runtime dependency in feature- and core-tier packages
(~10 deps at this writing `payload`, `inversify`, `zod`, `@trpc/server`,
`reflect-metadata`, `superjson`, `@sentry/*`, `@opentelemetry/*` family,
`socket.io`, etc.) gets a backfilled trace dated 2026-05-14 (adoption day).
ADR-002, ADR-014, ADR-017 are cited via the `adr:` frontmatter field;
verification-command output is captured at backfill time.
## Alternatives considered
- **No policy, keep relying on instinct.** Rejected. The 2026-05-14 OpenAPI
near-miss demonstrated the gap. Instinct catches some adds, misses others;
the failure mode is silent.
- **Every `package.json` change requires a trace, no tier distinction.**
Rejected. Devdeps in tooling packages and ESLint plugin bumps in apps would
drown the trace directory in noise. The boundary-tag system already
partitions blast radius the policy reuses that partition.
- **Only "category" decisions require process** (new auth provider, new ORM,
new queue). Rejected. The OpenAPI scenario was a sub-tool inside an existing
category, not a category swap. Category-only triggers miss it.
- **A central library-evaluation service / Linear queue / Slack bot.**
Rejected. The repo is agent-first and currently single-developer (+ agents).
Human-in-the-loop services don't fit the dispatch loop; the policy must be
agent-runnable end-to-end.
- **No CVE filter rely on `npm audit` ambient noise.** Rejected. CVE
status is point-in-time; pinning the result to the trace at adoption is the
whole value. Re-running the verification commands later detects drift.
- **Drop the named-consumer filter to a discussion prompt.** Rejected. The
filter is the one that would have stopped the OpenAPI flirtation; demoting
it back to a prompt is the same as not adding the filter.
## Consequences
**Positive**
- New dependencies require deliberate intent. The trace artifact + four-layer
enforcement stack make accidental adds detectable at four latencies, mirroring
the conformance-system pattern that already works.
- Rejection records are permanent. Future agents considering a previously
rejected library find the trace in `docs/library-decisions/` and don't
re-litigate.
- EU data residency becomes a binary, machine-readable filter result, not
an afterthought.
- Per-decision verification commands give future agents a single source of
truth they can re-run to verify the trace is still valid.
- The policy composes with `pnpm turbo gen core-package`: optional cores
remain a one-command scaffold without bypassing the rule.
**Negative**
- New feature-tier deps take longer to land. Walking eight filters + writing
the trace is ~5 minutes of agent work per addition.
- Backfilling ~10 existing deps is one-time work; expected ~half a day of
agent dispatch.
- The Claude `PreToolUse` hook adds latency to every `pnpm add` invocation.
Mitigated by the hook being a reminder-injector, not a blocker.
- The pre-commit hook adds a new failure mode ("you added a dep but forgot
the trace"). Mitigated by the skill being the natural path the hook
reminders point to.
- The CVE filter creates a maintenance obligation: when `pnpm audit` finds
a new advisory in an already-approved dep, the trace becomes stale. Acceptable
trade-off staleness detection is exactly what `pnpm audit` already does;
the trace adds a per-dep anchor for the conversation that follows.
**Neutral**
- ADR-002, ADR-014, and ADR-017 remain authoritative for their respective
libraries. Backfilled traces cite them rather than duplicating their reasoning.
- The policy doesn't constrain transitive dependencies; `pnpm audit` and license
scanning already handle those recursively. Only direct deps require a trace.
## Related
- ADR-006 Vertical feature packages (the tag system the trigger maps to)
- ADR-010 Turbo boundaries (the enforcement substrate)
- ADR-012 Feature conventions (the conformance-system shape this policy mirrors)
- ADR-017 OpenTelemetry migration (vendor-isolation pattern the policy extends)
- ADR-019 Sandcastle agent orchestration (reviewer-prompt layer of enforcement)
- ADR-021 release-please versioning (where dep additions show up in release notes)
- Companion guide: `docs/guides/adding-a-library.md`
- Companion skill: `.claude/skills/evaluate-library/SKILL.md`
- Glossary: `docs/glossary.md` entries for **Library trace** and **Pre-shipped trace**

View File

@@ -296,6 +296,12 @@ A `pnpm turbo gen <kind>` invocation. Generator-first is **non-negotiable** —
**Slice = task = PR = commit**:
The shipping rhythm. One vertical slice closes one task, becomes one PR, lands as one commit.
**Library trace**:
A per-decision artifact at `docs/library-decisions/<YYYY-MM-DD>-<package-name>.md` recording the outcome of the `evaluate-library` skill against a candidate third-party dependency. Frontmatter is the machine surface (tier, decision, filter-results enum); headings are the human surface (one per filter + one per prompt). Both `decision: approved` and `decision: rejected` traces are first-class — the rejection record is the value, since it stops future agents from re-evaluating the same library. Required for any new runtime dep in a `feature`- or `core`-tagged package; the pre-commit hook blocks the commit if the trace is missing.
**Pre-shipped trace**:
A library trace emitted by `pnpm turbo gen core-package <name>` for each direct runtime dep of a pre-curated optional core. Pre-approved by the template's ADRs (015 events / 016 realtime / 018 audit). Generated alongside the core's frozen snapshot so optional cores satisfy the library-evaluation policy by default.
## Modes
**Production mode**:

View File

@@ -1,5 +1,5 @@
{
"updated_at": "2026-05-13T18:41:19.463Z",
"updated_at": "2026-05-14T04:41:30.276Z",
"epics": {
"2026-05-13-binder-wrap-helper": {
"status": "done",

View File

@@ -0,0 +1,333 @@
---
id: 2026-05-14-library-evaluation-policy
title: Library evaluation policy — skill, traces, enforcement stack
type: prd
status: approved
author: danijel
created: 2026-05-14
adr: adr-022
---
## Problem
This template ships with a deliberately narrow third-party surface — every
feature package today holds the same 6 runtime deps and nothing else. That
discipline is uncodified. New dependencies enter via `pnpm add <pkg>` with no
checkpoint between intent and lockfile, and three recent signals show the gap:
1. The 2026-05-14 grill session nearly added `trpc-to-openapi` + `zod-to-json-schema`
- a build-time generator before someone asked "who calls this code path?"
The honest answer was "nobody — all callers are TypeScript via `createCaller`."
2. ADR-002 (Inversify), ADR-014 (Sentry), ADR-017 (OpenTelemetry) each record
library decisions, but every record was written _after_ adoption. No mechanism
exists to catch a bad choice before it becomes a migration project.
3. The repo 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 with defaults. Nothing currently flags this.
ADR-022 codifies the policy. This PRD implements it.
## Goal
A four-layer enforcement stack — Claude hook, skill, pre-commit hook, sandcastle
reviewer prompt — that makes every new runtime dependency in a feature- or
core-tier package produce a permanent **library trace** at
`docs/library-decisions/<YYYY-MM-DD>-<package-name>.md`, with rejection traces
treated as first-class records.
## In scope
- The `evaluate-library` skill at `.claude/skills/evaluate-library/SKILL.md`
authoritative agent runbook; walks 8 hard filters + 3 prompts; writes the trace.
- The human reading-room guide at `docs/guides/adding-a-library.md` with worked
examples (approved + rejected).
- The `docs/library-decisions/` directory + `_template.md` schema reference.
- A Zod-validated trace-schema module (`scripts/library-decisions/schema.mjs`)
shared by the skill, the pre-commit checker, and the generator.
- Claude `PreToolUse` hook (`.claude/hooks/library-policy-nudge.sh`) — matches
`pnpm add` / `pnpm i <pkg>` in Bash invocations; emits skill reminder.
- Claude `PostToolUse` hook (also in `library-policy-nudge.sh`, dispatching by
event type) — matches `Edit`/`Write` against any `**/package.json`.
- Pre-commit hook check script (`scripts/library-decisions/check.mjs`) wired
into `.husky/pre-commit`. Blocks the commit when a new runtime dep is staged
in a feature/core package and no sibling trace file is staged.
- Sandcastle reviewer prompt update (`.sandcastle/reviewer.prompt.md`) — the
reviewer agent runs the same check before issuing approve/reject.
- Optional-cores generator templates (`turbo/generators/templates/core-package/`)
emit pre-shipped traces per direct dep, dated at generation time, marked
`decision: approved`, citing the relevant ADR (015/016/018). Five generators
updated: `events`, `realtime`, `audit`, `trpc`, `ui`.
- Backfill traces for every existing runtime dependency in feature- and core-
tier packages, dated 2026-05-14, citing existing ADRs (002/014/017) where
applicable. Approx 10 traces.
- `CLAUDE.md` "Key Conventions" gets a one-line bullet pointing to ADR-022 +
the guide.
## Out of scope
- Transitive dependency tracing — `pnpm audit` already handles recursive scanning.
- Bundle-size analysis — Vercel / Vite build output already reports this.
- Auto-removal of approved-then-unused deps — `pnpm fallow` territory.
- License auto-enforcement at the lockfile layer (license-checker plugins) —
defer until the policy has run for some time and we know where it leaks.
- Anything app-tier. Deps in `apps/*` are author's call per the tier model.
- Devdeps in any tier. Only `dependencies` (runtime) require traces.
## Constraints
- **ADR-022** is the source of truth. This PRD implements but does not extend it.
- **ADR-006 + ADR-010** — the tier trigger maps onto the existing boundary-tag
system. No new mental model; ESLint already partitions blast radius.
- **ADR-019** — the sandcastle reviewer prompt is one of four enforcement
layers. Whatever the agent loop does must compose with the existing prompt
shape at `.sandcastle/reviewer.prompt.md`.
- **ADR-021** — release-please picks up dependency changes from commit history.
The trace file landing in the **same commit** as the `package.json` change
is required so release notes correlate cleanly with policy records.
- **Conformance system parity** — the enforcement stack mirrors the 5-gate
latency pattern from ADR-012. Same vocabulary, same agent feedback loop.
- **Conventional Commits** — every commit produced by the implementation
follows `<type>(<scope>): <subject>`.
- **`--no-verify` is forbidden** — the bash-guard hook already enforces this;
the new pre-commit check inherits that protection.
- **Skill must be deterministic from explicit args** — `/evaluate-library <name>
--tier <feature|core|app> --target <package-path>`. The Claude hook produces
exactly this invocation from a `pnpm add` command line.
## Success criteria
- `pnpm typecheck && pnpm test && pnpm lint && pnpm conformance && pnpm fallow:audit`
pass green at the end of the epic.
- `pnpm coverage:diff` covers every changed executable line introduced by
the implementation slices.
- Attempting to commit a new feature-tier runtime dep without a sibling trace
file is blocked by the pre-commit hook with a clear error pointing to the skill.
- Running the `evaluate-library` skill against `trpc-to-openapi` (the rejected
library from the grill session) produces a `decision: rejected` trace with
`named-consumer: fail` and prose citing the conversation, in <90 seconds of
agent work.
- Running `pnpm turbo gen core-package events` (or any other optional core)
emits pre-shipped traces for every direct dep of that core into
`docs/library-decisions/`, all `decision: approved` and ADR-cited.
- The Claude `PreToolUse` hook fires on `pnpm add <anything>` and emits the
skill-reminder system-reminder; does **not** auto-deny.
- All existing runtime deps in feature- and core-tier packages have backfilled
trace files dated 2026-05-14 in `docs/library-decisions/`.
- `CLAUDE.md` Key Conventions includes the one-line policy bullet.
- `docs/glossary.md` already includes **Library trace** and **Pre-shipped
trace** entries (landed inline during the grill session).
## User stories
1. **As a developer adding a new feature dependency**, I want a deterministic
skill that walks me through the 8 filters and 3 prompts in collect-cheap-
skip-expensive order, so I don't forget any check and the trace file is
written automatically with my answers.
2. **As an agent dispatched against a slice that needs a new dep**, I want the
Claude `PreToolUse` hook to inject a system-reminder pointing me at the
skill the moment I'm about to run `pnpm add`, so I don't bypass the policy
by reflex.
3. **As an agent editing a `package.json` by hand**, I want the Claude
`PostToolUse` hook to inject the same reminder, so the policy isn't
sidestepped by paste-then-install.
4. **As a reviewer (human or agent)**, I want the pre-commit hook to refuse a
commit that adds a runtime dep to a feature/core package without a sibling
trace file, so I don't have to remember to check during review.
5. **As a future agent considering a previously-rejected library**, I want
to find the rejection trace in `docs/library-decisions/` in <1s of
`ls`/`grep`, so I don't re-litigate a decision that has already been made.
6. **As an EU-resident maintainer**, I want the EU-data-residency filter to
reject US-only SaaS components by default and force a `self-hostable` or
`EU-region-configured` justification in the trace, so user data doesn't
leave the EU silently.
7. **As a maintainer scaffolding an optional core via
`pnpm turbo gen core-package <name>`**, I want pre-shipped traces emitted
for every direct dep of the new core, so the policy is satisfied by
construction.
8. **As a security-conscious maintainer**, I want the CVE-scan filter to run
`pnpm audit --audit-level=moderate` at evaluation time and snapshot the
result + commands into the trace, so I can re-run them later to detect drift.
9. **As an agent reviewing a slice in sandcastle**, I want the reviewer prompt
to check for trace presence + correctness, so I can reject incompliant
slices without needing a separate workflow.
10. **As a maintainer reading the repo for the first time**, I want
`docs/guides/adding-a-library.md` to explain the policy with worked
examples (one approved, one rejected), so I understand the why and how
before I face the gate myself.
## Implementation decisions
**Module sketch** — what lands where, by package and concern (no file paths
where prose suffices):
- **The skill itself** — `.claude/skills/evaluate-library/` follows the same
shape as `to-prd`, `grill-with-docs`, `improve-codebase-architecture`.
SKILL.md is authoritative; supporting files (`POLICY.md` mirroring ADR-022,
`TRACE-TEMPLATE.md` showing the YAML+headings shape, `EXAMPLES/` worked
cases) flesh it out. The skill is invocable via slash command
`/evaluate-library`.
- **Trace schema module** — a small shared module at
`scripts/library-decisions/schema.mjs` exporting (1) a Zod schema for the
trace's frontmatter, (2) a parser that reads a trace file and returns the
validated frontmatter, (3) a serializer that takes filter results + prose
blocks and emits a trace file. Both the skill and the pre-commit checker
import this module. Deep module — small interface (parse/serialize/validate),
high leverage across the four enforcement layers.
- **Pre-commit check script** — `scripts/library-decisions/check.mjs`. Walks
`git diff --cached --name-only -- '**/package.json'`, for each file extracts
newly-added dep lines via `git diff --cached <file>`, derives the tier from
the path, and for each new runtime dep checks that
`docs/library-decisions/*-<name>.md` is also staged with `decision: approved`.
Exits non-zero with a pointer to the skill if any check fails. Invoked from
`.husky/pre-commit` as step 4 (after the existing state-sync guard).
- **Claude hooks** — a single `.claude/hooks/library-policy-nudge.sh` that
dispatches on `tool_use_type` to handle both `PreToolUse` (Bash with
`pnpm add` / `pnpm i <pkg>` pattern) and `PostToolUse` (Edit/Write on
`**/package.json`). Same style as the existing `generator-first-nudge.sh`.
Emits a non-blocking system-reminder to stdout that the harness threads
into the agent's next turn.
- **Sandcastle reviewer prompt** — append a "Library-trace check" section
to `.sandcastle/reviewer.prompt.md`. The reviewer runs
`node scripts/library-decisions/check.mjs --staged-against <base>` in the
sandbox before issuing its verdict.
- **Generator templates** — each `turbo/generators/templates/core-package/<name>/`
gets a `docs/library-decisions/` subtree with one `.md` per direct dep of
that core. The generator copies these alongside the core's package.json
into the workspace. Frozen via the existing
`turbo/generators/__snapshots__/core-package/<name>.snapshot.json` mechanism.
- **Backfill traces** — write one trace per existing runtime dep in feature/
core tier. The deps cluster naturally by ADR provenance: ADR-002 cluster
(Inversify + reflect-metadata), ADR-014 cluster (Sentry family), ADR-017
cluster (OpenTelemetry family), and the un-cited cluster (`payload`,
`@trpc/server`, `zod`, `superjson`, plus any others surfaced by inventory).
All traces dated 2026-05-14, `decision: approved`, ADR citation where the
cluster maps to one.
- **`CLAUDE.md` update** — one bullet in Key Conventions: _"New runtime
dependencies in feature- or core-tier packages require a trace at
`docs/library-decisions/<date>-<name>.md` produced by the `evaluate-library`
skill — see ADR-022."_
**Trace schema (frontmatter)** — Zod schema (lifted from ADR-022 §4):
```
package: string
version: string // semver range as written in package.json
tier: "app" | "feature" | "core"
decision: "approved" | "rejected"
date: ISO date string
deciders: string[]
adr: string | null // "adr-NNN" slug or null
filter-results: {
license: SPDX-id-string
types: "native" | `@types/${string}` | "none"
maintenance: "active" | "dormant" | "abandoned"
boundary-fit: "pass" | "fail"
shadow-check: "pass" | "fail" | `shadows ${string}`
eu-residency: "ok" | "n/a" | "self-hostable" | "fail"
cve-scan: "clean" | `${cve-id}` | "fail"
named-consumer: "pass" | "fail"
}
verification-commands: string[]
accepted-cves?: string[] // optional per-trace allowlist
```
Headings (machine-checkable order): one `## Filter: <name>` per filter +
one `## Prompt: <name>` per prompt, in the order listed in ADR-022.
**Skill fail behavior** — collect-cheap-skip-expensive. The four cheap
structural filters (license, types, shadow-check, boundary-fit) always run
to completion. The four expensive filters (maintenance, CVE scan, EU residency,
named-consumer) short-circuit after the first reject. The trace records which
filters ran and which were skipped, with a "skipped because earlier filter
already rejected" sentinel value.
**Pre-commit hook decision-state check** — beyond presence, the script also
verifies that the trace's `decision` matches the dep status: a dep listed in
`package.json` requires `decision: approved`; a trace with `decision: rejected`
that names a dep that's also in the package.json is a hard fail (rejected
libraries cannot ship).
**Conformance system composition** — no new use cases, controllers, manifest
entries, audits, events, or jobs. This PRD is a workflow/policy implementation,
not a feature-domain change. The conformance gates apply only to the new
TypeScript/JS modules (Zod schema module + check script) — they get standard
vitest coverage.
## Testing decisions
- **`scripts/library-decisions/schema.mjs`** — unit tests covering: valid
trace parses round-trip; missing required field rejected; unknown filter
rejected; invalid enum value rejected.
- **`scripts/library-decisions/check.mjs`** — integration tests covering: new
feature-tier dep without trace → fail with exit 1; new feature-tier dep with
approved trace → pass; new feature-tier dep with rejected trace listed in
package.json → fail; new app-tier dep (no trace required) → pass; new devdep
→ pass (devdeps exempt); multi-file staged diff with mixed pass/fail → fail
with per-package report; non-runtime dep (`peerDependencies` only) → pass.
Use a temp git repo as the test fixture.
- **The skill** — no automated test in the conformance sense (it's a prose
runbook for an agent). The success criterion is that running it against
`trpc-to-openapi` produces the documented rejection trace; verified manually
during the epic.
- **Generator pre-shipped traces** — the existing
`turbo/generators/__snapshots__/core-package/<name>.snapshot.json` snapshot
test extends to cover the new trace files. A failing snapshot is the gate.
- **Claude hook scripts** — bash smoke tests that pipe a mocked Claude
hook payload (`{ "tool_input": { "command": "pnpm add foo" } }`) into the
script and assert stderr contains the skill-reminder marker. Same style as
`generator-first-nudge.sh`'s existing tests if any (check during impl).
- **Prior art** — mirror the test patterns from `scripts/work/state-sync-guard.mjs`
(the pre-commit `_state.json` check) for the new check script; the
fixture/assertion shape carries over directly.
- **Coverage bands** — the new scripts under `scripts/library-decisions/` are
not feature packages, so they don't have a `feature.manifest.ts` and aren't
bound by per-layer coverage thresholds. Add them to the diff-coverage
exception list **only if** the diff-coverage gate is too strict on script
files; default is they should hit 100% statement coverage because they're
small.
## Open questions
- **Q1:** CVE-accepted-risk mechanism — per-trace `accepted-cves: ["CVE-XXXX-YYYY"]`
frontmatter array vs central `docs/library-decisions/_cve-allowlist.md`? —
**Recommended:** per-trace. Acceptance is library-scoped, not global; a
central file becomes a god-object that no agent reads in full.
- **Q2:** Should the policy also gate `peerDependencies` additions, or only
`dependencies`? — **Recommended:** only `dependencies`. Peer deps are a
contract, not a runtime addition; if a feature declares a peer, the actual
runtime adopter (an app or another package) is the one whose `dependencies`
the policy catches.
- **Q3:** Should the backfill be one commit per trace or one batch commit per
ADR cluster? — **Recommended:** one commit per cluster (4 commits total),
each with conventional `chore(deps): backfill library traces for <cluster>`.
Avoids both extremes (1 mega-commit and 10 noisy single-trace commits).
- **Q4:** Should the skill be permitted to **write the trace** before the
user/agent approves the final decision, or must the final write be a
separate explicit step? — **Recommended:** skill writes the trace
unconditionally at the end of evaluation; the trace IS the record, including
for rejections. No "draft" state.
- **Q5:** Where do the Claude hooks register themselves? — **Investigate:**
the existing `.claude/hooks/*.sh` are referenced by some kind of settings
file or auto-discovered. Confirm during the first slice that adds the new
hook script and matches the existing registration pattern.
## Out of scope (deferred)
- **Periodic re-verification.** Running each trace's `verification-commands`
on a schedule (nightly?) to detect drift — new CVEs, license changes,
upstream abandonment. Deserves its own PRD; would compose with `pnpm fallow`
as a sixth gate.
- **Auto-generated PR comments** that summarize the trace for human reviewers.
Nice-to-have once the policy has lived for a quarter.
- **`pnpm libs <subcommand>` ergonomic CLI** — wrapping `check.mjs` as
`pnpm libs check`, plus `pnpm libs list`, `pnpm libs orphans`, etc. Defer
until the raw script proves the workflow.
## Further notes
- **Anchored by ADR-022** — Library evaluation policy. Read that first.
- **Glossary entries** for **Library trace** and **Pre-shipped trace** landed
during the 2026-05-14 grill session that produced ADR-022.
- **Conversation provenance** — the 2026-05-14 grill-with-docs session that
produced this PRD is captured in the session transcript; ADR-022 cites the
OpenAPI near-miss as concrete catalyst.