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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user