diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index e6ed67b..9af2198 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -11,7 +11,7 @@ Generator-first: pnpm turbo gen beats hand-rolled scaffolding (non-negoti Conformance: pnpm conformance + pnpm fallow (5-gate drift detection) Conventional Commits (non-negotiable): (): — see CLAUDE.md Key Conventions Releases: release-please reads commits + opens rolling release PR on merge to main (ADR-021) -Skills: to-prd, grill-with-docs, grill-me, handoff, improve-codebase-architecture (.claude/skills/) +Skills: to-prd, grill-with-docs, grill-me, handoff, improve-codebase-architecture, evaluate-library (.claude/skills/) EOF exit 0 diff --git a/.claude/skills/evaluate-library/EXAMPLES/approved-example.md b/.claude/skills/evaluate-library/EXAMPLES/approved-example.md new file mode 100644 index 0000000..8b376c9 --- /dev/null +++ b/.claude/skills/evaluate-library/EXAMPLES/approved-example.md @@ -0,0 +1,91 @@ +--- +package: clsx +version: "^2.1.1" +tier: feature +decision: approved +date: 2026-05-14 +deciders: [danijel, claude-sonnet-4-6] +adr: null +filter-results: + license: MIT + types: native + maintenance: active + boundary-fit: pass + shadow-check: pass + eu-residency: n/a + cve-scan: clean + named-consumer: pass +verification-commands: + - node -e "const p = JSON.parse(require('fs').readFileSync('./node_modules/clsx/package.json','utf8')); console.log(p.license)" + - ls node_modules/clsx/dist/clsx.d.ts + - npm view clsx time.modified + - pnpm audit --audit-level=moderate +accepted-cves: [] +--- + +## Filter: license + + + +`package.json` declares `"license": "MIT"`. MIT is on the allowlist. Pass. + +## Filter: types + + + +`clsx` ships its own `.d.ts` declarations at `dist/clsx.d.ts`. No `@types/clsx` package needed. +The TypeScript surface covers the full public API (`ClassValue`, overloads). Pass. + +## Filter: maintenance + + + +Last npm release: `2.1.1` published 2024-02-06 (15 months ago at evaluation date — within the 18-month threshold). GitHub shows open PR/issue activity within the last 3 months. The library is small, intentionally stable, and actively maintained. Pass. + +## Filter: boundary-fit + + + +`clsx` is a pure string-concatenation utility. It imports nothing from Node.js or browser globals; it has zero transitive dependencies. Adding it to `packages/navigation` as a `feature`-tagged package introduces no boundary violations under ADR-006 or ADR-010. It does not import `@sentry/*`, `@opentelemetry/*`, or any core-reserved vendor SDK. Pass. + +## Filter: shadow-check + + + +The locked workspace must-haves are: `zod`, `inversify`, `payload`, `@trpc/server`, `superjson`, `reflect-metadata`. None of these perform CSS class-name composition. There is no existing utility in the workspace for this purpose. Pass. + +## Filter: eu-residency + + + +`clsx` is a pure in-process string utility. It performs no network calls, transmits no user data, and has no SaaS endpoint. EU residency filter does not apply. + +## Filter: cve-scan + + + +`pnpm audit --audit-level=moderate` returns 0 vulnerabilities for `clsx@2.1.1` at evaluation time. No accepted advisories. + +## Filter: named-consumer + + + +Named consumer: `packages/navigation/src/ui/components/navigation-menu.tsx` — the `NavigationMenuLink` component must compute conditional class names for the active/inactive link state. Without `clsx` this is implemented as a ternary chain that becomes unreadable past three conditions. The component exists today; this is not a hypothetical future use case. + +Secondary consumer: `packages/navigation/src/ui/components/mobile-nav.tsx` — open-state drawer overlay class computation. Both components are blocked on this adoption. + +## Prompt: replaces + +Replaces inline ternary chains like `` `base-class ${isActive ? 'active' : ''} ${isDisabled ? 'disabled' : ''}` ``. No library is being retired — this is a first-time adoption of a class-composition utility. No parallel adoption risk. + +## Prompt: migration-cost-out + +**Mechanical.** `clsx` is called only at the component leaf level. Removal means replacing `clsx(...)` calls with equivalent template-literal ternaries — a mechanical sed-style refactor bounded to the `packages/navigation/src/ui/` subtree. No data format dependencies, no vendor lock-in, no protocol coupling. + +## Prompt: alternatives-considered + +1. **`classnames`** — functional equivalent, MIT, widely used. Rejected in favour of `clsx` because `clsx` is the successor written by the same author with better TypeScript support and 2× faster benchmarks at comparable bundle size (330 B vs 440 B minzipped). `classnames` would also pass all eight filters; `clsx` is strictly preferable. + +2. **Inline ternary chains (no library)** — the current approach. Adequate for one or two conditions; degrades rapidly past three. The `navigation-menu` component already has four conditional classes; this is the threshold where a utility library pays for itself. Rejected as the status quo. + +3. **`tailwind-merge`** — superset of `clsx` that also de-duplicates conflicting Tailwind classes. Overkill for this use case (navigation components use a small, non-conflicting class set). Higher migration cost out (data-format dependency on Tailwind class semantics). Deferred. diff --git a/.claude/skills/evaluate-library/EXAMPLES/rejected-trpc-to-openapi.md b/.claude/skills/evaluate-library/EXAMPLES/rejected-trpc-to-openapi.md new file mode 100644 index 0000000..54a520d --- /dev/null +++ b/.claude/skills/evaluate-library/EXAMPLES/rejected-trpc-to-openapi.md @@ -0,0 +1,99 @@ +--- +package: trpc-to-openapi +version: "^1.2.0" +tier: core +decision: rejected +date: 2026-05-14 +deciders: [danijel, claude-sonnet-4-6] +adr: null +filter-results: + license: MIT + types: native + maintenance: active + boundary-fit: pass + shadow-check: pass + eu-residency: n/a + cve-scan: clean + named-consumer: fail +verification-commands: + - npm info trpc-to-openapi license + - npm info trpc-to-openapi time.modified + - pnpm audit --audit-level=moderate +accepted-cves: [] +--- + +## Filter: license + + + +`package.json` declares `"license": "MIT"`. On the allowlist. Pass. + +## Filter: types + + + +`trpc-to-openapi` ships TypeScript declarations (`.d.ts`) alongside the compiled output. Full API surface typed. Pass. + +## Filter: maintenance + + + +Last npm release: `1.2.0` published within the past 12 months at evaluation date. GitHub shows active issue triage. Pass. + +## Filter: boundary-fit + + + +`trpc-to-openapi` would land in a `core`-tagged package alongside the tRPC router configuration. Core packages are permitted to hold tRPC-adjacent tooling. The library imports `@trpc/server` (already a workspace must-have) and standard `zod` types. No boundary violations under ADR-006 or ADR-010. Pass. + +## Filter: shadow-check + + + +No existing workspace library performs OpenAPI spec generation from tRPC routers. `trpc-to-openapi` does not duplicate any locked must-have. Pass. + +## Filter: eu-residency + + + +`trpc-to-openapi` is a pure in-process code-generation utility. It produces an OpenAPI JSON spec at build time or request time; it transmits nothing to a vendor endpoint. EU residency filter does not apply. + +## Filter: cve-scan + + + +`pnpm audit --audit-level=moderate` returns 0 vulnerabilities at evaluation time. Pass. + +## Filter: named-consumer + + + +**No named consumer exists.** + +The proposal arose during a 2026-05-14 grill session exploring whether to expose the tRPC router surface as a REST API for external consumers. The session established that **all current callers are TypeScript** and use `createCaller` directly — there are no HTTP REST clients calling the API, and no external consumers are blocked waiting for an OpenAPI spec. + +The hypothetical consumers cited were: + +- "External partners might want a REST API someday" — speculative; no partner is waiting. +- "A mobile client might prefer REST over tRPC-HTTP" — hypothetical; no mobile client exists. +- "OpenAPI docs improve DX for third-party integrations" — no third-party integration is in flight. + +The grill-session question "who calls this code path today, or who is blocked waiting for it?" had the honest answer: nobody. The library would have shipped approximately 30 lines of `.meta({...})` annotations per router and a `superjson`-incompatible HTTP handler configuration in exchange for zero downstream consumers — pure carrying cost. + +This trace exists as a permanent record per ADR-022 §4 so future agents do not re-evaluate `trpc-to-openapi` without first answering whether a named consumer now exists. If a concrete external integration is later planned, re-open this evaluation with the integration as the named consumer, re-run all eight filters, and write a new trace. + +## Prompt: replaces + +Nothing is being retired. The adoption would have been additive alongside the existing `createCaller` usage path. + +## Prompt: migration-cost-out + +**Hard.** Once `.meta({...})` annotations are added to tRPC procedures, they accumulate across routers over time. Removal requires stripping those annotations, deleting the OpenAPI spec generation step, and coordinating with any REST consumers that may have formed since adoption. The `superjson`-incompatible HTTP handler creates a parallel request path that would need to be decommissioned. Hard-rated because of the scattered annotation surface. + +## Prompt: alternatives-considered + +1. **`@anatine/zod-nestjs` + NestJS** — full REST framework alternative; overkill for a tRPC-native repo and would require replacing the tRPC layer entirely. Not a serious alternative for this use case. + +2. **Custom OpenAPI spec, hand-authored** — maintain a `openapi.yaml` alongside the tRPC router. Zero runtime cost; no dependency; the spec is always exactly what consumers need. Viable if a named consumer materialises and the schema surface is stable. The correct path when named-consumer passes. + +3. **No action (status quo)** — current approach: TypeScript callers use `createCaller`; no REST surface exposed. Correct given that no named consumer exists today. This is the chosen outcome. diff --git a/.claude/skills/evaluate-library/POLICY.md b/.claude/skills/evaluate-library/POLICY.md new file mode 100644 index 0000000..2b1db5d --- /dev/null +++ b/.claude/skills/evaluate-library/POLICY.md @@ -0,0 +1,110 @@ +# Library Evaluation Policy — Quick Reference + +> Authoritative source: `docs/decisions/adr-022-library-evaluation-policy.md` +> Authoritative runbook: `.claude/skills/evaluate-library/SKILL.md` + +--- + +## Why this policy exists + +The repo ships with a deliberately narrow runtime surface (six deps per feature package). That discipline is uncodified. Three signals exposed the gap: a near-miss adding `trpc-to-openapi` for hypothetical REST consumers; three ADRs recording library choices _after_ adoption; and no EU-residency gate before a library could silently transmit user data to a US-only SaaS endpoint. ADR-022 codifies the discipline and makes it agent-runnable. + +--- + +## Tier trigger + +The policy applies to **direct runtime dependencies** in feature- and core-tier packages. Devdeps and app-tier deps are exempt. + +| Where the dep lands | Process required | Companion record | +| -------------------------- | ------------------------- | ---------------- | +| `apps/` | Author's call — no policy | — | +| `packages/` | Trace required | — | +| `packages/core-*` | Trace required | ADR required | +| New optional-core category | Trace required | ADR required | + +The trigger maps onto the existing ESLint `boundaries` tag system (ADR-006, ADR-010) — no new mental model. + +--- + +## Eight hard auto-reject filters + +**Phase 1 — cheap (always run to completion)** + +| # | Filter | Auto-reject condition | +| --- | ---------------- | -------------------------------------------------------------------------------------------------------------------------- | +| 1 | **license** | Outside `MIT`, `Apache-2.0`, `BSD-*`, `ISC`, `MPL-2.0` | +| 2 | **types** | No `.d.ts` and no `@types/` | +| 3 | **shadow-check** | Functional parallel to a locked must-have (`zod`, `inversify`, `payload`, `@trpc/server`, `superjson`, `reflect-metadata`) | +| 4 | **boundary-fit** | Dep would violate ESLint boundary rules for the target tier (e.g., `@sentry/node` in a feature package — ADR-017 §4) | + +**Phase 2 — expensive (short-circuit after first reject)** + +| # | Filter | Auto-reject condition | +| --- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| 5 | **maintenance** | Last release ≥ 18 months OR activity gap ≥ 12 months (`abandoned`) | +| 6 | **cve-scan** | Open advisory at `moderate` severity or above (via `pnpm audit`) | +| 7 | **eu-residency** | Library transmits user data/telemetry to a vendor endpoint with no EU data region available or not configured | +| 8 | **named-consumer** | No concrete call site exists today and no feature is blocked waiting for it — hypothetical future use does not qualify | + +A single failure in any filter → `decision: rejected`. Cheap filters always run; expensive filters stop at the first fail. + +--- + +## Three discussion prompts + +Not auto-reject filters — any answer is acceptable with justification. Required in every trace. + +1. **replaces** — What existing approach does this replace? Parallel adoption of the same capability is a smell. +2. **migration-cost-out** — Rate the removal cost 18 months from now: mechanical / hard / impossible. +3. **alternatives-considered** — Two named alternatives minimum. For core-tier, also duplicated into the companion ADR. + +--- + +## Trace artifact + +Every decision — approved or rejected — produces a file at `docs/library-decisions/-.md`. + +**Required frontmatter fields:** + +| Field | Values | +| ------------------------------- | ------------------------------------------ | +| `package` | npm package name | +| `version` | semver range | +| `tier` | `app` \| `feature` \| `core` | +| `decision` | `approved` \| `rejected` | +| `date` | `YYYY-MM-DD` | +| `deciders` | list of authors (human and/or agent) | +| `adr` | `adr-NNN` or `null` | +| `filter-results.license` | SPDX id | +| `filter-results.types` | `native` \| `@types/` \| `none` | +| `filter-results.maintenance` | `active` \| `dormant` \| `abandoned` | +| `filter-results.boundary-fit` | `pass` \| `fail` | +| `filter-results.shadow-check` | `pass` \| `fail` \| `"shadows "` | +| `filter-results.eu-residency` | `ok` \| `n/a` \| `self-hostable` \| `fail` | +| `filter-results.cve-scan` | `clean` \| advisory ID \| `fail` | +| `filter-results.named-consumer` | `pass` \| `fail` | +| `verification-commands` | list of literal commands run | +| `accepted-cves` | list of accepted advisory IDs (optional) | + +Skipped expensive filters (short-circuited by an earlier reject) → write `skip` as the frontmatter value and note "Not evaluated" in the prose section. + +The trace lands in **the same commit** as the `package.json` change. The pre-commit hook validates this for approved traces. + +--- + +## Four-layer enforcement stack + +| Layer | Latency | Catches | +| ----------------------------------------------------------- | ---------- | ----------------------------------------------------------------- | +| Claude `PreToolUse`/`PostToolUse` hook | inline | Agent skipping the skill before `pnpm add` or `package.json` edit | +| `/evaluate-library` skill | seconds | The decision itself + writes the trace | +| Git pre-commit hook (`scripts/library-decisions/check.mjs`) | pre-commit | Humans or agents bypassing the skill | +| Sandcastle reviewer prompt | per-slice | Bypasses that slipped past pre-commit | + +The Claude hook injects a `` pointing to this skill. It is non-blocking — devdep additions and app-tier changes trigger the reminder but do not require a trace. The pre-commit hook is the deterministic gate. + +--- + +## Composition with generators + +`pnpm turbo gen core-package ` emits **pre-shipped traces** — one per direct runtime dep of the new core package — pre-marked `decision: approved` and citing the relevant ADR (ADR-015 for events, ADR-016 for realtime, ADR-018 for audit). No separate evaluation needed for scaffolded optional cores. diff --git a/.claude/skills/evaluate-library/SKILL.md b/.claude/skills/evaluate-library/SKILL.md new file mode 100644 index 0000000..5f123f7 --- /dev/null +++ b/.claude/skills/evaluate-library/SKILL.md @@ -0,0 +1,180 @@ +--- +name: evaluate-library +description: Walk the 8-filter + 3-prompt library evaluation protocol for a named package, write the decision trace to docs/library-decisions/, and return pass/fail. Use when adding a runtime dependency to a feature or core package, or when the library-policy-nudge hook fires. +--- + + + +``` +/evaluate-library --tier --target +``` + +All three arguments are required. The `library-policy-nudge` hook emits this exact invocation. For `app`-tier packages, evaluation still runs but a trace is optional (author's call per ADR-022 §1). + + + + + +## Overview + +Walk eight hard auto-reject filters in **collect-cheap-skip-expensive** order, then answer three discussion prompts. Write the trace unconditionally at the end — including for rejections. A rejection trace is a permanent record that prevents future agents from re-litigating the same decision. + +## Phase 1 — Cheap filters (always run to completion, even if one fails) + +Run all four cheap filters regardless of their outcomes. Record each result before moving to Phase 2. + +### Filter 1: license + +Command: `node -e "const p = JSON.parse(require('fs').readFileSync('./node_modules//package.json','utf8')); console.log(p.license)"` + +Allowlist: `MIT`, `Apache-2.0`, `BSD-2-Clause`, `BSD-3-Clause`, `ISC`, `MPL-2.0`. + +Result values: the SPDX identifier (e.g. `MIT`) if allowed, or ` (rejected)` if outside the allowlist. Anything outside the allowlist is an automatic reject but does not stop Phase 1. + +### Filter 2: types + +Check whether TypeScript types ship with the package or via `@types/`: + +``` +ls node_modules//index.d.ts 2>/dev/null && echo native || npm info @types/ version 2>/dev/null | head -1 +``` + +Result values: `native` (ships its own `.d.ts`), `@types/` (community types available), or `none` (auto-reject — un-typed library shifts maintenance cost to the feature). + +### Filter 3: shadow-check + +Check whether this library duplicates a must-have already locked in the workspace. Locked must-haves: `zod` (validation), `inversify` (DI, ADR-002), `payload` (CMS), `@trpc/server` (API layer), `superjson` (serialisation), `reflect-metadata` (DI metadata). + +Command: `cat package.json | grep -E '"(zod|inversify|payload|@trpc/server|superjson|reflect-metadata)"'` — run from the workspace root. + +Result values: `pass` (no shadow), `fail` (exact duplicate of a locked dep), `"shadows "` (functional parallel that would create two libraries doing the same job — auto-reject). A replacement must be a separate ADR with consequences analysis, not a parallel adoption. + +### Filter 4: boundary-fit + +Confirm the dependency does not violate ESLint boundary-tag rules for the target tier (ADR-006, ADR-010, ADR-017). + +Key rules: + +- Feature packages cannot import `@sentry/*` or `@opentelemetry/sdk-*` directly — those are reserved for core (ADR-017 §4). +- No package may import across feature boundaries without going through the event bus or tRPC. +- Optional core packages can only be imported by apps and `core-composition`-tagged packages. + +Check by reviewing what the proposed library's transitive imports would bring in and whether any violate the boundary ruleset. + +Result values: `pass` or `fail`. + +--- + +After Phase 1: tally results. If **any cheap filter failed**, the overall decision is `rejected`. Proceed to Phase 2 anyway — all expensive filters still run if the Phase 1 decision is already rejected (they inform the full record). If all cheap filters passed, proceed to Phase 2 to determine the final decision. + +## Phase 2 — Expensive filters (short-circuit after first reject) + +Run in order. On the first failure, set remaining filter results to `skip` and skip to the [Trace write step](#trace-write-step). + +### Filter 5: maintenance + +Check last release date and recent PR/issue activity: + +``` +npm info time.modified +npm info time | tail -5 +``` + +Result values: + +- `active` — last release < 18 months **and** PR/issue activity < 12 months +- `dormant` — stable, not actively developed (acceptable for finished libraries like `reflect-metadata`) +- `abandoned` — last release ≥ 18 months **or** no activity in ≥ 12 months → auto-reject; short-circuit remaining expensive filters + +On `abandoned` → set `cve-scan`, `eu-residency`, `named-consumer` to `skip` → write trace. + +### Filter 6: cve-scan + +``` +pnpm audit --audit-level=moderate 2>&1 | head -40 +``` + +Result values: `clean` (no advisories), an advisory ID like `GHSA-xxxx-xxxx-xxxx` (accepted risk — document in `accepted-cves` frontmatter), or `fail` (open advisory not accepted → auto-reject; short-circuit remaining expensive filters). + +On `fail` → set `eu-residency`, `named-consumer` to `skip` → write trace. + +### Filter 7: eu-residency + +Applies only if the library transmits user data, telemetry, business state, or secrets to a vendor-controlled endpoint by default. Examples: analytics SDKs, error-tracking clients, AI APIs, log aggregation services. + +Exemptions (result: `n/a`): pure in-process libraries (no network calls), self-hostable software where the operator controls the endpoint, and build-time-only tools. + +For non-exempt libraries: verify the vendor offers an EU data region AND that the integration in `target` is configured to use it. + +Result values: `ok` (vendor offers EU region, integration configured), `n/a` (no data transmission), `self-hostable` (operator-controlled endpoint), `fail` → auto-reject; short-circuit `named-consumer`. + +On `fail` → set `named-consumer` to `skip` → write trace. + +### Filter 8: named-consumer + +Answer: **Who calls this code path today, or who is blocked waiting for it?** + +A named consumer is a concrete call site that exists now or a feature blocked on this capability today. "We might want this later", "external clients could use this", and "it would be nice to have" are not named consumers. + +If the only possible callers are hypothetical or future → `fail` → auto-reject. + +Result value: `pass` or `fail`. + +--- + +## Skip sentinel + +When a filter is short-circuited (not evaluated), write `skip` for its frontmatter value. The Zod schema validates approved traces end-to-end; rejected/partial traces may carry `skip` in fields that would normally require an enum value. The pre-commit check only validates that approved traces exist for new deps — partial traces are informational records. + +## Three discussion prompts + +Answer all three in the trace, regardless of filter outcome. These are not auto-reject filters; any answer is acceptable with justification. + +### Prompt: replaces + +What existing library or approach does this replace? New-and-old running in parallel is a smell — name the thing being retired and the retirement plan, or explain why parallel adoption is intentional and time-bounded. + +### Prompt: migration-cost-out + +What does ripping this back out look like 18 months from now? Rate: **mechanical** (swap one package, update call sites), **hard** (scattered integration points, data-format dependencies), or **impossible** (vendor lock-in, protocol coupling). Higher cost raises the bar for adoption. + +### Prompt: alternatives-considered + +Name at least two alternatives evaluated before choosing this library. For `core`-tier adoptions, this section is also duplicated into the companion ADR. If no alternatives exist, explain why (e.g., the library is the de-facto standard with no viable substitutes). + +--- + +## Trace write step + +Write the trace **unconditionally** at evaluation end — even for rejections, even for partial traces. + +**Path:** `docs/library-decisions/-.md` + +Use today's date. Use the `TRACE-TEMPLATE.md` in this directory as the structural guide. + +Frontmatter rules: + +- `decision: approved` only if all eight filters passed. Otherwise `decision: rejected`. +- `adr: null` for feature-tier. For core-tier approvals, coordinate the ADR slug before writing (`adr: adr-NNN`). +- `verification-commands` — include the literal commands run for each filter, one per line. +- `accepted-cves: []` (empty unless you accepted a specific advisory). +- For skipped expensive filters, write `skip` for the frontmatter value and omit the prose section body or note "Not evaluated — skipped due to earlier rejection." + +After writing the trace: + +- For approved traces: confirm the trace is staged in the same commit as the `package.json` change. The pre-commit hook validates this. +- For rejected traces: stage the trace file alone. Do not run `pnpm add `. + + + + + +After completing the evaluation, emit a one-paragraph summary: + +``` +/evaluate-library result: @ () +Rejection filters (if any): +Trace written to: docs/library-decisions/-.md +``` + + diff --git a/.claude/skills/evaluate-library/TRACE-TEMPLATE.md b/.claude/skills/evaluate-library/TRACE-TEMPLATE.md new file mode 100644 index 0000000..4037081 --- /dev/null +++ b/.claude/skills/evaluate-library/TRACE-TEMPLATE.md @@ -0,0 +1,165 @@ +# Trace Template + +Use this file as the structural guide when writing a library decision trace. Copy the frontmatter block and all 11 headings. Replace placeholder values with real results. + +Trace path: `docs/library-decisions/-.md` + +--- + +## Frontmatter — all filters evaluated + +```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: + - + - + - + - +accepted-cves: [] +--- +``` + +## Frontmatter — partial trace (expensive filters short-circuited) + +When an expensive filter fails (Phase 2 short-circuit), set remaining filter fields to `skip`. The Zod schema validates approved traces end-to-end; `skip` is the accepted sentinel for unevaluated fields in rejected traces. + +Example: `maintenance: abandoned` → `cve-scan`, `eu-residency`, `named-consumer` skipped. + +```markdown +--- +package: +version: "" +tier: feature | core +decision: rejected +date: +deciders: [, ...] +adr: null +filter-results: + license: MIT + types: native + maintenance: abandoned + boundary-fit: pass + shadow-check: pass + eu-residency: skip + cve-scan: skip + named-consumer: skip +verification-commands: + - npm view time.modified +accepted-cves: [] +--- +``` + +--- + +## Required headings (11 total, in this order) + +### Filter sections (8) + +```markdown +## Filter: license + + + +Record the SPDX identifier from `package.json` or `npx license-checker --packages `. +Allowed: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, MPL-2.0. +Anything else → auto-reject (note the identifier and rejection reason). + +## Filter: types + + + +Confirm TypeScript types are available. `native` = ships its own `.d.ts`; `@types/` = community +types package exists and is current; `none` = no types → auto-reject. + +## Filter: maintenance + + + +Check last release date and recent PR/issue activity. `active` = last release < 18 months AND +activity < 12 months. `dormant` = stable but not actively developed (acceptable for finished +libraries). `abandoned` = auto-reject. +If skipped (earlier expensive filter failed), write: "Not evaluated — skipped due to rejection." + +## Filter: boundary-fit + + + +Confirm the dependency does not violate ESLint boundary-tag rules for the target tier +(ADR-006, ADR-010, ADR-017). Name the specific rule checked and the result. + +## Filter: shadow-check + + + +Check whether this library duplicates a must-have already locked in the workspace. +Locked must-haves: zod, inversify, payload, @trpc/server, superjson, reflect-metadata. +`shadows ` → auto-reject; a replacement requires a dedicated ADR. + +## Filter: eu-residency + + + +If the library transmits user data, telemetry, or business state to a vendor-controlled +endpoint by default, the vendor must offer an EU data region and the integration must be +configured to use it. Pure in-process libraries and build-time tools → `n/a`. +If skipped, write: "Not evaluated — skipped due to rejection." + +## Filter: cve-scan + + + +Run `pnpm audit --audit-level=moderate`. `clean` = no advisories at adoption time. Record +accepted advisory IDs in the `accepted-cves` frontmatter field and explain the risk acceptance +here. If skipped, write: "Not evaluated — skipped due to rejection." + +## Filter: named-consumer + + + +Answer: "Who calls this code path today, or who is blocked waiting for it?" +Hypothetical future callers are not consumers (ADR-022 §2.8 — the direct response to the +2026-05-14 OpenAPI near-miss). If skipped, write: "Not evaluated — skipped due to rejection." +``` + +### Prompt sections (3) + +```markdown +## Prompt: replaces + + + +What existing library or approach does this replace? New-and-old running in parallel is a smell. +Name the thing being retired and its retirement plan, or explain why parallel adoption is +intentional and time-bounded. + +## Prompt: migration-cost-out + + + +What does ripping this back out look like 18 months from now? Rate: mechanical (swap package, +update call sites), hard (scattered integration, data-format dependencies), or impossible +(vendor lock-in, protocol coupling). Higher migration cost raises the adoption bar. + +## Prompt: alternatives-considered + + + +Name at least two alternatives evaluated before choosing this library. For core-tier adoptions, +this section is also duplicated into the companion ADR. If no alternatives exist, explain why. +```