chore(work): decompose dsr-consent-and-cookie-banner epic

Generated 1 epic + 11 stories under docs/work/epics/ from the approved
PRD docs/work/prds/dsr-consent-and-cookie-banner.prd.md. Stories cover
subject-linkage types, ADR-018 audit enum amendment, core-consent
foundation + impl + react, core-dsr (4 interfaces), core-api router
composition, core-ui scaffold + CookieConsentBanner, auth signup
migration, and documentation. Ready for `pnpm work dispatch --execute`.
This commit is contained in:
2026-05-19 11:55:05 +02:00
parent ae886a4499
commit 5abf7fe623
13 changed files with 806 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
{
"updated_at": "2026-05-19T09:41:27.367Z",
"updated_at": "2026-05-19T09:55:07.388Z",
"epics": {
"binder-wrap-helper": {
"status": "done",
@@ -233,6 +233,138 @@
}
}
},
"dsr-consent-and-cookie-banner": {
"status": "in-progress",
"title": "DSR + consent abstraction + cookie consent banner — Epic B of ADR-025",
"prd": "docs/work/prds/dsr-consent-and-cookie-banner.prd.md",
"stories": {
"01-subject-linkage-types": {
"status": "in-progress",
"title": "Subject-linkage types in core-shared",
"ac_total": 1,
"ac_completed": 0,
"depends_on": [],
"blocks": [
"02-audit-enum-amendment"
]
},
"02-audit-enum-amendment": {
"status": "todo",
"title": "Audit action enum amendment (ADR-018)",
"ac_total": 1,
"ac_completed": 0,
"depends_on": [
"01-subject-linkage-types"
],
"blocks": [
"03-core-consent-foundation",
"06-core-dsr"
]
},
"03-core-consent-foundation": {
"status": "todo",
"title": "core-consent foundation — types, brand, withConsent wrapper, conformance extension, ESLint rule",
"ac_total": 2,
"ac_completed": 0,
"depends_on": [
"02-audit-enum-amendment"
],
"blocks": [
"04-core-consent-implementation"
]
},
"04-core-consent-implementation": {
"status": "todo",
"title": "core-consent implementation — Payload impl, DI binders, migration helpers, tRPC router",
"ac_total": 3,
"ac_completed": 0,
"depends_on": [
"03-core-consent-foundation"
],
"blocks": []
},
"05-core-consent-react": {
"status": "todo",
"title": "core-consent React subpath — ConsentProvider + useConsent()",
"ac_total": 1,
"ac_completed": 0,
"depends_on": [
"04-core-consent-implementation"
],
"blocks": [
"09-cookie-consent-banner"
]
},
"06-core-dsr": {
"status": "todo",
"title": "core-dsr — scaffold, interfaces, Payload impls, recording doubles, handlers, dsrRouter",
"ac_total": 3,
"ac_completed": 0,
"depends_on": [
"02-audit-enum-amendment"
],
"blocks": [
"07-core-api-router-composition"
]
},
"07-core-api-router-composition": {
"status": "todo",
"title": "core-api router composition — dsrRouter + consentRouter into appRouter",
"ac_total": 1,
"ac_completed": 0,
"depends_on": [
"04-core-consent-implementation",
"06-core-dsr"
],
"blocks": [
"11-documentation"
]
},
"08-core-ui-scaffold": {
"status": "todo",
"title": "core-ui scaffold",
"ac_total": 1,
"ac_completed": 0,
"depends_on": [],
"blocks": [
"09-cookie-consent-banner"
]
},
"09-cookie-consent-banner": {
"status": "todo",
"title": "CookieConsentBanner component in core-ui",
"ac_total": 1,
"ac_completed": 0,
"depends_on": [
"05-core-consent-react",
"08-core-ui-scaffold"
],
"blocks": [
"11-documentation"
]
},
"10-auth-signup-migration": {
"status": "todo",
"title": "auth signUp anonymous consent migration",
"ac_total": 1,
"ac_completed": 0,
"depends_on": [
"04-core-consent-implementation"
],
"blocks": [
"11-documentation"
]
},
"11-documentation": {
"status": "todo",
"title": "Documentation — DSR guide, consent guide, glossary, CLAUDE.md",
"ac_total": 1,
"ac_completed": 0,
"depends_on": [],
"blocks": []
}
}
},
"library-evaluation-policy": {
"status": "done",
"title": "Library evaluation policy — skill, traces, enforcement stack",
@@ -436,7 +568,90 @@
}
}
},
"ready": [],
"blocked": [],
"ready": [
{
"epic": "dsr-consent-and-cookie-banner",
"story": "01-subject-linkage-types",
"title": "Subject-linkage types in core-shared"
},
{
"epic": "dsr-consent-and-cookie-banner",
"story": "08-core-ui-scaffold",
"title": "core-ui scaffold"
},
{
"epic": "dsr-consent-and-cookie-banner",
"story": "11-documentation",
"title": "Documentation — DSR guide, consent guide, glossary, CLAUDE.md"
}
],
"blocked": [
{
"epic": "dsr-consent-and-cookie-banner",
"story": "02-audit-enum-amendment",
"title": "Audit action enum amendment (ADR-018)",
"waiting_on": [
"dsr-consent-and-cookie-banner/01-subject-linkage-types"
]
},
{
"epic": "dsr-consent-and-cookie-banner",
"story": "03-core-consent-foundation",
"title": "core-consent foundation — types, brand, withConsent wrapper, conformance extension, ESLint rule",
"waiting_on": [
"dsr-consent-and-cookie-banner/02-audit-enum-amendment"
]
},
{
"epic": "dsr-consent-and-cookie-banner",
"story": "04-core-consent-implementation",
"title": "core-consent implementation — Payload impl, DI binders, migration helpers, tRPC router",
"waiting_on": [
"dsr-consent-and-cookie-banner/03-core-consent-foundation"
]
},
{
"epic": "dsr-consent-and-cookie-banner",
"story": "05-core-consent-react",
"title": "core-consent React subpath — ConsentProvider + useConsent()",
"waiting_on": [
"dsr-consent-and-cookie-banner/04-core-consent-implementation"
]
},
{
"epic": "dsr-consent-and-cookie-banner",
"story": "06-core-dsr",
"title": "core-dsr — scaffold, interfaces, Payload impls, recording doubles, handlers, dsrRouter",
"waiting_on": [
"dsr-consent-and-cookie-banner/02-audit-enum-amendment"
]
},
{
"epic": "dsr-consent-and-cookie-banner",
"story": "07-core-api-router-composition",
"title": "core-api router composition — dsrRouter + consentRouter into appRouter",
"waiting_on": [
"dsr-consent-and-cookie-banner/04-core-consent-implementation",
"dsr-consent-and-cookie-banner/06-core-dsr"
]
},
{
"epic": "dsr-consent-and-cookie-banner",
"story": "09-cookie-consent-banner",
"title": "CookieConsentBanner component in core-ui",
"waiting_on": [
"dsr-consent-and-cookie-banner/05-core-consent-react",
"dsr-consent-and-cookie-banner/08-core-ui-scaffold"
]
},
{
"epic": "dsr-consent-and-cookie-banner",
"story": "10-auth-signup-migration",
"title": "auth signUp anonymous consent migration",
"waiting_on": [
"dsr-consent-and-cookie-banner/04-core-consent-implementation"
]
}
],
"needs_prd_ship": []
}

View File

@@ -0,0 +1,46 @@
---
id: 01-subject-linkage-types
epic: dsr-consent-and-cookie-banner
title: Subject-linkage types in core-shared
type: technical-story
status: in-progress
feature: core-shared
depends-on: []
blocks: [02-audit-enum-amendment]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Add the subject-linkage type definitions to `@repo/core-shared` and apply the first instance of the `custom.subject` declaration to the `auth.users` collection, establishing the pattern every downstream consumer will follow.
## Why
The DSR cascade (Epic B's `core-dsr`) walks `custom.subject` fields to discover which rows belong to a given subject. Before any DSR or consent implementation can begin, the TypeScript types and Payload ambient declaration must exist in `core-shared` (must-have package) so all other packages can reference them without an optional-core dependency.
## Done when
- `packages/core-shared/src/payload/subject-linkage-types.ts` exports `SubjectLinkKind`, `SubjectLink`, and `CollectionSubject`.
- An ambient module declaration extends Payload's `CollectionConfig.custom?` with `subject?: CollectionSubject | CollectionSubject[]`.
- `PAYLOAD_AUTH_PII_DEFAULTS` (Epic A's auth-managed exclusions list) gains `processingRestrictedAt` and `consentState` as excluded fields.
- `packages/auth/` sets `custom.subject = { kind: "self", field: "id" }` on the `users` collection config (canonical usage example).
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
## In scope
- `packages/core-shared/src/payload/subject-linkage-types.ts``SubjectLinkKind` (`"self" | "owner" | "reference"`), `SubjectLink` (field, kind, optional target + role), `CollectionSubject` (single or array form).
- Ambient declaration extending `CollectionConfig.custom?` in `core-shared` (parallel to `custom.pii` from Epic A).
- `PAYLOAD_AUTH_PII_DEFAULTS` extension with `processingRestrictedAt` and `consentState`.
- Export of new types from the `core-shared` payload barrel.
- `auth.users` collection: explicit `custom.subject = { kind: "self", field: "id" }` declaration for documentation clarity.
## Out of scope
- Payload impl walking `custom.subject` (Story 06 — core-dsr).
- All other existing collections (`blog.articles`, `media.media`, etc.) — no subject linkage needed per Epic A's PII backfill.
- `docs/compliance/subject-linkage.example.md` — Story 11.
## Tasks
- [ ] Add `SubjectLink`, `SubjectLinkKind`, `CollectionSubject` types in `packages/core-shared/src/payload/subject-linkage-types.ts` + ambient declaration extending Payload `CollectionConfig.custom?` with `subject?: CollectionSubject | CollectionSubject[]` + extend `PAYLOAD_AUTH_PII_DEFAULTS` with `processingRestrictedAt` and `consentState` as excluded fields + export from `core-shared` payload barrel + add `custom.subject = { kind: "self", field: "id" }` to `packages/auth/`'s `users` collection config; all gates pass.

View File

@@ -0,0 +1,44 @@
---
id: 02-audit-enum-amendment
epic: dsr-consent-and-cookie-banner
title: Audit action enum amendment (ADR-018)
type: technical-story
status: todo
feature: core-shared
depends-on: [01-subject-linkage-types]
blocks: [03-core-consent-foundation, 06-core-dsr]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Extend the audit action enum with four new action types needed by consent and DSR, and amend ADR-018 to document the addition.
## Why
`core-consent`'s `IConsent.grant` / `IConsent.withdraw` emit `CONSENT_GRANT` / `CONSENT_WITHDRAW` for Art. 7 legal proof. `core-dsr`'s `IProcessingRestriction` emits `RESTRICT` / `UNRESTRICT` for Art. 18. Both optional cores must emit via `core-audit`'s existing channel; the action types must exist in `core-shared`'s enum before either optional core can be implemented.
## Done when
- The audit action enum in `core-shared/audit/` gains `CONSENT_GRANT`, `CONSENT_WITHDRAW`, `RESTRICT`, `UNRESTRICT`.
- `core-audit`'s `IAuditLog.record` accepts the new action types without type errors.
- `docs/guides/audit-and-compliance.md` is updated to list the new action types.
- `docs/decisions/ADR-018.md` gains an `## Amendments` section recording the date and the reason (consent + restriction events added for Epic B).
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
## In scope
- Audit action enum extension (four new values) in `core-shared/audit/`.
- `core-audit` type update so `IAuditLog.record` is compatible with the new values (no new interface methods).
- `docs/guides/audit-and-compliance.md` update — "Six action types" wording amended to reflect the new count.
- ADR-018 amendment section.
## Out of scope
- New audit emission call sites (Stories 04 and 06 — they live in the optional-core implementations).
- `eraseSubject` flow changes — existing post-DSR-delete pseudonymization is unchanged.
## Tasks
- [ ] Add `CONSENT_GRANT`, `CONSENT_WITHDRAW`, `RESTRICT`, `UNRESTRICT` to the audit action enum in `packages/core-shared/src/audit/` + update `core-audit`'s `IAuditLog` type to accept the new values + update `docs/guides/audit-and-compliance.md` with the new action types + add `## Amendments` section to `docs/decisions/ADR-018.md` recording the date and reason; all gates pass.

View File

@@ -0,0 +1,57 @@
---
id: 03-core-consent-foundation
epic: dsr-consent-and-cookie-banner
title: core-consent foundation — types, brand, withConsent wrapper, conformance extension, ESLint rule
type: technical-story
status: todo
feature: core-consent
depends-on: [02-audit-enum-amendment]
blocks: [04-core-consent-implementation]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Scaffold `@repo/core-consent`, define the public type surface (`ConsentCategory`, `IConsent`), establish the `ConsentChecked` brand + `withConsent` wrapper, wire the `requiresConsent` manifest field into `assertFeatureConformance`, and add the `no-undeclared-consent-check` ESLint rule — the full conformance-layer foundation that all subsequent consent implementation and use-case integration depends on.
## Why
Manifest-first ordering requires that the consent type surface and structural enforcement land before any runtime implementation. The `ConsentChecked` brand must exist in `core-shared/conformance/brands.ts` before `assertFeatureConformance` can check it; the `requiresConsent` manifest field must be schema-valid before any feature can declare it; the ESLint rule must exist before use cases can be lint-gated. Landing both tasks in this story keeps the conformance layer coherent before any optional-core impl lands.
## Done when
- `pnpm turbo gen core-package consent` produces a green package shell.
- `core-consent/consent-types.ts` exports `ConsentCategory`, `ConsentState`, `UserConsentState`.
- `core-consent/consent.interface.ts` exports `IConsent`.
- `core-shared/conformance/brands.ts` exports `ConsentChecked` brand + `isConsentChecked` helper.
- `core-consent/with-consent.ts` exports `withConsent` wrapper attaching `ConsentChecked` brand at bind time; unit tests assert brand is attached and factory passthrough is preserved.
- Feature manifest schema gains `requiresConsent: ConsentCategory[]` (default `[]`); existing features declare it as empty array without errors.
- `assertFeatureConformance` boot check requires `ConsentChecked` brand when `requiresConsent.length > 0`; synthetic fixture test asserts failure when brand is absent.
- `core-eslint/rules/no-undeclared-consent-check.js` is registered at warn severity; `_manifest-ast.js` extracts `requiresConsent` field; RuleTester fixtures cover: matching call (pass), undeclared category in call site (warn), unused manifest declaration (warn), non-use-case file (no-op).
- Conformance ESLint rule count in CLAUDE.md and `docs/guides/conformance-quickref.md` advances from 11 → 12.
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass after each task.
## In scope
- `pnpm turbo gen core-package consent` scaffold.
- `ConsentCategory` (string-literal union with escape hatch), `ConsentState`, `UserConsentState` types.
- `IConsent` interface (`isGranted`, `grant`, `withdraw`, `getCategories`).
- `ConsentChecked` brand in `core-shared/conformance/brands.ts`.
- `withConsent` wrapper (composes innermost — order: `withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)`).
- `requiresConsent: ConsentCategory[]` manifest field (schema + default).
- `assertFeatureConformance` extension for `ConsentChecked` brand check.
- `no-undeclared-consent-check` ESLint rule + `_manifest-ast.js` `requiresConsent` extraction.
- CLAUDE.md + `docs/guides/conformance-quickref.md` rule-count bump (11 → 12).
## Out of scope
- `PayloadConsent` Payload-backed implementation (Story 04).
- DI binders, handlers, tRPC router (Story 04).
- React subpath (Story 05).
- Anonymous migration helpers (Story 04).
## Tasks
- [ ] Run `pnpm turbo gen core-package consent` + add `ConsentCategory`, `ConsentState`, `UserConsentState` types in `core-consent/consent-types.ts` + `IConsent` interface in `core-consent/consent.interface.ts` + `ConsentChecked` brand + `isConsentChecked` helper in `core-shared/conformance/brands.ts` + `withConsent` wrapper in `core-consent/with-consent.ts` with unit tests asserting brand attachment and factory passthrough + `requiresConsent: ConsentCategory[]` field in the feature manifest schema (default `[]`) + extend `assertFeatureConformance` to require `ConsentChecked` brand when `requiresConsent.length > 0` with a synthetic fixture test asserting the boot failure; all gates pass.
- [ ] Add `no-undeclared-consent-check` ESLint rule (warn severity) in `packages/core-eslint/rules/` + extend `_manifest-ast.js` to extract `requiresConsent` + add RuleTester fixtures; update CLAUDE.md and `docs/guides/conformance-quickref.md` rule count 11 → 12; all gates pass.

View File

@@ -0,0 +1,57 @@
---
id: 04-core-consent-implementation
epic: dsr-consent-and-cookie-banner
title: core-consent implementation — Payload impl, DI binders, migration helpers, tRPC router
type: technical-story
status: todo
feature: core-consent
depends-on: [03-core-consent-foundation]
blocks:
[
05-core-consent-react,
07-core-api-router-composition,
10-auth-signup-migration,
]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Implement the runtime layer of `@repo/core-consent`: the Payload-backed `PayloadConsent` impl, the `RecordingConsent` test double, DI binders, the anonymous → authenticated consent migration helpers, protocol-agnostic handlers, and the `consentRouter` tRPC router.
## Why
Story 03 established the type surface and structural enforcement. This story delivers the working machinery: a Payload impl that reads/writes `users.consentState` and emits `CONSENT_GRANT`/`CONSENT_WITHDRAW` audit entries, migration helpers that consumers call in `signUp`, and a tRPC router that `core-api` can compose. Without this story, no downstream use case can actually check or record consent.
## Done when
- `PayloadConsent` in `core-consent/` reads `users.consentState` for fast `isGranted` reads and writes both the cache field and a `CONSENT_GRANT`/`CONSENT_WITHDRAW` audit entry via injected `core-audit` on grant/withdraw.
- `RecordingConsent` test double in `core-testing` records calls and payloads; unit tests assert captured calls match invocations.
- DI binders `core-consent/di/bind-production.ts` + `core-consent/di/bind-dev-seed.ts` exist; `assertFeatureConformance` passes at boot.
- Contract tests cover: `isGranted` returns `false` before grant, `true` after; `grant` writes both cache + audit entry with correct shape; `withdraw` clears cache + emits `CONSENT_WITHDRAW`; `getCategories` returns all categories with `granted: true`; consent state round-trip including `bannerVersion`, `policyVersion`, `method`.
- `extractAnonymousConsent(cookieHeader: string)` + `migrateAnonymousConsent({ userId, cookieState, bannerVersion, policyVersion })` exist in `core-consent/` with tests covering the happy path and absent-cookie no-op.
- Protocol-agnostic handlers in `core-consent/handlers/` return normalized `{ status, body, headers }`.
- `consentRouter` in `core-consent/consent.router.ts` exports `grant`, `withdraw`, `isGranted`, `getCategories` procedures with auth checks via `defineErrorMiddleware` pattern; integration tests assert response shapes and auth error passthrough.
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass after each task.
## In scope
- `PayloadConsent` Payload-backed implementation (reads/writes `users.consentState`, emits consent audit entries via injected `core-audit`).
- `RecordingConsent` test double in `packages/core-testing/`.
- DI binders (`bind-production`, `bind-dev-seed`) in `core-consent/di/`.
- `extractAnonymousConsent` + `migrateAnonymousConsent` migration helpers in `core-consent/`.
- Protocol-agnostic handlers in `core-consent/handlers/`.
- `consentRouter` tRPC router in `core-consent/consent.router.ts`.
## Out of scope
- React subpath `<ConsentProvider>` + `useConsent()` (Story 05).
- `core-api` appRouter composition (Story 07).
- `auth.signUp` integration (Story 10).
## Tasks
- [ ] Add `PayloadConsent` Payload-backed implementation in `core-consent/` reading/writing `users.consentState` and emitting `CONSENT_GRANT`/`CONSENT_WITHDRAW` audit entries via injected `core-audit` + `RecordingConsent` test double in `packages/core-testing/` + DI binders `core-consent/di/bind-production.ts` and `core-consent/di/bind-dev-seed.ts`; contract tests covering grant/withdraw/isGranted round-trip, audit entry shape, and `getCategories`; all gates pass.
- [ ] Add `extractAnonymousConsent(cookieHeader: string)` + `migrateAnonymousConsent({ userId, cookieState, bannerVersion, policyVersion })` helpers in `core-consent/` with tests covering the happy path (cookie present → calls `IConsent.grant` with `method: "signup-migration"`) and the absent-cookie no-op; all gates pass.
- [ ] Add protocol-agnostic handlers in `core-consent/handlers/` + `consentRouter` tRPC router in `core-consent/consent.router.ts` exporting `grant`, `withdraw`, `isGranted`, `getCategories` procedures with auth checks; integration tests asserting response shapes and auth error passthrough; all gates pass.

View File

@@ -0,0 +1,46 @@
---
id: 05-core-consent-react
epic: dsr-consent-and-cookie-banner
title: core-consent React subpath — ConsentProvider + useConsent()
type: technical-story
status: todo
feature: core-consent
depends-on: [04-core-consent-implementation]
blocks: [09-cookie-consent-banner]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Add the React subpath `core-consent/react` exporting `<ConsentProvider>` and `useConsent()`, following the pattern established by `@repo/core-analytics/react` (`<AnalyticsProvider>` + `useAnalytics()`).
## Why
The `<CookieConsentBanner>` component (Story 09) reads consent state and dispatches grant/withdraw via `useConsent()`. The React subpath must exist and be stable before the banner can be built.
## Done when
- `packages/core-consent/src/react/index.ts` exports `<ConsentProvider>` + `useConsent()`.
- `<ConsentProvider>` accepts an `IConsent` instance as a prop and makes it available via context.
- `useConsent()` returns `{ isGranted, grant, withdraw, getCategories }` bound to the context instance.
- The subpath is exposed via `package.json` exports as `"./react"`.
- Tests using React Testing Library assert: `useConsent()` returns the methods; `grant()` and `withdraw()` propagate to the injected `IConsent`; `isGranted()` reflects the mock instance's state; missing `<ConsentProvider>` ancestor throws a descriptive error.
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
## In scope
- `packages/core-consent/src/react/consent-provider.tsx` + `use-consent.ts`.
- `packages/core-consent/src/react/index.ts` barrel.
- `package.json` exports `"./react"` subpath entry.
- RTL + vitest unit tests.
## Out of scope
- `<CookieConsentBanner>` component (Story 09).
- Any cookie read/write logic (lives in the banner, not the hook).
- SSR placeholder / dynamic-import loader (documented in consent.md, Story 11; the hook itself is client-only by design — no SSR surface here).
## Tasks
- [ ] Add `<ConsentProvider>` + `useConsent()` hook in `packages/core-consent/src/react/` + barrel export + `"./react"` subpath in `package.json`; RTL tests asserting context propagation, method delegation, and missing-provider error; all gates pass.

View File

@@ -0,0 +1,63 @@
---
id: 06-core-dsr
epic: dsr-consent-and-cookie-banner
title: core-dsr — scaffold, interfaces, Payload impls, recording doubles, handlers, dsrRouter
type: technical-story
status: todo
feature: core-dsr
depends-on: [02-audit-enum-amendment]
blocks: [07-core-api-router-composition]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Scaffold `@repo/core-dsr` and deliver the complete DSR capability: four GDPR interfaces (`IDataExport`, `IDataDelete`, `IDataRectify`, `IProcessingRestriction`), their Payload-backed implementations walking Epic A's `custom.pii` tags and Story 01's `custom.subject` linkage, four recording doubles in `core-testing`, DI binders, protocol-agnostic handlers, and the `dsrRouter` tRPC router.
## Why
Every EU-bound consumer needs endpoints to satisfy GDPR Arts. 1518 + 20. The DSR interfaces encapsulate the cascade walk over `custom.subject`-linked collections so consumers never reinvent it. The Payload impls drive the reference implementation; the recording doubles let feature tests assert DSR behaviour without Payload. The tRPC router surfaces the capability to `core-api` (Story 07).
## Done when
- `pnpm turbo gen core-package dsr` produces a green package shell.
- `IDataExport`, `IDataDelete`, `IDataRectify`, `IProcessingRestriction` interfaces exist in `core-dsr/<interface>.interface.ts`.
- `core-dsr/contexts/user-data.jsonld` ships the schema.org JSON-LD `@context`.
- `PayloadDataExport.exportSubjectData("alice", "json")` walks `users` + any `custom.subject`-linked collections, returning a `UserDataBundle` with `asSelf` for `kind: "self" | "owner"` rows and `asReference` for `kind: "reference"` rows.
- `PayloadDataDelete.deleteSubjectData("alice", "soft")` flips `processingRestrictedAt`, NULLs exportable PII, redacts `reference`-role linked fields to `null`, emits one audit entry per affected collection, returns a `DeletionCertificate`.
- `PayloadDataDelete.deleteSubjectData("alice", "cascade-hard")` hard-deletes `self`/`owner` rows and redacts `reference` fields immediately (admin-only; auth check at procedure level).
- `PayloadDataRectify.updateSubjectField` updates the specified field and emits a `RESTRICT` audit entry with `reason: "art-16-request"`.
- `PayloadProcessingRestriction.{setRestriction, isRestricted}` toggles and reads `processingRestrictedAt`; emits `RESTRICT`/`UNRESTRICT` audit entries.
- `RecordingDataExport`, `RecordingDataDelete`, `RecordingDataRectify`, `RecordingProcessingRestriction` test doubles in `core-testing` record calls and payloads; unit tests assert shape.
- DI binders `core-dsr/di/bind-production.ts` + `core-dsr/di/bind-dev-seed.ts` wire all four interfaces; `assertFeatureConformance` passes at boot.
- Contract tests cover: happy path per role/mode, multi-subject row redaction (only the requesting subject's link is redacted, row preserved), JSON-LD `@context` correctness (parsed by `jsonld` library in test only), audit emission shape per operation, restriction flag honored by `isRestricted`.
- Protocol-agnostic handlers in `core-dsr/handlers/{export,delete,rectify,restrict}-handler.ts` return normalized `{ status, body, headers }`.
- `dsrRouter` in `core-dsr/dsr.router.ts` exports `export`, `delete`, `rectify`, `restrict` procedures with auth checks; integration tests assert response shapes and error passthrough.
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass after each task.
## In scope
- `pnpm turbo gen core-package dsr` scaffold.
- Four GDPR interfaces + `UserDataBundle` + `DeletionCertificate` types.
- `core-dsr/contexts/user-data.jsonld` (schema.org `@context`; consumer-overridable).
- `PayloadDataExport`, `PayloadDataDelete`, `PayloadDataRectify`, `PayloadProcessingRestriction` Payload-backed implementations.
- `RecordingData*` test doubles in `packages/core-testing/`.
- DI binders in `core-dsr/di/`.
- Protocol-agnostic handlers in `core-dsr/handlers/`.
- `dsrRouter` tRPC router in `core-dsr/dsr.router.ts`.
## Out of scope
- `core-api` appRouter composition (Story 07).
- Streaming `IDataExport` — in-memory `UserDataBundle` only (streaming v2 deferred).
- `dsr_rectifications` separate audit collection — main audit log via `reason: "art-16-request"` tag is sufficient.
- `withRestriction` brand-treatment — consumer calls `isRestricted` where needed; no wrapper.
- 30-day grace period hard-delete — handled by Epic A's existing retention purge job (no new code here).
- Consent checks on DSR use cases — DSR operations are subject-rights fulfilment, not consent-gated.
## Tasks
- [ ] Run `pnpm turbo gen core-package dsr` + define `IDataExport`, `IDataDelete`, `IDataRectify`, `IProcessingRestriction` interfaces in `core-dsr/<interface>.interface.ts` + add `UserDataBundle` + `DeletionCertificate` types + ship `core-dsr/contexts/user-data.jsonld`; all gates pass.
- [ ] Add `PayloadDataExport`, `PayloadDataDelete`, `PayloadDataRectify`, `PayloadProcessingRestriction` Payload-backed implementations walking `custom.pii` tags and `custom.subject` linkage (cascade semantics per role: self/owner/reference) + `RecordingDataExport`, `RecordingDataDelete`, `RecordingDataRectify`, `RecordingProcessingRestriction` test doubles in `packages/core-testing/` + DI binders `core-dsr/di/bind-production.ts` and `core-dsr/di/bind-dev-seed.ts`; contract tests covering happy path per role/mode, multi-subject row redaction, JSON-LD `@context` correctness, audit emission shape, restriction flag; all gates pass.
- [ ] Add protocol-agnostic handlers in `core-dsr/handlers/{export,delete,rectify,restrict}-handler.ts` + `dsrRouter` tRPC router in `core-dsr/dsr.router.ts` with auth checks; integration tests asserting procedure response shapes and error passthrough; all gates pass.

View File

@@ -0,0 +1,42 @@
---
id: 07-core-api-router-composition
epic: dsr-consent-and-cookie-banner
title: core-api router composition — dsrRouter + consentRouter into appRouter
type: technical-story
status: todo
feature: core-api
depends-on: [04-core-consent-implementation, 06-core-dsr]
blocks: [11-documentation]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Compose `dsrRouter` and `consentRouter` into `core-api`'s `appRouter` via the existing `<gen:*>` anchor + barrel pattern, making the DSR and consent tRPC procedures available to consuming apps without manual wiring.
## Why
The two optional-core routers exist after Stories 04 and 06 but are unreachable until `core-api` composes them. The `<gen:*>` anchor means no hand-wired boilerplate — just adding the imports and router entries. Completing this story closes the API surface loop.
## Done when
- `core-api`'s `appRouter` includes `dsr: dsrRouter` and `consent: consentRouter`.
- Import and composition follow the existing `<gen:*>` anchor pattern.
- tRPC integration tests assert `trpc.dsr.export`, `trpc.dsr.delete`, `trpc.dsr.rectify`, `trpc.dsr.restrict`, `trpc.consent.grant`, `trpc.consent.withdraw`, `trpc.consent.isGranted`, `trpc.consent.getCategories` all resolve (auth + response shape checks).
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
## In scope
- `core-api/src/router.ts` (or equivalent) — add `dsr: dsrRouter` + `consent: consentRouter` entries.
- Import of `dsrRouter` from `@repo/core-dsr` and `consentRouter` from `@repo/core-consent`.
- tRPC integration tests for all eight new procedures.
## Out of scope
- HTTP endpoint scaffolding — tRPC only per established pattern; REST wrapping documented for regulators in Epic D.
- Per-framework router auto-wiring for cookie banner pageview reset — banner emits `onConsentChange`; consumer wires their router.
## Tasks
- [ ] Import `dsrRouter` from `@repo/core-dsr` and `consentRouter` from `@repo/core-consent` and compose them into `core-api`'s `appRouter` via the `<gen:*>` anchor pattern; add tRPC integration tests asserting all eight procedures resolve with correct auth and response shapes; all gates pass.

View File

@@ -0,0 +1,39 @@
---
id: 08-core-ui-scaffold
epic: dsr-consent-and-cookie-banner
title: core-ui scaffold
type: technical-story
status: todo
feature: core-ui
depends-on: []
blocks: [09-cookie-consent-banner]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Scaffold `@repo/core-ui` via the generator, producing a green package shell that Story 09 will populate with the `<CookieConsentBanner>` component.
## Why
The PRD notes that `core-ui`'s directory exists but is empty. Running the generator is the required first step before any component work can land — it wires the package into the Turborepo graph, establishes the package.json and tsconfig.json, and creates the Storybook entry point. Story 09 cannot land without a valid package shell.
## Done when
- `packages/core-ui/` is a valid Turborepo package with `package.json`, `tsconfig.json`, `vitest.config.ts`, and an empty `src/index.ts` barrel.
- The package appears in `pnpm turbo boundaries` output.
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
## In scope
- `pnpm turbo gen core-package ui` invocation and any post-scaffold fixes required to make the shell green.
## Out of scope
- `<CookieConsentBanner>` component (Story 09).
- Any UI component other than the banner — this epic ships only what the PRD requires.
## Tasks
- [ ] Run `pnpm turbo gen core-package ui` and apply any post-scaffold fixes needed to produce a green package shell (`pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` pass).

View File

@@ -0,0 +1,54 @@
---
id: 09-cookie-consent-banner
epic: dsr-consent-and-cookie-banner
title: CookieConsentBanner component in core-ui
type: user-story
status: todo
feature: core-ui
depends-on: [05-core-consent-react, 08-core-ui-scaffold]
blocks: [11-documentation]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Ship `<CookieConsentBanner>` in `@repo/core-ui` — a headless component with a EU-compliant default UI (equal-prominence Reject All / Accept All), granular category toggles, render-prop overrides for branding, `__consent_state` cookie management for the anonymous pre-signup flow, and `useConsent()` integration for authenticated state.
## Why
Downstream consumers need a drop-in cookie consent banner that satisfies CNIL guidance and EDPB Art. 7 interpretation out of the box (equal visual weight for Reject All / Accept All, no pre-ticked boxes, equal tab order). Building the component as a headless default with render-prop overrides lets consumers brand the visuals without forking the compliance logic. The Storybook story doubles as the human-readable compliance review surface.
## Done when
- `<CookieConsentBanner variant="modal">` renders with Reject All / Accept All as equal-size, equal-weight side-by-side buttons; tab order treats them equally; ARIA labels mirror; focus-trapped inside modal; ESC = "Reject All" (explicit legal choice, not silent dismiss).
- `<CookieConsentBanner variant="banner">` renders fixed to the bottom of the viewport, full-width.
- Default categories: `essential` (always enabled, non-toggleable), `functional`, `analytics`, `marketing`.
- Render-prop overrides: `renderHeader`, `renderCategoryRow`, `renderActions` — default UI works out-of-box; consumer surgically overrides.
- `__consent_state` cookie: SameSite=Lax, Secure, 1-year max-age, versioned with `_v: 1`; component reads/writes/clears.
- `onConsentChange` callback fires with updated `UserConsentState`.
- When `<ConsentProvider>` is present (authenticated context), the banner reads and writes via `useConsent()` from `@repo/core-consent/react`; when absent, the banner manages state via the cookie only (anonymous flow).
- Storybook story covers: modal variant, banner variant, render-prop override example, a11y tab-order demo.
- axe-core a11y test in Storybook passes (WCAG 2.2 AA color contrast, no violations).
- RTL behavioral tests assert: click "Reject All" → `onConsentChange` fires with all non-essential categories `granted: false`; toggle analytics → click "Save Selected" → `onConsentChange` fires with `analytics.granted: true`; ESC in modal → Reject All semantics; tab order visits Reject All before Accept All.
- Modal focus-trap test: focus does not escape the modal while it is open.
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
## In scope
- `packages/core-ui/src/cookie-consent-banner/` — component, types, cookie helpers.
- `__consent_state` cookie read/write/clear (versioned `_v: 1` shape; migrate older versions on read).
- Storybook story in `packages/core-ui/src/cookie-consent-banner/cookie-consent-banner.stories.tsx`.
- axe-core a11y integration in Storybook + RTL behavioral tests.
- Export from `packages/core-ui/src/index.ts` barrel.
- SSR-safe pattern: component is client-only; ship a `<CookieConsentBannerLoader>` SSR placeholder that dynamic-imports the actual banner client-side (see consent.md, Q3 of grill).
## Out of scope
- Per-framework router auto-wiring on consent toggle — banner emits `onConsentChange`; consumer wires (e.g., re-initialize analytics SDK).
- Cookie versioning migration policy documentation (Story 11).
- Strict-mode `ConsentCategory` declaration merging — string-literal-union escape hatch is sufficient.
## Tasks
- [ ] Add `<CookieConsentBanner>` headless component in `packages/core-ui/src/cookie-consent-banner/` with `variant: "modal" | "banner"` prop, granular category toggles (essential non-toggleable), equal-prominence Reject All / Accept All buttons (CNIL compliance baked into default visual treatment), render-prop overrides (`renderHeader`, `renderCategoryRow`, `renderActions`), `__consent_state` cookie management (SameSite=Lax, Secure, 1-year, versioned `_v: 1`), `onConsentChange` callback, `useConsent()` integration for authenticated context, `<CookieConsentBannerLoader>` SSR-safe wrapper; Storybook story covering both variants + render-prop example; axe-core a11y pass; RTL behavioral tests (Reject All, Save Selected with toggle, ESC = Reject All, tab order, modal focus-trap); export from `core-ui` index; all gates pass.

View File

@@ -0,0 +1,43 @@
---
id: 10-auth-signup-migration
epic: dsr-consent-and-cookie-banner
title: auth signUp anonymous consent migration
type: technical-story
status: todo
feature: auth
depends-on: [04-core-consent-implementation]
blocks: [11-documentation]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Extend the template's `auth.signUp` use case to call `extractAnonymousConsent` + `migrateAnonymousConsent` when a `__consent_state` cookie is present, so a user's pre-signup banner choices persist into their account automatically.
## Why
Without this integration, anonymous users who consented via the banner before signing up would lose their consent state at account creation — forcing them to re-consent or leaving analytics gated incorrectly. The migration call is the canonical example for downstream consumers implementing the same pattern in their own `signUp` use cases.
## Done when
- `auth.signUp` use case calls `extractAnonymousConsent(cookieHeader)` after user-record creation; if a `__consent_state` cookie is present, calls `migrateAnonymousConsent({ userId, cookieState, bannerVersion, policyVersion })`.
- Response includes `Set-Cookie: __consent_state=; Max-Age=0` to clear the anonymous cookie after migration.
- `auth.signUp.use-case.test.ts` covers: mock cookie header present → `migrateAnonymousConsent` called with correct args → audit entry has `method: "signup-migration"` → response cookie cleared; no cookie present → `migrateAnonymousConsent` not called.
- `RecordingConsent` from `core-testing` is used for assertions (not a raw mock).
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
## In scope
- `packages/auth/src/use-cases/sign-up.use-case.ts` — add `extractAnonymousConsent` + `migrateAnonymousConsent` calls.
- `packages/auth/src/use-cases/sign-up.use-case.test.ts` — extend with migration scenarios.
- Auth feature's DI binders — inject `IConsent` dependency when present (guard with `?.` for consumers who haven't installed `core-consent`).
## Out of scope
- Anonymous consent storage in `users.consentState` directly from the banner — anonymous state lives in the cookie until this migration.
- Post-migration analytics re-initialization — consumer's responsibility via `onConsentChange` callback.
## Tasks
- [ ] Extend `auth.signUp` use case to call `extractAnonymousConsent(cookieHeader)` + `migrateAnonymousConsent({ userId, cookieState, bannerVersion, policyVersion })` when a `__consent_state` cookie is present, set `Set-Cookie: __consent_state=; Max-Age=0` in the response, and inject `IConsent` into the use case deps (optional, guarded with `?.`); extend `sign-up.use-case.test.ts` with `RecordingConsent` to assert migration call shape, audit entry `method: "signup-migration"`, and cookie-clear; all gates pass.

View File

@@ -0,0 +1,55 @@
---
id: 11-documentation
epic: dsr-consent-and-cookie-banner
title: Documentation — DSR guide, consent guide, glossary, CLAUDE.md
type: technical-story
status: todo
feature: core-shared
depends-on:
[
07-core-api-router-composition,
09-cookie-consent-banner,
10-auth-signup-migration,
]
blocks: []
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Write the consumer-facing documentation for DSR and consent, update the glossary with new terms, and update CLAUDE.md and conformance-quickref with the new manifest field and rule count.
## Why
Story 15 in the PRD: a DPO should be able to answer "what data do we hold + how does a subject act on it" by reading `compliance/data-map.yml` (Epic A) and the DSR endpoint mapping (this epic) without reading code. The documentation also serves AI agents scaffolding new features that need consent gates or DSR wiring.
## Done when
- `docs/guides/dsr.md` exists and covers: interfaces, tRPC procedure → GDPR article mapping, route wiring for consumers, multi-subject handling, soft vs cascade-hard delete semantics, `DeletionCertificate` format, Art. 15/16/17/18/20 compliance notes.
- `docs/guides/consent.md` exists and covers: `requiresConsent` manifest field + brand + runtime check pattern, `IConsent.grant` + audit trail, anonymous → authenticated migration flow, cookie versioning policy (`_v` field, migration-on-read), SSR-safe banner loading pattern, CNIL/EDPB equal-prominence requirement.
- `docs/compliance/subject-linkage.example.md` documents the `custom.subject` declaration pattern with a worked example of a multi-subject collection (e.g., a support ticket with submitter + assignee), providing the anchor for downstream consumers adding PII-holding collections.
- `docs/glossary.md` gains entries for: `SubjectLink`, `DeletionCertificate`, `UserConsentState`, `ConsentChecked` (brand).
- `CLAUDE.md` reflects: conformance rule count 11 → 12, new manifest field `requiresConsent: ConsentCategory[]` in the Key Conventions section, updated brand composition order (`withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)`).
- `docs/guides/conformance-quickref.md` reflects the new rule + manifest field.
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
## In scope
- `docs/guides/dsr.md` (new file).
- `docs/guides/consent.md` (new file).
- `docs/compliance/subject-linkage.example.md` (new file).
- `docs/glossary.md` — four new entries.
- `CLAUDE.md` — rule count + manifest field + brand composition order.
- `docs/guides/conformance-quickref.md` — rule + manifest field.
## Out of scope
- Pre-launch compliance checklist + fill-in templates (Epic D).
- REST endpoint documentation (Epic D).
- Cross-region transfer documentation / Schrems II / TIA (Epic D).
- Per-framework router auto-wiring docs (out of scope per PRD).
## Tasks
- [ ] Write `docs/guides/dsr.md` + `docs/guides/consent.md` + `docs/compliance/subject-linkage.example.md` + update `docs/glossary.md` with `SubjectLink`, `DeletionCertificate`, `UserConsentState`, `ConsentChecked` entries + update `CLAUDE.md` (rule count 11 → 12, `requiresConsent` manifest field, updated brand composition order) + update `docs/guides/conformance-quickref.md`; all gates pass.

View File

@@ -0,0 +1,42 @@
---
id: dsr-consent-and-cookie-banner
prd: docs/work/prds/dsr-consent-and-cookie-banner.prd.md
title: DSR + consent abstraction + cookie consent banner — Epic B of ADR-025
type: epic
status: in-progress
features:
[
core-shared,
core-consent,
core-dsr,
core-ui,
core-eslint,
core-testing,
core-api,
auth,
]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T09:55:07.020Z
---
## Goal
Ship the user-rights surface end-to-end: DSR endpoints that walk Epic A's PII tags to export/delete/rectify/restrict any subject's data, per-use-case consent gates with audit-logged proof, and a compliant cookie consent banner with EU-prominence defaults.
## Why
Epic A delivered declarative PII inventory + retention + sub-processors. Epic B closes the remaining gaps: GDPR Arts. 1518 + 20 DSR endpoints, Art. 7 demonstrable consent with structural lint enforcement, and a CNIL-compliant cookie consent banner that downstream consumers can drop in without forking legal-compliance logic.
## Stories
- [ ] [01 — Subject-linkage types in core-shared](01-subject-linkage-types/_story.md)
- [ ] [02 — Audit action enum amendment (ADR-018)](02-audit-enum-amendment/_story.md)
- [ ] [03 — core-consent foundation: types + brand + withConsent + conformance + ESLint rule](03-core-consent-foundation/_story.md)
- [ ] [04 — core-consent implementation: Payload impl + DI + migration helpers + tRPC router](04-core-consent-implementation/_story.md)
- [ ] [05 — core-consent React subpath: ConsentProvider + useConsent()](05-core-consent-react/_story.md)
- [ ] [06 — core-dsr: scaffold + interfaces + Payload impls + handlers + dsrRouter](06-core-dsr/_story.md)
- [ ] [07 — core-api router composition: dsrRouter + consentRouter into appRouter](07-core-api-router-composition/_story.md)
- [ ] [08 — core-ui scaffold](08-core-ui-scaffold/_story.md)
- [ ] [09 — CookieConsentBanner component in core-ui](09-cookie-consent-banner/_story.md)
- [ ] [10 — auth signUp anonymous consent migration](10-auth-signup-migration/_story.md)
- [ ] [11 — Documentation: DSR guide + consent guide + glossary + CLAUDE.md](11-documentation/_story.md)