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>
This commit is contained in:
2026-05-18 18:44:09 +00:00
parent 8486037f20
commit 98d96d2e19
12 changed files with 176 additions and 1 deletions

View File

@@ -178,13 +178,69 @@ Name at least two alternatives evaluated before choosing this library. For `core
---
## 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 the `TRACE-TEMPLATE.md` in this directory as the structural guide.
Use today's date. Use `docs/library-decisions/_template.md` as the structural guide.
Frontmatter rules:
@@ -192,8 +248,46 @@ Frontmatter rules:
- `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:
```yaml
---
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.

View File

@@ -196,6 +196,52 @@ Every existing runtime dependency in feature- and core-tier packages
ADR-002, ADR-014, ADR-017 are cited via the `adr:` frontmatter field;
verification-command output is captured at backfill time.
### 9. Sub-processor discriminated union (amendment: 2026-05-18)
Every trace carries two top-level frontmatter fields classifying the library
from a GDPR sub-processor perspective:
```yaml
is-sub-processor: false # boolean — true when the vendor receives personal data on the operator's behalf
processes-pii: false # boolean — true when the library processes PII in-process (even without transmitting it)
```
When `is-sub-processor: true`, five additional fields are **required**:
```yaml
data-sent: "<what personal data the library transmits to the vendor>"
region: "<vendor data region, e.g. eu-west-1>"
dpa-signed: true | false
sccs-required: true | false
contact: "<vendor DPO or privacy contact email/URL>"
```
**Discriminated-union rules:**
| `is-sub-processor` | `processes-pii` | Conditional fields required? |
| ------------------ | --------------- | --------------------------------------------------------------------------------- |
| `false` | `false` | No pure library, no data involvement |
| `false` | `true` | No in-process only, no vendor data flow |
| `true` | `true` | Yes all five conditional fields required |
| `true` | `false` | Technically possible but very unusual; still requires all five conditional fields |
**Baseline for backfill:** pure in-process libraries (no network calls to
vendor-controlled endpoints) get `is-sub-processor: false` + `processes-pii: false`.
Self-hosted software that stores PII but transmits nothing to the vendor (e.g.
`payload`) gets `is-sub-processor: false` + `processes-pii: true`.
These fields are the machine surface consumed by `scripts/emit-sub-processors.mjs`
(see Story 06 of the compliance-manifests-pii-retention-subprocessors epic). A
trace missing `is-sub-processor` is treated as `false` by the generator for
backward-compatibility; all new traces authored after this amendment must include
both fields. The `evaluate-library` skill 7) prompts for these fields
unconditionally and writes the conditional block only when `is-sub-processor: true`.
The weekly `dpa-signed` staleness check in CI (ADR-023 cross-reference) should
flag any `dpa-signed: true` traces where the DPA has not been revalidated within
the prior 365 days. Implementation of that cron is deferred to the CI security
hardening work.
## Alternatives considered
- **No policy, keep relying on instinct.** Rejected. The 2026-05-14 OpenAPI

View File

@@ -6,6 +6,9 @@ decision: approved
date: 2026-05-14
deciders: [Danijel Martinek]
adr: null
lastRevalidated: null
is-sub-processor: false
processes-pii: false
filter-results:
license: MIT
types: native

View File

@@ -6,6 +6,9 @@ decision: approved
date: 2026-05-14
deciders: [Danijel Martinek]
adr: adr-002
lastRevalidated: null
is-sub-processor: false
processes-pii: false
filter-results:
license: MIT
types: native

View File

@@ -6,6 +6,9 @@ decision: approved
date: 2026-05-14
deciders: [Danijel Martinek]
adr: null
lastRevalidated: null
is-sub-processor: false
processes-pii: true
filter-results:
license: MIT
types: native

View File

@@ -6,6 +6,9 @@ decision: approved
date: 2026-05-14
deciders: [Danijel Martinek]
adr: null
lastRevalidated: null
is-sub-processor: false
processes-pii: false
filter-results:
license: MIT
types: native

View File

@@ -6,6 +6,9 @@ decision: approved
date: 2026-05-14
deciders: [Danijel Martinek]
adr: null
lastRevalidated: null
is-sub-processor: false
processes-pii: false
filter-results:
license: MIT
types: native

View File

@@ -6,6 +6,9 @@ decision: approved
date: 2026-05-14
deciders: [Danijel Martinek]
adr: adr-002
lastRevalidated: null
is-sub-processor: false
processes-pii: false
filter-results:
license: Apache-2.0
types: native

View File

@@ -6,6 +6,9 @@ decision: approved
date: 2026-05-14
deciders: [Danijel Martinek]
adr: null
lastRevalidated: null
is-sub-processor: false
processes-pii: false
filter-results:
license: MIT
types: native

View File

@@ -6,6 +6,9 @@ decision: approved
date: 2026-05-14
deciders: [Danijel Martinek]
adr: null
lastRevalidated: null
is-sub-processor: false
processes-pii: false
filter-results:
license: MIT
types: native

View File

@@ -6,6 +6,9 @@ decision: approved
date: 2026-05-14
deciders: [Danijel Martinek]
adr: null
lastRevalidated: null
is-sub-processor: false
processes-pii: false
filter-results:
license: MIT
types: native

View File

@@ -7,6 +7,14 @@ 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