Files
agentic-dev-template/.claude/skills/evaluate-library/SKILL.md
Danijel Martinek 98d96d2e19 docs(tooling): add sub-processor discriminated union to ADR-022 and traces
Amends ADR-022 §9 with the `is-sub-processor` / `processes-pii` discriminated
union spec, including the five conditional fields required when a library is a
true GDPR sub-processor. Updates the evaluate-library skill to prompt for these
fields during every trace authoring pass and adds the updated frontmatter
template. Backfills all nine existing library-decision traces with the new
fields; payload gets `processes-pii: true` (self-hosted CMS that stores user
data); all pure in-process libraries get `false / false`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:44:09 +00:00

14 KiB

name, description
name description
evaluate-library Walk the 9-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 <package-name> --tier <feature|core|app> --target <package-path>

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 nine 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/<pkg>/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 <SPDX-id> (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/<pkg>:

ls node_modules/<pkg>/index.d.ts 2>/dev/null && echo native || npm info @types/<pkg> version 2>/dev/null | head -1

Result values: native (ships its own .d.ts), @types/<pkg> (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 <x>" (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.

Filter 5: maintenance

Check last release date and recent PR/issue activity:

npm info <pkg> time.modified
npm info <pkg> 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, socketRisk 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, socketRisk 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, socketRisk 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 → set socketRisk to skip → auto-reject.

Result value: pass or fail.

Filter 9: supply-chain behavior (Socket)

Expensive — network call. Run last in Phase 2. Short-circuit: if any earlier Phase 2 filter already rejected the library, set socketRisk to skip and proceed to the Trace write step.

Verify the package's supply-chain health via socket-cli:

npx socket-cli@latest scan . --json 2>&1

This scans the current directory's lockfile for packages installed from the target under evaluation. For a targeted single-package check before installing:

npx socket-cli@latest info <pkg>@<version> --json 2>&1

The JSON output contains an array of findings, each with a severity field. Cross-reference with the repo-root .socket.json issueRules to determine the classification:

Finding severity .socket.json rule socketRisk value
No findings, or only medium/low ignore clean
high-severity finding present warn flagged
critical-severity finding present error <finding-summary>

Where <finding-summary> is a concise label for the critical finding (e.g. "new-author-on-publish", "install-scripts-added", "exfiltrates-env").

Set filter-results.socketRisk in the trace frontmatter to one of these three values.

Result values:

  • clean — no meaningful supply-chain signals; proceed to Phase 3 prompts.
  • flaggedhigh-severity finding; document the specific signal in the trace body and decide whether to accept with justification. Not an auto-reject.
  • <finding-summary>critical-severity finding; auto-reject. This is the last filter — no further filters to skip.

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).


Sub-processor classification

Answer these two questions before writing the trace. The answers become top-level frontmatter fields required by ADR-022 §9.

Question: is-sub-processor

Does the vendor receive personal data on the operator's behalf?

A library is a sub-processor when it transmits personal data (user identifiers, email addresses, behavioural events, request bodies, etc.) to a vendor-controlled endpoint — analytics SDKs, error-tracking clients, AI APIs, log aggregation services. Network calls alone do not make a library a sub-processor; only calls that carry personal data do.

Pure in-process libraries (no network calls), self-hostable software where the operator controls the endpoint, and build-time-only tools are not sub-processors.

Set is-sub-processor: true | false.

Question: processes-pii

Does the library process personal data in-process, even without transmitting it to a vendor?

A library processes PII if it reads, validates, serialises, stores, or transforms data fields that may contain personal information (names, emails, IDs, content authored by users, authentication credentials). A self-hosted database or CMS is a prime example: no data leaves to a vendor, yet the library clearly handles PII.

Pure utility libraries (DI containers, type validators, serialisers operating on already-typed objects without inspecting field semantics, test runners) typically answer false.

Set processes-pii: true | false.

Conditional block: when is-sub-processor is true

When is-sub-processor: true, five additional fields are required in the trace frontmatter. Gather them before writing the trace:

data-sent: "<what personal data the library transmits to the vendor>"
region: "<vendor data region, e.g. eu-west-1 or eu>"
dpa-signed: true | false     # has the operator signed a DPA with this vendor?
sccs-required: true | false  # does the vendor require SCCs (non-EEA transfer)?
contact: "<vendor DPO or privacy contact email/URL>"

If the vendor does not yet have a signed DPA or if you cannot determine the region, record dpa-signed: false / region as best-known and add a prose note under ## Sub-processor in the trace body explaining the gap.


Trace write step

Write the trace unconditionally at evaluation end — even for rejections, even for partial traces.

Path: docs/library-decisions/<YYYY-MM-DD>-<package-name>.md

Use today's date. Use docs/library-decisions/_template.md 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).
  • is-sub-processor and processes-pii are always required (see Sub-processor classification above).
  • When is-sub-processor: true, include data-sent, region, dpa-signed, sccs-required, and contact.
  • 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."

Frontmatter template:

---
package: <name>
version: "<semver range>"
tier: app | feature | core
decision: approved | rejected
date: <YYYY-MM-DD>
deciders: [<author>, ...]
adr: adr-NNN | null
lastRevalidated: null
is-sub-processor: false
processes-pii: false
# include the block below only when is-sub-processor: true
# data-sent: "<description>"
# region: "<eu | eu-west-1 | ...>"
# dpa-signed: false
# sccs-required: false
# contact: "<url or email>"
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
  socketRisk: clean | flagged | <arbitrary-string>
verification-commands:
  - <literal command that produced each filter result>
accepted-cves: []
---

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 <pkg>.

After completing the evaluation, emit a one-paragraph summary:

/evaluate-library result: <approved|rejected> — <package>@<version> (<tier>)
Rejection filters (if any): <filter names>
Trace written to: docs/library-decisions/<date>-<package>.md