From 7f1a8d0212922cf7c199d5a872dcf39d0d19977d Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Thu, 14 May 2026 06:41:28 +0200 Subject: [PATCH] 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. --- .../adr-022-library-evaluation-policy.md | 272 ++++++++++++++ docs/glossary.md | 6 + docs/work/_state.json | 2 +- ...026-05-14-library-evaluation-policy.prd.md | 333 ++++++++++++++++++ 4 files changed, 612 insertions(+), 1 deletion(-) create mode 100644 docs/decisions/adr-022-library-evaluation-policy.md create mode 100644 docs/work/prds/2026-05-14-library-evaluation-policy.prd.md diff --git a/docs/decisions/adr-022-library-evaluation-policy.md b/docs/decisions/adr-022-library-evaluation-policy.md new file mode 100644 index 0000000..3ccbb1f --- /dev/null +++ b/docs/decisions/adr-022-library-evaluation-policy.md @@ -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 `, 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/` | 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/-.md`. Always written, +regardless of whether an accompanying ADR exists. Shape: + +```markdown +--- +package: +version: "" +tier: app | feature | core +decision: approved | rejected +date: +deciders: [, ...] +adr: adr-NNN | null +filter-results: + license: + types: native | "@types/" | none + maintenance: active | dormant | abandoned + boundary-fit: pass | fail + shadow-check: pass | fail | "shadows " + eu-residency: ok | n/a | self-hostable | fail + cve-scan: clean | "" | fail + named-consumer: pass | fail +verification-commands: + - +--- + +## Filter: + + + +## Prompt: + + +``` + +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 `` 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 --tier --target +``` + +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** diff --git a/docs/glossary.md b/docs/glossary.md index fe9d17e..6608233 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -296,6 +296,12 @@ A `pnpm turbo gen ` 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/-.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 ` 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**: diff --git a/docs/work/_state.json b/docs/work/_state.json index 38c9adf..2928188 100644 --- a/docs/work/_state.json +++ b/docs/work/_state.json @@ -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", diff --git a/docs/work/prds/2026-05-14-library-evaluation-policy.prd.md b/docs/work/prds/2026-05-14-library-evaluation-policy.prd.md new file mode 100644 index 0000000..7c30d6f --- /dev/null +++ b/docs/work/prds/2026-05-14-library-evaluation-policy.prd.md @@ -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 ` 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/-.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 ` 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 `(): `. +- **`--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 +--tier --target `. 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 ` 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 `**, 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 `, derives the tier from + the path, and for each new runtime dep checks that + `docs/library-decisions/*-.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 ` 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 ` in the + sandbox before issuing its verdict. +- **Generator templates** — each `turbo/generators/templates/core-package//` + 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/.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/-.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: ` per filter + +one `## Prompt: ` 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/.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 `. + 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 ` 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.