docs(work): archive shipped template epics and PRDs
Move the 8 shipped template epics and their 9 PRDs (incl.
coverage-architecture) to docs/work/archive/{epics,prds}/ so dispatch
context and prioritization only see live Veect work. The state builder
already walks docs/work/epics/ + docs/work/prds/ only; the one work-CLI
script that matched archive paths (bump-updated-timestamps.mjs, staged
docs/work/**/*.md) now excludes docs/work/archive/ so archived content
stays byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: 01-wire-use-case-helper
|
||||
epic: binder-wrap-helper
|
||||
title: Introduce wireUseCase helper in core-shared
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: []
|
||||
blocks: [02-migrate-feature-binders, 03-update-generator-templates]
|
||||
created: 2026-05-13T19:17:10+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add `wire-use-case.ts` + `wire-use-case.test.ts` to `packages/core-shared/src/conformance/` and export the helper from the `@repo/core-shared/conformance` barrel. The helper encapsulates `withSpan(withCapture(withAudit?(factory(deps))))` composition and performs the container bind step, so callers pass options and get back a brand-stacked wired value.
|
||||
|
||||
## Why
|
||||
|
||||
Every feature binder currently inlines the same `withSpan + withCapture` wrapping per use case — 30–79 lines of mechanical boilerplate per binder pair. A single helper in core-shared eliminates the structural clone groups and makes adjusting the wrapping shape a one-file change.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-shared/src/conformance/wire-use-case.ts` exists and is exported from `packages/core-shared/src/conformance/index.ts`.
|
||||
- `wire-use-case.test.ts` covers: no-audit path (Instrumented + Captured brands present), audit path (Instrumented + Captured + Audited brands present), span-name derivation (`<feature>.<name>`), capture-tag structure (`{ feature, layer, name }`), container binding (symbol resolves to wired value), idempotent re-bind (unbind + bind when symbol already bound).
|
||||
- `pnpm typecheck && pnpm test && pnpm lint && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
- `wire-use-case.ts` reaches 100% statement/branch coverage (file is small; exhaustive test coverage is achievable).
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-shared/src/conformance/wire-use-case.ts` — the helper and its exported types.
|
||||
- `packages/core-shared/src/conformance/wire-use-case.test.ts` — unit tests using `RecordingTracer` / `RecordingLogger` / `RecordingAuditLog` (or equivalent mocks from core-shared/core-audit).
|
||||
- Export line added to `packages/core-shared/src/conformance/index.ts`.
|
||||
- No changes to `withSpan`, `withCapture`, or `withAudit` internals — the helper composes them as-is.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Feature binder changes (Story 02).
|
||||
- Generator template changes (Story 03).
|
||||
- `wireController` peer — the `layer` discriminator in `wireUseCase` covers controllers too if the shapes converge; a separate peer is a follow-up if needed.
|
||||
- Repository or service binding patterns.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `wire-use-case.ts` + `wire-use-case.test.ts` to `packages/core-shared/src/conformance/` and export from the conformance index — implement the helper (options object: `container`, `symbol`, `factory`, `deps`, `feature`, `layer`, `name`, `tracer`, `logger`, optional `auditLog`) with full unit tests covering both audit and no-audit paths, brand presence assertions via `isInstrumented` / `isCaptured` / `isAudited`, container binding, and idempotent re-bind; all gates pass on this single commit.
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
id: 02-migrate-feature-binders
|
||||
epic: binder-wrap-helper
|
||||
title: Migrate all five feature binders to wireUseCase
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: auth, blog, media, marketing-pages, navigation
|
||||
depends-on: [01-wire-use-case-helper]
|
||||
blocks: []
|
||||
created: 2026-05-13T19:17:10+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Replace every inline `withSpan + withCapture (+ withAudit)` block in each feature's `bind-production.ts` and `bind-dev-seed.ts` with a call to `wireUseCase`. Existing binder-level and integration tests must continue to pass unchanged. Each feature migrates in its own commit.
|
||||
|
||||
## Why
|
||||
|
||||
The five inline wrapping clone groups are the top source of duplication reported by `pnpm fallow`. Moving to `wireUseCase` eliminates all five clone groups and leaves each binder as decision content only (which adapter, which mode, which symbol).
|
||||
|
||||
## Done when
|
||||
|
||||
- All 10 binder files (`auth ×2`, `blog ×2`, `media ×2`, `marketing-pages ×2`, `navigation ×2`) call `wireUseCase` for every use case binding; no inline `withSpan + withCapture` blocks remain for use cases.
|
||||
- `assertFeatureConformance` at the tail of each `bind-production.ts` continues to accept all wired use cases (brands present, manifest entries match).
|
||||
- `pnpm typecheck && pnpm test && pnpm lint && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` pass after each per-feature commit.
|
||||
- `pnpm fallow dupes` no longer shows the five binder-pair clone groups in its output.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/auth/src/di/bind-production.ts` + `bind-dev-seed.ts` — 3 use cases (signIn, signUp, signOut).
|
||||
- `packages/blog/src/di/bind-production.ts` + `bind-dev-seed.ts` — 3 use cases (getArticles, getArticleBySlug, createArticle).
|
||||
- `packages/media/src/di/bind-production.ts` + `bind-dev-seed.ts` — 3 use cases (getMedia, listMedia, deleteMedia).
|
||||
- `packages/marketing-pages/src/di/bind-production.ts` + `bind-dev-seed.ts` — 2 use cases (getPageBySlug, getSiteSettings).
|
||||
- `packages/navigation/src/di/bind-production.ts` + `bind-dev-seed.ts` — 1 use case (getHeader).
|
||||
- If any binder's existing tests assert on internal wrapping shape rather than observable behaviour, refactor those assertions to test through the helper's contract (same commit as the binder change).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Controller bindings — covered by the helper per the PRD but deferred to a follow-up if scope is a concern.
|
||||
- Repository and service bindings — stay as direct `.toConstantValue()` calls.
|
||||
- Apps' `bindAll()` dispatcher — untouched.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Migrate `auth` binders (`bind-production.ts` + `bind-dev-seed.ts`) to `wireUseCase` for all 3 use cases (signIn, signUp, signOut); all gates pass on this commit.
|
||||
- [x] Migrate `blog` binders to `wireUseCase` for all 3 use cases (getArticles, getArticleBySlug, createArticle); all gates pass on this commit.
|
||||
- [x] Migrate `media` binders to `wireUseCase` for all 3 use cases (getMedia, listMedia, deleteMedia); all gates pass on this commit.
|
||||
- [x] Migrate `marketing-pages` binders to `wireUseCase` for both use cases (getPageBySlug, getSiteSettings); all gates pass on this commit.
|
||||
- [x] Migrate `navigation` binders to `wireUseCase` for the single use case (getHeader); all gates pass on this commit; verify `pnpm fallow dupes` no longer surfaces the five binder-pair clone groups.
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
id: 03-update-generator-templates
|
||||
epic: binder-wrap-helper
|
||||
title: Update feature generator templates to emit wireUseCase call shape
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: [01-wire-use-case-helper]
|
||||
blocks: []
|
||||
created: 2026-05-13T19:17:10+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Update `turbo/generators/templates/feature/src/di/bind-production.ts.hbs` and `bind-dev-seed.ts.hbs` so that `pnpm turbo gen feature` scaffolds new features with `wireUseCase` calls instead of the longhand inline `withSpan + withCapture` form.
|
||||
|
||||
## Why
|
||||
|
||||
New features scaffolded after the migration would otherwise drift back to the inline form, re-introducing the clone group. The generator is the canonical source for scaffolded binder shape; updating it closes the loop.
|
||||
|
||||
## Done when
|
||||
|
||||
- `turbo/generators/templates/feature/src/di/bind-production.ts.hbs` emits `wireUseCase(...)` calls (not inline `withSpan + withCapture`) for the placeholder use case.
|
||||
- `turbo/generators/templates/feature/src/di/bind-dev-seed.ts.hbs` emits `wireUseCase(...)` calls for the placeholder use case.
|
||||
- Running `pnpm turbo gen feature testfeature` produces binder files that call `wireUseCase` and pass `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` (or the generated feature is cleaned up after verification).
|
||||
|
||||
## In scope
|
||||
|
||||
- `turbo/generators/templates/feature/src/di/bind-production.ts.hbs`
|
||||
- `turbo/generators/templates/feature/src/di/bind-dev-seed.ts.hbs`
|
||||
- Any supporting type imports in the templates that change as a result (e.g. dropping `ProductionUseCase` intermediate type if the helper absorbs it).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Generator templates for events, jobs, realtime, or core-packages — those do not produce binder files with use-case wrapping.
|
||||
- Runtime behaviour changes — the generated output must be semantically equivalent to the old inline form.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Update `bind-production.ts.hbs` and `bind-dev-seed.ts.hbs` in `turbo/generators/templates/feature/src/di/` to emit `wireUseCase` calls; verify a test-scaffold of a new feature produces correct binder output and all gates pass on this commit.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
id: binder-wrap-helper
|
||||
prd: docs/work/prds/binder-wrap-helper.prd.md
|
||||
title: Collapse binder duplication via wireUseCase helper
|
||||
type: epic
|
||||
status: done
|
||||
features: [core-shared, auth, blog, media, marketing-pages, navigation]
|
||||
created: 2026-05-13T00:00:00Z
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Introduce a `wireUseCase(...)` helper in `@repo/core-shared/conformance/` that encapsulates the `withSpan + withCapture (+ optional withAudit)` composition. Refactor all five features' binders to call the helper. Update the feature generator templates to emit the new call shape by default.
|
||||
|
||||
## Why
|
||||
|
||||
Five of `pnpm fallow`'s top-ten clone groups come from binder pairs across features. The inline wrapping runs 30–79 duplicated lines per binder pair. The helper becomes the single source of truth for the wrapping shape; per-feature binders shrink to their decision content plus a list of `wireUseCase` calls.
|
||||
|
||||
## Stories
|
||||
|
||||
- [x] [01 — Introduce `wireUseCase` helper in core-shared](01-wire-use-case-helper/_story.md)
|
||||
- [x] [02 — Migrate all five feature binders to `wireUseCase`](02-migrate-feature-binders/_story.md)
|
||||
- [x] [03 — Update generator templates to emit `wireUseCase` call shape](03-update-generator-templates/_story.md)
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
id: 01-trace-schema-extensions
|
||||
epic: ci-security-and-supply-chain
|
||||
title: Trace schema extensions (socketRisk + lastRevalidated)
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: scripts
|
||||
depends-on: []
|
||||
blocks:
|
||||
[
|
||||
02-socket-integration,
|
||||
04-major-bump-reevaluation,
|
||||
05-trace-revalidation-workflow,
|
||||
]
|
||||
created: 2026-05-14T18:59:12+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Extend `scripts/library-decisions/schema.mjs` with two new fields — `socketRisk` (the 9th filter result) and `lastRevalidated` (ISO-date or null) — and update `docs/library-decisions/_template.md` to reflect the expanded schema.
|
||||
|
||||
## Why
|
||||
|
||||
Every downstream enforcement layer (the evaluate-library skill's 9th filter, the check.mjs major-bump mode, the revalidate.mjs weekly cron) depends on these two fields being present and validated at the schema layer first. Landing them here in one green commit before any consumer touches them prevents schema-drift between layers.
|
||||
|
||||
**External dependency:** library-evaluation epic story 01 (trace schema foundation) must be complete before this story — `scripts/library-decisions/schema.mjs` and `docs/library-decisions/_template.md` must exist. That epic is marked done.
|
||||
|
||||
## Done when
|
||||
|
||||
- `scripts/library-decisions/schema.mjs` exports an updated Zod schema where `filter-results` includes `socketRisk: z.union([z.literal("clean"), z.literal("flagged"), z.string()])` and the trace frontmatter includes `lastRevalidated: z.string().nullable()` (ISO date or null).
|
||||
- `docs/library-decisions/_template.md` mirrors both new fields with inline documentation of their allowed values.
|
||||
- `schema.test.mjs` covers: `socketRisk` round-trips for all three variants (`clean`, `flagged`, arbitrary string); `lastRevalidated` accepts ISO date strings and `null`; a trace missing `socketRisk` in `filter-results` fails validation; `lastRevalidated: null` is valid (default for fresh adoptions).
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `scripts/library-decisions/schema.mjs` — schema additions only (no new exports, no breaking changes to existing field shapes).
|
||||
- `scripts/library-decisions/schema.test.mjs` — new test cases for the two new fields.
|
||||
- `docs/library-decisions/_template.md` — field documentation additions.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `socket-cli` invocation or Socket CI step — Story 02.
|
||||
- `check.mjs` major-bump mode — Story 04.
|
||||
- `revalidate.mjs` script — Story 05.
|
||||
- Backfilling existing traces with the new fields — Story 05 (the revalidation cron handles this on its first run).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Extend `scripts/library-decisions/schema.mjs` adding `socketRisk` (union: `"clean" | "flagged" | string`) to the `filter-results` Zod object and `lastRevalidated` (nullable ISO-date string) to the trace frontmatter schema; update `docs/library-decisions/_template.md` to document both fields with their enum values; add test cases to `schema.test.mjs` covering `socketRisk` round-trips (all three variants), `lastRevalidated` ISO + null acceptance, and missing-`socketRisk` rejection; all gates pass on this single commit.
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: 02-socket-integration
|
||||
epic: ci-security-and-supply-chain
|
||||
title: Socket integration (skill + CI)
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: [01-trace-schema-extensions]
|
||||
blocks: [08-reviewer-prompt-update]
|
||||
created: 2026-05-14T18:59:12+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Wire Socket.dev into two enforcement layers: (1) the `evaluate-library` skill gains Filter 9 (supply-chain behavior) using `socket-cli`, and (2) `ci.yml` gains a `socket-cli scan` step that fails on `critical` severity findings.
|
||||
|
||||
## Why
|
||||
|
||||
CVE databases are lagging indicators — `event-stream`, `ua-parser-js`, and `tj-actions/changed-files` all shipped malware before any CVE existed. Socket detects behavioral signals (new network calls, new post-install scripts, maintainer-account changes) in real time. Placing it as the 9th filter in `evaluate-library` + as a CI gate closes the behavior-compromise surface that CVE scanning misses.
|
||||
|
||||
**External dependency:** library-evaluation epic story 04 (evaluate-library skill) must be complete — the skill's SKILL.md must exist and have the 8-filter structure. That epic is marked done.
|
||||
|
||||
## Done when
|
||||
|
||||
- `.claude/skills/evaluate-library/SKILL.md` has a "Filter 9 — Supply-chain behavior (Socket)" section. The skill's fail-fast logic positions Socket as expensive (network call), running it after the cheap structural filters. The section documents the `socket-cli` verification command and the JSON output fields used to classify `clean` / `flagged` / `<finding-summary>`. The trace's `socket-risk` field in `filter-results` is set from this output.
|
||||
- `.socket.json` exists at repo root: `{ "issueRules": { "critical": "error", "high": "warn", "medium": "ignore", "low": "ignore" } }`.
|
||||
- `ci.yml`'s `validate` job has a step that runs `socket-cli scan` against the lockfile, filtered to PRs that touch `package.json` or `pnpm-lock.yaml` (via `paths:` condition). The step exits non-zero on any `critical` finding.
|
||||
- `docs/guides/ci-security.md` Socket App install instructions are deferred to Story 09 (the human guide). This story ships only the machine-enforced layers.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `.claude/skills/evaluate-library/SKILL.md` — Filter 9 section addition.
|
||||
- `.socket.json` — repo-root config file.
|
||||
- `.github/workflows/ci.yml` — one new step in the `validate` job (with `paths:` filter).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Paid Socket Team plan or server-side PR-block enforcement — explicitly out of PRD scope.
|
||||
- Socket GitHub App install — consumer-facing instructions live in Story 09's guide.
|
||||
- Backfilling existing traces with `socket-risk` — Story 05 (revalidation cron handles this).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `.socket.json` at repo root and extend `.claude/skills/evaluate-library/SKILL.md` with a "Filter 9 — Supply-chain behavior (Socket)" section: position Socket after cheap filters, document `socket-cli` as the verification command, specify how `clean`/`flagged`/`<finding-summary>` maps to the trace's `socket-risk` field; one commit, all gates pass.
|
||||
- [x] Add a `socket-cli scan` step to `ci.yml`'s `validate` job, scoped to PRs touching `package.json` or `pnpm-lock.yaml` via a `paths:` condition; step exits non-zero on any `critical` finding; one commit, all gates pass.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
id: 03-renovate-adoption
|
||||
epic: ci-security-and-supply-chain
|
||||
title: Renovate adoption
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: []
|
||||
blocks: [09-ci-security-guide-and-docs]
|
||||
created: 2026-05-14T18:59:12+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Ship `.github/renovate.json` configuring automated dependency management: grouped per-ecosystem npm bumps, Dockerfile base-image tracking, GitHub Actions SHA pinning, automerge for green minor+patch PRs, and a single dependency dashboard issue.
|
||||
|
||||
## Why
|
||||
|
||||
Major-tag pinning for GitHub Actions is documented insecure — the 2025 `tj-actions/changed-files` incident proved it. Renovate's `pinGitHubActionDigests` preset automates the one-time SHA-pin sweep and keeps SHAs current thereafter. Grouping ecosystem clusters (Sentry, OTel, tRPC, Payload, Inversify) into weekly PRs prevents noise while ensuring drift is surfaced. Automerge on green minor+patch PRs removes human toil for routine bumps.
|
||||
|
||||
## Done when
|
||||
|
||||
- `.github/renovate.json` exists and is valid JSON, extending presets: `config:base`, `helpers:pinGitHubActionDigests`, `:separateMajorReleases`, `:automergeMinor`, `:automergePatch`.
|
||||
- `packageRules` groups `@sentry/*`, `@opentelemetry/*`, `@trpc/*`, `payload*`, and `inversify*` into per-cluster weekly PRs.
|
||||
- Dockerfile manager is enabled for `.sandcastle/Dockerfile`.
|
||||
- `dependencyDashboard: true` is set (opens a single Renovate-managed issue summarising open + queued PRs, labeled `renovate/dashboard` per PRD Q6).
|
||||
- Renovate's bump commits use `chore(deps):` (minor/patch) and `chore(deps-major):` (major) commit-message prefixes so release-please's per-package bump rules apply cleanly.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass (no executable code change; JSON config only).
|
||||
|
||||
## In scope
|
||||
|
||||
- `.github/renovate.json` — full Renovate configuration.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Verifying the first Renovate PR (SHA-pin sweep) — that happens when the GitHub App runs, not at commit time. The success criterion (all `@v<N>` pins rewritten to SHAs) is verified when Renovate's first PR merges.
|
||||
- Renovate Dependency Dashboard → `docs/work/` task integration — explicitly out of scope in the PRD.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Create `.github/renovate.json` extending presets `config:base`, `helpers:pinGitHubActionDigests`, `:separateMajorReleases`, `:automergeMinor`, `:automergePatch`; add `packageRules` grouping `@sentry/*`, `@opentelemetry/*`, `@trpc/*`, `payload*`, `inversify*` into weekly per-cluster PRs; enable Dockerfile manager for `.sandcastle/Dockerfile`; set `dependencyDashboard: true`; set `commitMessagePrefix` to enforce `chore(deps):` / `chore(deps-major):` per Conventional Commits; one commit, all gates pass.
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
id: 04-major-bump-reevaluation
|
||||
epic: ci-security-and-supply-chain
|
||||
title: Major-bump re-evaluation flow
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: scripts
|
||||
depends-on: [01-trace-schema-extensions]
|
||||
blocks: [05-trace-revalidation-workflow]
|
||||
created: 2026-05-14T18:59:12+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Extend `scripts/library-decisions/check.mjs` with a major-bump detection mode: when invoked on a Renovate PR, parse the lockfile diff to find bumped deps, classify each as major/minor/patch, and for any feature- or core-tier major bump require the trace's `lastRevalidated` field to be fresh (set today); exit non-zero with a pointer to the `evaluate-library` skill if not.
|
||||
|
||||
## Why
|
||||
|
||||
ADR-022 closes the adoption-time gate; ADR-023 closes the drift gate for major-version changes. A Renovate PR that bumps `@sentry/node` from `7.x → 8.x` is effectively a new adoption decision — the original trace may have evaluated a very different API surface and risk profile. Requiring a fresh `lastRevalidated` ensures the trace is re-walked before the bump merges. Minor and patch bumps don't trigger re-evaluation (backwards-compatible by semver contract).
|
||||
|
||||
**External dependency:** library-evaluation epic story 02 (pre-commit check script) must be complete — `scripts/library-decisions/check.mjs` must exist. That epic is marked done.
|
||||
|
||||
## Done when
|
||||
|
||||
- `check.mjs` has a new mode (invocable as `node scripts/library-decisions/check.mjs --renovate-pr`) that:
|
||||
- Detects Renovate PRs via `renovate/` branch prefix (from `GITHUB_HEAD_REF` or `--branch` arg).
|
||||
- Parses the lockfile diff (from `git diff origin/main -- pnpm-lock.yaml` or a `--diff` arg) to extract bumped deps with from/to versions.
|
||||
- Classifies each bump as major / minor / patch using semver comparison.
|
||||
- For each feature- or core-tier major bump: reads the corresponding trace from `docs/library-decisions/`, checks `lastRevalidated` equals today's ISO date. If not fresh, exits non-zero with a message referencing the `evaluate-library` skill and the trace path.
|
||||
- App-tier deps: pass unconditionally (ADR-022 exemption).
|
||||
- Non-Renovate branch: pass unconditionally (the rule is scoped to Renovate PRs only).
|
||||
- Multiple bumps in one PR: validated per-dep independently (PRD Q3).
|
||||
- Integration tests in `check.test.mjs` (or equivalent) cover: minor bump on feature-tier dep → pass; major bump + fresh `lastRevalidated` → pass; major bump + stale `lastRevalidated` → fail with clear pointer; major bump on app-tier dep → pass; patch bump in Renovate branch → pass; non-Renovate branch with major bump → pass.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `scripts/library-decisions/check.mjs` — new `--renovate-pr` mode only; existing modes unchanged.
|
||||
- `scripts/library-decisions/check.test.mjs` — integration tests for the new mode (use fixture trace files + fixture lockfile diffs; no network calls).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Auto-closing `library-policy/re-evaluation` issues when `lastRevalidated` is refreshed — Story 05 (revalidation cron) handles this.
|
||||
- The CI step that invokes this check on Renovate PRs — the script is the unit; wiring it into CI workflows is part of Story 09's guide and/or the reviewer prompt in Story 08.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Extend `scripts/library-decisions/check.mjs` with a `--renovate-pr` mode: detect Renovate branch prefix, parse lockfile diff for bumped deps, classify semver deltas, require fresh `lastRevalidated` on feature/core-tier major bumps (fail with `evaluate-library` pointer if stale), pass app-tier + non-Renovate + minor/patch unconditionally; write integration tests in `check.test.mjs` with fixture trace files and lockfile diffs covering all six cases; one commit, all gates pass.
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: 05-trace-revalidation-workflow
|
||||
epic: ci-security-and-supply-chain
|
||||
title: Trace revalidation workflow
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: scripts
|
||||
depends-on: [01-trace-schema-extensions, 04-major-bump-reevaluation]
|
||||
blocks: [09-ci-security-guide-and-docs]
|
||||
created: 2026-05-14T18:59:12+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Write `scripts/library-decisions/revalidate.mjs` — a script that walks every approved + pre-shipped trace, re-runs each trace's `verification-commands`, classifies divergence as soft or hard, and opens/updates/closes GitHub issues accordingly — then wire it into `.github/workflows/trace-revalidation-weekly.yml` (weekly cron + `workflow_dispatch`).
|
||||
|
||||
## Why
|
||||
|
||||
ADR-022 traces go stale silently when new CVEs drop or Socket picks up behavioral changes in a package post-adoption. A weekly automated revalidation creates a feedback loop: soft divergence (minor drift) surfaces as a rolling dashboard issue; hard divergence (a re-evaluation is warranted) surfaces as a per-dep `library-policy/re-evaluation` issue with the trace path + finding + re-walk handoff. No auto-edit of traces and no CI gating on main — the workflow runs in parallel, not on the critical path.
|
||||
|
||||
**External dependency:** library-evaluation epic story 02 (pre-commit check script) must be complete — `check.mjs` and the `docs/library-decisions/` fixture patterns are the prior art this script mirrors.
|
||||
|
||||
## Done when
|
||||
|
||||
- `scripts/library-decisions/revalidate.mjs` walks all traces in `docs/library-decisions/` whose `decision` field is `accepted` or `pre-shipped`; for each trace, re-runs its `verification-commands`; classifies divergence (soft: minor discrepancy from expected output; hard: finding that would change the evaluation decision); opens a rolling `library-policy/dashboard`-labeled issue for soft divergence (creates or updates a single issue); opens a `library-policy/re-evaluation`-labeled per-dep issue for hard divergence with title `re-evaluate: <package>@<version> — <finding>`, trace path, and `evaluate-library` re-walk pointer; closes open `library-policy/re-evaluation` issues whose dep has since had `lastRevalidated` refreshed; skips rejected traces entirely.
|
||||
- Integration tests use a fixture trace directory (no real `gh` CLI / no network): no-drift trace → no issue; soft-drift trace → dashboard issue created; hard-drift trace → per-dep issue with correct labels + title format; open per-dep issue already exists → no duplicate opened; `lastRevalidated` refreshed → open issue closed with comment.
|
||||
- `.github/workflows/trace-revalidation-weekly.yml` triggers on `schedule: - cron: "30 6 * * 1"` and `workflow_dispatch`; job steps: checkout, `pnpm install --frozen-lockfile`, `node scripts/library-decisions/revalidate.mjs`; permissions: `issues: write`, `contents: read` (NO `contents: write`).
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `scripts/library-decisions/revalidate.mjs` — the revalidation script.
|
||||
- `scripts/library-decisions/revalidate.test.mjs` — integration tests with fixture directory and mocked `gh` CLI surface.
|
||||
- `.github/workflows/trace-revalidation-weekly.yml` — the workflow file.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Auto-editing trace files — explicitly forbidden (no `contents: write`).
|
||||
- Auto-dispatching on `library-policy/re-evaluation` issues — human triage required (PRD out of scope).
|
||||
- CI gating on main from this workflow — main keeps deploying; revalidation runs in parallel.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Write `scripts/library-decisions/revalidate.mjs` (walk approved+pre-shipped traces, re-run `verification-commands`, classify soft/hard divergence, open/update/close issues via `gh` CLI; mock-friendly `gh` surface for tests); write `revalidate.test.mjs` integration tests with fixture traces covering: no-drift, soft-drift (dashboard issue), hard-drift (per-dep issue with correct labels+title), duplicate-issue guard, stale-issue close on refreshed `lastRevalidated`, rejected-trace skip; one commit, all gates pass.
|
||||
- [x] Create `.github/workflows/trace-revalidation-weekly.yml` (trigger: `schedule: cron: "30 6 * * 1"` + `workflow_dispatch`; steps: checkout, `pnpm install --frozen-lockfile`, `node scripts/library-decisions/revalidate.mjs`; permissions: `issues: write`, `contents: read`); one commit, all gates pass.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
id: 06-codeql-and-audit-signatures
|
||||
epic: ci-security-and-supply-chain
|
||||
title: CodeQL workflow + pnpm audit signatures
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: []
|
||||
blocks: [08-reviewer-prompt-update]
|
||||
created: 2026-05-14T18:59:12+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add two baseline GitHub-native gates: (1) a `pnpm audit signatures --audit-level=high` step in `ci.yml`'s validate job, and (2) a `.github/workflows/codeql.yml` workflow running javascript-typescript static analysis on push/PR/weekly schedule.
|
||||
|
||||
## Why
|
||||
|
||||
`pnpm audit signatures` catches tampered package signatures before they reach production — a post-install script from a compromised maintainer account would fail this check. CodeQL's javascript-typescript analysis catches common vulnerability patterns (XSS, injection, prototype pollution) that are invisible to dependency-scanning tools. Both are zero-cost on public repos and the GitHub Free plan; CodeQL's template includes a clear no-op on plans that don't support it.
|
||||
|
||||
## Done when
|
||||
|
||||
- `ci.yml`'s `validate` job includes a `pnpm audit signatures --audit-level=high` step. The step fails the job on `high` or `critical` severity signature failures.
|
||||
- `.github/workflows/codeql.yml` exists; triggers: `push: branches: [main]`, `pull_request`, and `schedule: - cron: "0 2 * * 3"` (Wednesday 02:00 UTC, staggered from the trace-revalidation cron); language: `javascript-typescript`; uses default queries. Includes a comment noting that CodeQL on private repos requires GitHub Advanced Security (consumer-toggleable per PRD constraint).
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass (no executable code change; CI config only).
|
||||
|
||||
## In scope
|
||||
|
||||
- `.github/workflows/ci.yml` — one new `pnpm audit signatures` step in `validate` job.
|
||||
- `.github/workflows/codeql.yml` — new workflow file.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Configuring GitHub branch protection to require CodeQL as a status check — consumer-facing instruction deferred to Story 09's guide.
|
||||
- OSSF Scorecard — explicitly out of PRD scope.
|
||||
- StepSecurity Harden Runner — explicitly out of PRD scope.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `pnpm audit signatures --audit-level=high` as a step in `ci.yml`'s `validate` job; one commit, all gates pass.
|
||||
- [x] Create `.github/workflows/codeql.yml` (language: `javascript-typescript`; triggers: push to main, pull_request, weekly schedule Wednesday 02:00 UTC; default queries; consumer note about GitHub Advanced Security requirement for private repos); one commit, all gates pass.
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
id: 07-gitleaks-precommit
|
||||
epic: ci-security-and-supply-chain
|
||||
title: Gitleaks pre-commit hook
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: []
|
||||
blocks: [09-ci-security-guide-and-docs]
|
||||
created: 2026-05-14T18:59:12+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add `gitleaks protect --staged --redact` as a step in `.husky/pre-commit` and ship a `.gitleaks.toml` allowlist that covers test-fixture patterns in `__seeds__/**`, so a commit containing a known secret pattern is blocked locally before it reaches the remote.
|
||||
|
||||
## Why
|
||||
|
||||
Developer accidents (pasting tokens into config, seeding test fixtures with real-looking keys) are the most common secret-leak vector. A pre-commit hook stops the leak at the earliest possible point — before the secret is ever pushed. GitHub native push protection is the second line of defense (documented in Story 09's guide); the hook is the first. The `__seeds__/**` allowlist prevents false positives from test fixtures that deliberately use token-shaped strings as dummy data.
|
||||
|
||||
## Done when
|
||||
|
||||
- `.husky/pre-commit` has a `gitleaks protect --staged --redact` step that runs before the existing state-sync guard (or after — order between guards doesn't matter, both must run).
|
||||
- `.gitleaks.toml` exists at repo root with at minimum one allowlist rule scoping `__seeds__/**` test fixtures (using `paths` or `allowlist.paths` depending on the gitleaks version).
|
||||
- A smoke test (bash script or vitest) pipes a staged commit containing a Stripe-style test key (`sk_test_...`) through the hook and asserts non-zero exit code. The smoke test is documented in the story's Done-when but may live as a manual verification step given gitleaks requires a binary; include instructions in `docs/guides/ci-security.md` (Story 09) for consumers to verify locally.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `.husky/pre-commit` — new `gitleaks` step.
|
||||
- `.gitleaks.toml` — allowlist config.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Installing `gitleaks` as a project devDependency — consumers install it via their OS package manager or `brew`; the hook exits gracefully with a warning if `gitleaks` is not found in `$PATH` (to avoid blocking developers who haven't installed it yet, while still enforcing for those who have).
|
||||
- GitHub native push protection configuration — consumer-facing instruction deferred to Story 09's guide.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `gitleaks protect --staged --redact` step to `.husky/pre-commit` (exit-gracefully if `gitleaks` not in `$PATH`); create `.gitleaks.toml` at repo root with `__seeds__/**` allowlist for test-fixture patterns; one commit, all gates pass.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
id: 08-reviewer-prompt-update
|
||||
epic: ci-security-and-supply-chain
|
||||
title: Sandcastle reviewer prompt update
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: [02-socket-integration, 06-codeql-and-audit-signatures]
|
||||
blocks: [09-ci-security-guide-and-docs]
|
||||
created: 2026-05-14T18:59:12+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Extend `.sandcastle/reviewer.prompt.md` with a "CI security checks" section that instructs the reviewer agent to read Socket CI output and CodeQL findings via `gh run view`, and reject the slice with specific notes if any `critical` Socket finding or `error`-severity CodeQL finding is present.
|
||||
|
||||
## Why
|
||||
|
||||
The sandcastle reviewer is the single composable gate for agent PRs (ADR-019 constraint). Without an explicit section, an agent reviewer has no machine-readable instruction to check Socket + CodeQL outputs and may approve a slice that introduced a flagged dependency or a static-analysis error. Landing this after Stories 02 + 06 ensures the reviewer references gates that actually exist in CI.
|
||||
|
||||
**External dependency:** library-evaluation epic story 06 (sandcastle reviewer prompt) must be complete — the reviewer prompt must exist and have the library-trace check section that this story composes with. That epic is marked done.
|
||||
|
||||
## Done when
|
||||
|
||||
- `.sandcastle/reviewer.prompt.md` has a "CI security checks" section added after the existing library-trace check section.
|
||||
- The section instructs the reviewer to: (a) run `gh run view <run-id> --log` (or equivalent) for the PR's check suite; (b) scan the output for Socket findings of severity `critical` — if found, reject with notes naming the finding and referencing the failure-mode hierarchy in `docs/guides/ci-security.md`; (c) scan the output for CodeQL findings of severity `error` — same rejection pattern.
|
||||
- The reviewer composes these checks with the existing library-trace presence check (both must pass for approval).
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass (prose-only change; no executable code).
|
||||
|
||||
## In scope
|
||||
|
||||
- `.sandcastle/reviewer.prompt.md` — new "CI security checks" section only; existing sections unchanged.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Automated tests for the reviewer prompt — it's a prose runbook for an agent; success is verified manually (PRD testing decisions).
|
||||
- Extending the reviewer for `pnpm audit signatures` step failures — those surface as standard CI job failures, already handled by the reviewer's existing "all CI checks must pass" instruction.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Extend `.sandcastle/reviewer.prompt.md` with a "CI security checks" section after the library-trace check: instruct the reviewer to read `gh run view` output for Socket `critical` findings and CodeQL `error` findings, reject on either with notes naming the finding and citing `docs/guides/ci-security.md` failure-mode hierarchy; one commit, all gates pass.
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
id: 09-ci-security-guide-and-docs
|
||||
epic: ci-security-and-supply-chain
|
||||
title: CI security guide + CLAUDE.md
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: docs
|
||||
depends-on:
|
||||
[
|
||||
01-trace-schema-extensions,
|
||||
02-socket-integration,
|
||||
03-renovate-adoption,
|
||||
04-major-bump-reevaluation,
|
||||
05-trace-revalidation-workflow,
|
||||
06-codeql-and-audit-signatures,
|
||||
07-gitleaks-precommit,
|
||||
08-reviewer-prompt-update,
|
||||
]
|
||||
blocks: []
|
||||
created: 2026-05-14T18:59:12+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Write `docs/guides/ci-security.md` — the human reading-room for the four-pillar stack — and add a Key Conventions bullet to `CLAUDE.md` pointing agents and developers to ADR-023 + the guide.
|
||||
|
||||
## Why
|
||||
|
||||
Each prior story lands a machine-enforced layer, but no single document explains the composed system to a developer or consumer who hasn't read all nine stories. `docs/guides/ci-security.md` fills that gap: it explains the four pillars, the failure-mode hierarchy, what settings are consumer-toggleable, and gives two worked examples so the mental model is concrete. The CLAUDE.md bullet ensures the enforcement stack is discoverable during every agent session via the startup context.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/guides/ci-security.md` exists and covers:
|
||||
- Overview of the four pillars (Renovate + Action SHA pinning, Socket, trace revalidation, GitHub-native gates).
|
||||
- Per-pillar section with what the gate does, what it catches, and how to toggle it in a downstream consumer repo.
|
||||
- Failure-mode hierarchy table (mirroring ADR-023 §5): pillar, trigger condition, action, label, who resolves.
|
||||
- Consumer-toggleable settings list: GitHub native push protection, Socket GitHub App install, branch protection rules requiring `library-policy/*`-labeled checks before merge.
|
||||
- Two worked examples: (a) a passing Renovate minor-bump PR (gates pass, auto-merges); (b) a blocked major-bump PR (Renovate opens PR → `check.mjs` requires `lastRevalidated` refresh → agent re-walks `evaluate-library` → trace updated → PR unblocked) + a hard-divergence revalidation issue (weekly cron finds Socket-flagged dep → per-dep `library-policy/re-evaluation` issue opened → agent closes issue after re-walk).
|
||||
- Socket GitHub App install instructions for consumers.
|
||||
- `gitleaks` installation instructions for developers (OS package manager / brew; hook exits gracefully if binary absent).
|
||||
- Note that CodeQL requires GitHub Advanced Security on private repos.
|
||||
- `CLAUDE.md` Key Conventions section has a bullet: _"CI security + supply-chain enforcement: Renovate for bumps + Action SHA pinning, Socket for supply-chain behavior, weekly trace revalidation, CodeQL + audit signatures + gitleaks. See ADR-023 + `docs/guides/ci-security.md`."_
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass (docs + CLAUDE.md; no executable code).
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/guides/ci-security.md` — new guide file.
|
||||
- `CLAUDE.md` — one bullet addition to Key Conventions.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Configuring GitHub branch protection rules — documented as consumer action, not a tracked file change.
|
||||
- Installing the Socket GitHub App — documented as consumer action in the guide; no config file change.
|
||||
- Backfilling existing traces with `last-revalidated` — handled by the first revalidation cron run (Story 05).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Write `docs/guides/ci-security.md` with: four-pillar overview, per-pillar sections, failure-mode hierarchy table (ADR-023 §5), consumer-toggleable settings list, Socket GitHub App + gitleaks install instructions, CodeQL note for private repos, two worked examples (passing minor-bump PR; blocked major-bump PR + hard-divergence revalidation issue); one commit, all gates pass.
|
||||
- [x] Add CI security Key Conventions bullet to `CLAUDE.md` referencing ADR-023 + `docs/guides/ci-security.md`; one commit, all gates pass.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
id: ci-security-and-supply-chain
|
||||
prd: docs/work/prds/ci-security-and-supply-chain.prd.md
|
||||
title: CI security + supply-chain enforcement stack
|
||||
type: epic
|
||||
status: done
|
||||
features: [scripts, tooling, docs]
|
||||
created: 2026-05-14T00:00:00Z
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Implement a four-pillar CI security stack — Renovate-managed bumps + Action SHA pinning, Socket-based supply-chain-behavior detection, continuous trace revalidation extending ADR-022, and baseline GitHub-native gates — composed via a single failure-mode hierarchy that the sandcastle reviewer prompt enforces machine-readably for agent-driven PRs. Codifies ADR-023.
|
||||
|
||||
## Why
|
||||
|
||||
The repo's security posture has zero security tooling. ADR-022 + the library-evaluation epic close the adoption-time gate for new dependencies but not the drift gate. Six post-adoption threats remain uncovered: CVE disclosures, supply-chain behavior compromise, maintainer-account compromise, GitHub Actions supply-chain (major-tag pinning), license drift, and EU-residency drift. This epic closes all six via the four-pillar stack.
|
||||
|
||||
## Stories
|
||||
|
||||
- [x] [01 — Trace schema extensions (socketRisk + lastRevalidated)](01-trace-schema-extensions/_story.md)
|
||||
- [x] [02 — Socket integration (skill + CI)](02-socket-integration/_story.md)
|
||||
- [x] [03 — Renovate adoption](03-renovate-adoption/_story.md)
|
||||
- [x] [04 — Major-bump re-evaluation flow](04-major-bump-reevaluation/_story.md)
|
||||
- [x] [05 — Trace revalidation workflow](05-trace-revalidation-workflow/_story.md)
|
||||
- [x] [06 — CodeQL workflow + pnpm audit signatures](06-codeql-and-audit-signatures/_story.md)
|
||||
- [x] [07 — Gitleaks pre-commit hook](07-gitleaks-precommit/_story.md)
|
||||
- [x] [08 — Sandcastle reviewer prompt update](08-reviewer-prompt-update/_story.md)
|
||||
- [x] [09 — CI security guide + CLAUDE.md](09-ci-security-guide-and-docs/_story.md)
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
id: 01-land-operator-checklist
|
||||
epic: compliance-docs-scaffolds
|
||||
title: Land operator-checklist.md verbatim
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: ~
|
||||
depends-on: []
|
||||
blocks: [02-refresh-operator-checklist]
|
||||
created: 2026-05-20T12:00:00Z
|
||||
updated: 2026-05-20T12:57:04.290Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Track the existing untracked `docs/guides/operator-checklist.md` in git without altering its content. Establishes it as the baseline before the refresh story rewrites sections.
|
||||
|
||||
## Why
|
||||
|
||||
`docs/guides/operator-checklist.md` has been untracked since the ADR-022/023 work. The PRD (Q5) calls for landing it verbatim as a separate commit so the "what existed" diff is legible for review, distinct from the "what changed" diff that story 02 produces.
|
||||
|
||||
## Done when
|
||||
|
||||
- `git status` shows `docs/guides/operator-checklist.md` as tracked (no longer `??`).
|
||||
- File content is identical to its untracked state — no edits.
|
||||
- Commit type is `chore(docs)`.
|
||||
- `pnpm lint && pnpm typecheck && pnpm test && pnpm conformance && pnpm fallow:audit` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- Staging and committing `docs/guides/operator-checklist.md` verbatim.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Any content changes to the file (story 02).
|
||||
- Any other files.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Stage and commit `docs/guides/operator-checklist.md` verbatim as `chore(docs): track operator-checklist.md verbatim`; no content edits — the file is committed exactly as found in the working tree.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
id: 02-refresh-operator-checklist
|
||||
epic: compliance-docs-scaffolds
|
||||
title: Refresh operator-checklist.md for ADR-024 and ADR-025
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: ~
|
||||
depends-on: [01-land-operator-checklist]
|
||||
blocks: []
|
||||
created: 2026-05-20T12:00:00Z
|
||||
updated: 2026-05-20T12:08:20.429Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add two new sections to `docs/guides/operator-checklist.md` covering ADR-024 (analytics backend wiring) and ADR-025 (compliance directory, drift gate, retention purge scheduling, and sub-processors file). Preserve all existing ADR-022/023 content.
|
||||
|
||||
## Why
|
||||
|
||||
The operator checklist predates ADR-024 (analytics) and ADR-025 (the compliance epics). A template operator following the checklist today would miss compliance directory setup, the drift CI gate, and the retention purge job — all of which require a deliberate operator action.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/guides/operator-checklist.md` contains a section for ADR-024 operator actions: note that the analytics backend is consumer-chosen + consumer-wired, and that the operator should decide whether to wire a vendor via `/evaluate-library`.
|
||||
- The file contains a section for ADR-025 operator actions: `compliance/*.yml` as committed audit evidence; compliance drift gate running in pre-commit + CI; retention purge job scheduling per `custom.retention`; `compliance/sub-processors.manual.yml` hand-authored for non-npm vendors.
|
||||
- All existing ADR-022/023 content is preserved unchanged.
|
||||
- Commit type is `docs(compliance)`.
|
||||
- `pnpm lint && pnpm typecheck && pnpm test && pnpm conformance && pnpm fallow:audit` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- Adding ADR-024 operator actions section to `docs/guides/operator-checklist.md`.
|
||||
- Adding ADR-025 operator actions section to `docs/guides/operator-checklist.md`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Rewriting or restructuring the existing ADR-022/023 sections.
|
||||
- Any other files.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add ADR-024 (analytics backend is consumer-chosen; note `/evaluate-library` decision process) and ADR-025 (commit `compliance/*.yml` as audit evidence, verify compliance drift gate in pre-commit + CI, schedule retention purge job per `custom.retention`, hand-author `compliance/sub-processors.manual.yml` for non-npm vendors) operator action sections to `docs/guides/operator-checklist.md`, preserving all existing content; commit as `docs(compliance): refresh operator-checklist with ADR-024 and ADR-025 actions`.
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
id: 03-policy-templates
|
||||
epic: compliance-docs-scaffolds
|
||||
title: Write seven policy templates in docs/compliance/templates/
|
||||
type: user-story
|
||||
status: done
|
||||
feature: ~
|
||||
depends-on: []
|
||||
blocks: [04-pre-launch-compliance-checklist, 05-compliance-overview]
|
||||
created: 2026-05-20T12:00:00Z
|
||||
updated: 2026-05-20T12:21:53.062Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Create `docs/compliance/templates/` and populate it with seven `*.template.md` files: two anchored templates (`incident-runbook`, `dsr-procedure`) cross-referencing real shipped features, and five skeleton templates (`backup-policy`, `password-policy`, `device-policy`, `onboarding`, `offboarding`) with "not code-enforced" banners. All templates use the `[FILL IN: <description>]` marker convention throughout.
|
||||
|
||||
## Why
|
||||
|
||||
A downstream consumer preparing for a DPA audit currently writes organizational policy documents from a blank page. These templates reduce that to copy-and-fill work. The anchored templates are substantive because the template ships the relevant code (DSR endpoints, audit channel, Sentry alerting, security headers, rate-limit) — they reference real ADR numbers and `pnpm` commands so the procedure matches the shipped system. The skeleton templates provide the document structure; the consumer supplies the organization-specific content.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/compliance/templates/` directory exists with exactly 7 files: `incident-runbook.template.md`, `dsr-procedure.template.md`, `backup-policy.template.md`, `password-policy.template.md`, `device-policy.template.md`, `onboarding.template.md`, `offboarding.template.md`.
|
||||
- Each template has YAML frontmatter with `status: template` and `playbook-section: <n>`.
|
||||
- Anchored templates (`incident-runbook`, `dsr-procedure`) each reference at least one ADR number and one `pnpm` command or shipped interface/endpoint; each still contains `[FILL IN:]` markers for org-specific values (contacts, SLA targets, etc.).
|
||||
- Skeleton templates all open with the "not code-enforced" banner; `password-policy.template.md`'s banner cites ADR-025's explicit deferral of MFA + password policy + lockout by ADR number.
|
||||
- `grep -rn '\[FILL IN:' docs/compliance/templates/` returns hits in every template.
|
||||
- Every relative Markdown link in the new files resolves to an existing file.
|
||||
- `pnpm lint && pnpm typecheck && pnpm test && pnpm conformance && pnpm fallow:audit` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/compliance/templates/incident-runbook.template.md` — breach detection → triage → containment → notification (GDPR Art. 33 72h / DPA 24h) → post-mortem; cross-references ADR-018 (audit channel), ADR-014 (Sentry alerting), Epic C (security-headers + rate-limit surfaces).
|
||||
- `docs/compliance/templates/dsr-procedure.template.md` — DSR receipt → validation → fulfilment → recording; cross-references Epic B DSR endpoints (`/api/gdpr/*`), `core-dsr` interfaces, audit `CONSENT_*`/`RESTRICT` actions, `compliance/data-map.yml` (Epic A).
|
||||
- `docs/compliance/templates/backup-policy.template.md` — skeleton with banner.
|
||||
- `docs/compliance/templates/password-policy.template.md` — skeleton with banner; banner cites ADR-025 MFA/password/lockout deferral.
|
||||
- `docs/compliance/templates/device-policy.template.md` — skeleton with banner.
|
||||
- `docs/compliance/templates/onboarding.template.md` — skeleton with banner.
|
||||
- `docs/compliance/templates/offboarding.template.md` — skeleton with banner.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Filling in org-specific values — the `[FILL IN:]` markers are intentionally left for the consumer.
|
||||
- Any changes to existing compliance guides (`dsr.md`, `consent.md`, etc.).
|
||||
- A CI gate enforcing no `[FILL IN:]` in `compliance/` (deferred).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Create `docs/compliance/templates/` and write `incident-runbook.template.md` + `dsr-procedure.template.md`: both anchored with YAML frontmatter (`status: template`, `playbook-section`), procedure skeleton cross-referencing real ADRs/commands/interfaces/endpoints, and `[FILL IN:]` markers for org-specific values; commit as `docs(compliance): add anchored policy templates (incident-runbook, dsr-procedure)`.
|
||||
- [x] Write `backup-policy.template.md`, `password-policy.template.md`, `device-policy.template.md`, `onboarding.template.md`, and `offboarding.template.md` in `docs/compliance/templates/`: each with YAML frontmatter, the "not code-enforced" banner (password-policy's banner cites ADR-025 MFA/lockout deferral by number), and `[FILL IN:]` markers throughout; commit as `docs(compliance): add skeleton policy templates (backup, password, device, onboarding, offboarding)`.
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: 04-pre-launch-compliance-checklist
|
||||
epic: compliance-docs-scaffolds
|
||||
title: Write pre-launch compliance checklist
|
||||
type: user-story
|
||||
status: done
|
||||
feature: ~
|
||||
depends-on: [03-policy-templates]
|
||||
blocks: [05-compliance-overview]
|
||||
created: 2026-05-20T12:00:00Z
|
||||
updated: 2026-05-20T12:37:24.995Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Create `docs/guides/pre-launch-compliance-checklist.md` as a two-column Markdown table mapping every playbook obligation (drawn from playbook §19 + the 22 sections) to its coverage status: "Shipped by template" (with verification command), "Consumer responsibility", or "Infra responsibility". The table operationalizes ADR-025's three-way coverage split into a checkable launch gate.
|
||||
|
||||
## Why
|
||||
|
||||
A launching team currently has no single answer to "are we compliant enough to ship to a paying customer?" The playbook §19 is generic; nothing maps it to this template's concrete features (`pnpm compliance:emit-all --check`, `core-audit`, DSR endpoints) or explicitly flags which obligations are on the consumer vs the infra vs already handled. This checklist provides that clarity and produces verification evidence on demand for compliance officers.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/guides/pre-launch-compliance-checklist.md` exists and contains a two-column table.
|
||||
- Rows are grouped by playbook section (Infrastructure, Data, Application, Secrets, Sub-Processors, Logging, Breach, DSR, Backup, SDLC, Workforce, Legal, Documentation).
|
||||
- Every "Shipped by template" row names a runnable verification command (e.g. `pnpm compliance:emit-all --check`, `pnpm conformance`, securityheaders.com scan).
|
||||
- Every "Consumer responsibility" and "Infra responsibility" row is explicitly labelled as such.
|
||||
- The file links to `docs/guides/compliance-overview.md` and to relevant templates in `docs/compliance/templates/`.
|
||||
- All 22 playbook sections are represented.
|
||||
- Every relative Markdown link resolves to an existing file.
|
||||
- Commit type is `docs(compliance)`.
|
||||
- `pnpm lint && pnpm typecheck && pnpm test && pnpm conformance && pnpm fallow:audit` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/guides/pre-launch-compliance-checklist.md` — the two-column launch gate table.
|
||||
- Links outward to `compliance-overview.md` and `docs/compliance/templates/*.template.md`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Machine-enforceable CI gate for this checklist (deferred).
|
||||
- Changes to any existing guide files.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Write `docs/guides/pre-launch-compliance-checklist.md` as a two-column table covering all 22 playbook sections (Infrastructure, Data, Application, Secrets, Sub-Processors, Logging, Breach, DSR, Backup, SDLC, Workforce, Legal, Documentation), labelling each obligation as "Shipped by template" (with inline verification command), "Consumer responsibility", or "Infra responsibility", and linking to `compliance-overview.md` + relevant templates; commit as `docs(compliance): add pre-launch compliance checklist`.
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
id: 05-compliance-overview
|
||||
epic: compliance-docs-scaffolds
|
||||
title: Write compliance-overview.md hub
|
||||
type: user-story
|
||||
status: done
|
||||
feature: ~
|
||||
depends-on: [03-policy-templates, 04-pre-launch-compliance-checklist]
|
||||
blocks: [06-doc-wiring]
|
||||
created: 2026-05-20T12:00:00Z
|
||||
updated: 2026-05-20T12:47:06.204Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Create `docs/guides/compliance-overview.md` as the single navigational entry point for compliance in this template. It maps each of the 22 playbook sections to the ADR / guide / template / epic that covers it, names ADR-025's deferrals and consumer/infra-scope items explicitly, and links outward to every compliance guide, ADR, and template.
|
||||
|
||||
## Why
|
||||
|
||||
After four epics, compliance documentation sprawls across `docs/compliance/`, `docs/guides/` (8+ files), `docs/decisions/` (6 ADRs), and root `compliance/`. An auditor, new engineer, or AI agent asked "is feature X compliant?" has no entry point and must reverse-engineer the structure from grep. ADR-025 designated this hub as the solution; it must be written last so it can link to finished templates + checklist.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/guides/compliance-overview.md` exists.
|
||||
- The file maps all 22 playbook sections; every row points at a real ADR, guide path, template path, or epic that covers it.
|
||||
- A closing summary restates ADR-025's deferrals (RBAC, MFA, breach-detection, GDPR Art. 22) and the consumer/infra-scope items (EU region, TLS, MDM, legal instruments).
|
||||
- The file links one-directionally to `docs/guides/pre-launch-compliance-checklist.md`, `docs/compliance/templates/*.template.md`, all relevant ADRs (`docs/decisions/`), and all relevant guides.
|
||||
- Every relative Markdown link resolves to an existing file.
|
||||
- Commit type is `docs(compliance)`.
|
||||
- `pnpm lint && pnpm typecheck && pnpm test && pnpm conformance && pnpm fallow:audit` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/guides/compliance-overview.md` — the 22-section hub with outbound links and deferrals summary.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Editing any existing ADR, guide, or template (one-directional only).
|
||||
- Duplicating ADR-025's rationale — the overview is the navigational map, not the decision record.
|
||||
- Replacing `docs/compliance/README.md` as the hub — README stays scoped to generator schema reference.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Write `docs/guides/compliance-overview.md` mapping all 22 playbook sections to their covering ADR/guide/template/epic, with a closing deferrals summary (RBAC, MFA, breach-detection, GDPR Art. 22, EU region, TLS, MDM, legal instruments) and outbound links to all referenced files; verify every relative link resolves; commit as `docs(compliance): add compliance-overview hub`.
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: 06-doc-wiring
|
||||
epic: compliance-docs-scaffolds
|
||||
title: Wire compliance docs into CLAUDE.md, README, and glossary
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: ~
|
||||
depends-on: [05-compliance-overview]
|
||||
blocks: []
|
||||
created: 2026-05-20T12:00:00Z
|
||||
updated: 2026-05-20T12:52:53.917Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Complete the one-directional wiring of Epic D's new documents into the three shared navigation surfaces: add a "Policy templates" section to `docs/compliance/README.md`, add four new glossary entries to `docs/glossary.md`, and add a `compliance-overview.md` pointer to CLAUDE.md's "Read First" section.
|
||||
|
||||
## Why
|
||||
|
||||
Without these changes, `compliance-overview.md` is orphaned from the primary AI navigation entry point (CLAUDE.md), the policy templates have no discovery path in `docs/compliance/`, and the `[FILL IN:]` marker convention has no canonical glossary definition. The PRD specifies these as the final wiring step, after all new documents exist so links resolve.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/compliance/README.md` has a "Policy templates" section that explains: the `docs/compliance/templates/` directory, the copy-to-`compliance/` workflow, the `[FILL IN:]` convention, and the `grep -rn '\[FILL IN:' compliance/` verification one-liner.
|
||||
- `docs/glossary.md` has four new entries: `pre-launch compliance checklist`, `compliance overview`, `fill-in template`, `[FILL IN:] marker`.
|
||||
- CLAUDE.md "Read First" lists `compliance-overview.md` (with path and one-line description matching the existing entries' style).
|
||||
- No existing content in any of the three files is removed or modified.
|
||||
- Every relative Markdown link added resolves to an existing file.
|
||||
- `pnpm lint && pnpm typecheck && pnpm test && pnpm conformance && pnpm fallow:audit` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/compliance/README.md` — add "Policy templates" section.
|
||||
- `docs/glossary.md` — add 4 entries.
|
||||
- `CLAUDE.md` — add `compliance-overview.md` pointer to "Read First".
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Any changes to `docs/guides/compliance-overview.md` or the templates (they are complete by the time this story runs).
|
||||
- Any changes to the existing compliance guides (`dsr.md`, `consent.md`, etc.).
|
||||
- Broadening `docs/compliance/README.md` beyond the generator schema + policy template scope.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add a "Policy templates" section to `docs/compliance/README.md` (explaining the `docs/compliance/templates/` directory, copy-to-`compliance/` workflow, `[FILL IN:]` convention, and the `grep -rn '\[FILL IN:' compliance/` one-liner) and add four glossary entries (`pre-launch compliance checklist`, `compliance overview`, `fill-in template`, `[FILL IN:] marker`) to `docs/glossary.md`; commit as `docs(compliance): document policy template convention in README and glossary`.
|
||||
- [x] Add `docs/guides/compliance-overview.md` to the "Read First" section of `CLAUDE.md` with a one-line description matching the existing entries' style; commit as `docs(core): add compliance-overview.md to CLAUDE.md Read First`.
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
id: compliance-docs-scaffolds
|
||||
prd: docs/work/prds/compliance-docs-scaffolds.prd.md
|
||||
title: Compliance docs scaffolds — Epic D of ADR-025
|
||||
type: epic
|
||||
status: done
|
||||
features: []
|
||||
created: 2026-05-20T12:00:00Z
|
||||
updated: 2026-05-20T12:52:53.917Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Ship the compliance documentation layer: seven copy-and-fill policy templates, a template-tailored pre-launch compliance checklist, a single `compliance-overview.md` hub mapping the 22 playbook sections, and a refreshed `operator-checklist.md`. Pure documentation — no code, no manifests, no conformance rules.
|
||||
|
||||
## Why
|
||||
|
||||
Epics A–C shipped the compliance machinery (PII manifests + retention + sub-processor generators, DSR + consent + cookie banner, security headers + rate-limit + SBOM). Epic D fills three remaining documentation gaps: no human-authored policy artifacts, no single launch gate, and no compliance map. A downstream consumer currently writes these from a blank page. ADR-025 settled the strategy; this epic is the implementation.
|
||||
|
||||
## Stories
|
||||
|
||||
- [x] [01 — Land operator-checklist.md verbatim](01-land-operator-checklist/_story.md)
|
||||
- [x] [02 — Refresh operator-checklist.md for ADR-024 and ADR-025](02-refresh-operator-checklist/_story.md)
|
||||
- [x] [03 — Write seven policy templates](03-policy-templates/_story.md)
|
||||
- [x] [04 — Write pre-launch compliance checklist](04-pre-launch-compliance-checklist/_story.md)
|
||||
- [x] [05 — Write compliance-overview.md hub](05-compliance-overview/_story.md)
|
||||
- [x] [06 — Wire compliance docs into CLAUDE.md, README, and glossary](06-doc-wiring/_story.md)
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
id: 01-pii-retention-type-primitives
|
||||
epic: compliance-manifests-pii-retention-subprocessors
|
||||
title: PII and retention type primitives in core-shared
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: []
|
||||
blocks:
|
||||
[
|
||||
02-eslint-rule-pii-declaration-complete,
|
||||
03-adr-022-amendment-and-evaluate-library-skill,
|
||||
04-retention-purge-job,
|
||||
05-backfill-template-collections,
|
||||
06-compliance-generator-scripts,
|
||||
]
|
||||
created: 2026-05-18T17:52:09Z
|
||||
updated: 2026-05-18T18:29:19.373Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Produce the TypeScript type primitives (`PiiCategory`, `DataProcessingPurpose`, `RetentionAction`, `RetentionTrigger`, `FieldRetention`, `FieldPii`, `CollectionRetention`, `PurgeSchedule`, `PAYLOAD_AUTH_PII_DEFAULTS`) in `core-shared/payload/` plus the ambient module declaration that extends Payload's `Field` and `CollectionConfig` custom fields to be typed. These are the "manifest" for the whole epic — everything else depends on them existing and compiling.
|
||||
|
||||
## Why
|
||||
|
||||
All downstream stories need the type contracts before they can compile. Landing these first means every subsequent story gets full TypeScript coverage on Payload config files, and the ESLint rule has a known schema to validate against.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-shared/src/payload/pii-types.ts` exports `PiiCategory`, `DataProcessingPurpose`, `RetentionTrigger`, `RetentionAction`, `FieldRetention`, `FieldPii`, and `PAYLOAD_AUTH_PII_DEFAULTS` (with `null` for credential fields: `password`, `salt`, `hash`, `resetPasswordToken`, `resetPasswordExpiration`, `loginAttempts`, `lockUntil`, `apiKey`, `apiKeyIndex`).
|
||||
- `packages/core-shared/src/payload/retention-types.ts` exports `PurgeSchedule` and `CollectionRetention`.
|
||||
- `packages/core-shared/src/payload/payload-custom-ambient.d.ts` augments the `payload` module to type `Field.custom.pii?: FieldPii` and `CollectionConfig.custom.retention?: CollectionRetention` / `CollectionConfig.custom.authPii?: Record<string, FieldPii | null>`.
|
||||
- Both modules are re-exported from the `core-shared` barrel (or a `payload` sub-barrel).
|
||||
- Vitest tests cover: `@ts-expect-error` on malformed `FieldPii` (missing required fields), `PAYLOAD_AUTH_PII_DEFAULTS` structure (credential fields are `null`, `email` is non-null with correct shape).
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-shared/src/payload/pii-types.ts`
|
||||
- `packages/core-shared/src/payload/retention-types.ts`
|
||||
- `packages/core-shared/src/payload/payload-custom-ambient.d.ts`
|
||||
- Vitest tests for both type files.
|
||||
- Barrel export additions.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- ESLint rule (Story 02).
|
||||
- Retention purge job (Story 04).
|
||||
- Collection backfill (Story 05).
|
||||
- Generator scripts (Story 06).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `pii-types.ts`, `retention-types.ts`, and `payload-custom-ambient.d.ts` to `packages/core-shared/src/payload/` — complete type contracts per the PRD, ambient Payload module augmentation, `PAYLOAD_AUTH_PII_DEFAULTS` constant with `null` for credential fields, vitest tests verifying shape and defaults, barrel exports — all gates pass on this commit.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: 02-eslint-rule-pii-declaration-complete
|
||||
epic: compliance-manifests-pii-retention-subprocessors
|
||||
title: Add pii-declaration-must-be-complete ESLint rule to core-eslint
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-eslint
|
||||
depends-on: [01-pii-retention-type-primitives]
|
||||
blocks: [05-backfill-template-collections]
|
||||
created: 2026-05-18T17:52:09Z
|
||||
updated: 2026-05-18T18:35:43.840Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add the `conformance/pii-declaration-must-be-complete` rule to `@repo/core-eslint` at warn severity. The rule detects `custom: { pii: { ... } }` blocks in Payload collection/field TypeScript files and warns when any required sub-field (`category`, `purpose`, `exportable`, `restrictable`) is missing.
|
||||
|
||||
## Why
|
||||
|
||||
Provides sub-second editor and CI feedback when a developer partially declares a `custom.pii` block. Without this rule, a missing `exportable: false` could silently survive into `compliance/data-map.yml` and appear incorrectly in an audit report.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-eslint/rules/pii-declaration-must-be-complete.js` exists and passes RuleTester fixtures.
|
||||
- Fixtures cover: complete `custom.pii` → no warning; `category` missing → warn; `purpose` missing → warn; `exportable` missing → warn; `restrictable` missing → warn; non-pii `custom` block → no-op; malformed/non-object `custom.pii` → no-op.
|
||||
- Rule registered in `plugin.js` and `base.js` at `"warn"` severity.
|
||||
- ESLint rule count in `docs/guides/conformance-quickref.md` and `CLAUDE.md` updated from 7 to 8.
|
||||
- `pnpm lint` exercises the rule; all gates pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-eslint/rules/pii-declaration-must-be-complete.js` + RuleTester test file.
|
||||
- `packages/core-eslint/plugin.js` + `packages/core-eslint/base.js` — rule registration at warn.
|
||||
- `docs/guides/conformance-quickref.md` + `CLAUDE.md` conformance rule count bump.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Auto-fix path — warn only, no `--fix`.
|
||||
- Migration of existing collection files — that is Story 05's job.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `pii-declaration-must-be-complete.js` rule to `@repo/core-eslint` — AST rule detecting incomplete `custom.pii` blocks in Payload config files, RuleTester fixtures (complete passes, each missing required field warns, non-pii custom block is no-op), register in `plugin.js` + `base.js` at `"warn"`, update conformance rule count in `conformance-quickref.md` and `CLAUDE.md` (7 → 8) — all gates pass on this commit.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
id: 03-adr-022-amendment-and-evaluate-library-skill
|
||||
epic: compliance-manifests-pii-retention-subprocessors
|
||||
title: ADR-022 amendment for sub-processor fields and evaluate-library skill update
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: [01-pii-retention-type-primitives]
|
||||
blocks: []
|
||||
created: 2026-05-18T17:52:09Z
|
||||
updated: 2026-05-18T18:46:05.473Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Amend ADR-022 to add the discriminated-union sub-processor fields to every library trace frontmatter (`is-sub-processor`, `processes-pii`, and conditionally `data-sent`, `region`, `dpa-signed`, `sccs-required`, `contact`). Update `.claude/skills/evaluate-library/SKILL.md` to prompt for these new fields during trace authoring. Backfill existing `docs/library-decisions/*.md` traces so they comply with the amended schema.
|
||||
|
||||
## Why
|
||||
|
||||
Without the skill update, a developer running `/evaluate-library` today produces a trace missing the sub-processor fields, causing `emit-sub-processors.mjs` (Story 06) to silently skip it. Making the skill ask the questions in one pass prevents traces that require a later backfill amendment.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/decisions/ADR-022.md` documents the discriminated-union sub-processor frontmatter specification: `is-sub-processor: boolean`, `processes-pii: boolean`, and the 5 conditional required fields when `is-sub-processor: true`.
|
||||
- `.claude/skills/evaluate-library/SKILL.md` updated with two new prompts ("is this library a sub-processor?" / "does it process PII in-process?"), conditional prompt for the 5 required sub-processor fields, and an updated trace frontmatter template.
|
||||
- All existing `docs/library-decisions/*.md` traces gain `is-sub-processor` + `processes-pii` fields (with `false` / `false` as the baseline for non-sub-processors, or correct values where applicable).
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/decisions/ADR-022.md` — sub-processor discriminated union specification.
|
||||
- `.claude/skills/evaluate-library/SKILL.md` — new prompts + updated trace template.
|
||||
- All existing `docs/library-decisions/*.md` traces — `is-sub-processor` + `processes-pii` field additions.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `emit-sub-processors.mjs` generator implementation (Story 06).
|
||||
- Weekly `dpa-signed` staleness check in CI — that is an ADR-023 cross-reference; document it, but do not implement the cron here.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Amend `docs/decisions/ADR-022.md` with the sub-processor discriminated union frontmatter spec, update `.claude/skills/evaluate-library/SKILL.md` with new prompts and trace template, and backfill all existing `docs/library-decisions/*.md` traces with `is-sub-processor` + `processes-pii` fields — all gates pass on this commit.
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
id: 04-retention-purge-job
|
||||
epic: compliance-manifests-pii-retention-subprocessors
|
||||
title: Background retention purge job in core-shared
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: [01-pii-retention-type-primitives]
|
||||
blocks: []
|
||||
created: 2026-05-18T17:52:09Z
|
||||
updated: 2026-05-18T18:58:23.920Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add `core-shared/payload/retention-purge/retention-purge.job.ts` — a module that walks every Payload collection's `custom.retention.purgeSchedule`, registers a per-collection scheduled job via `IJobQueue`, queries rows whose active-retention period has elapsed, then either pseudonymizes or hard-deletes each row while emitting one `IAuditLog.record(...)` entry per row. Optional `auditLog` is skipped gracefully when `core-audit` is not wired.
|
||||
|
||||
## Why
|
||||
|
||||
Retention without automated purge is a compliance statement with no enforcement. The job makes `custom.retention` actionable: declarations in code become real deletes on schedule.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-shared/src/payload/retention-purge/retention-purge.job.ts` exists; receives `IJobQueue` + `SanitizedConfig` (+ optional `IAuditLog`) via constructor/factory.
|
||||
- At registration time, one scheduled job is created per collection that declares `custom.retention.purgeSchedule`.
|
||||
- Job body queries by `createdAt` (trigger `from-creation`) or `updatedAt` (trigger `from-last-access`), applies `pseudonymize` (NULL PII fields) or `hard-delete` (Payload cascade delete) per `postDeletion.action`.
|
||||
- Each processed row emits `auditLog.record({ action: "DELETE", subject: row.id, actor: "system", reason: "retention-policy" })`; when `auditLog` is undefined the emission is silently skipped.
|
||||
- Unit tests with an in-memory Payload mock cover: schedule registration per collection, row matching per trigger type, audit emission, pseudonymize vs hard-delete branches, and graceful auditLog skip.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-shared/src/payload/retention-purge/retention-purge.job.ts` + sibling unit test.
|
||||
- Uses existing `IJobQueue` from `core-shared/jobs` and `IAuditLog` from `core-audit` (optional injection).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- App-side boot wiring — the module is the deliverable; consumers integrate it into their `bindAll()`.
|
||||
- `lastAccessedAt` field hook for true "from-last-access" tracking (deferred; see PRD Q2).
|
||||
- Advisory lock for concurrent purge guard (deferred; see PRD Q4).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `retention-purge.job.ts` + unit tests to `packages/core-shared/src/payload/retention-purge/` — walks `custom.retention.purgeSchedule` per collection, registers scheduled jobs via `IJobQueue`, executes pseudonymize or hard-delete per `postDeletion.action`, emits optional audit entry per row, unit tests cover all branches including graceful auditLog skip — all gates pass on this commit.
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: 05-backfill-template-collections
|
||||
epic: compliance-manifests-pii-retention-subprocessors
|
||||
title: Backfill existing template Payload collections with PII and retention metadata
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: auth
|
||||
depends-on:
|
||||
[01-pii-retention-type-primitives, 02-eslint-rule-pii-declaration-complete]
|
||||
blocks: [06-compliance-generator-scripts]
|
||||
created: 2026-05-18T17:52:09Z
|
||||
updated: 2026-05-18T19:21:32.358Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add `custom.pii` + `custom.retention` to `auth.users` (full PII tagging), and `custom.retention` to the five remaining template collections (`blog.articles`, `marketing-pages.site-settings`, `marketing-pages.pages`, `media.media`, `navigation.header`), plus `custom.pii` on `media.media.uploadedBy` if the field exists. Each feature's collection file lands as its own commit.
|
||||
|
||||
## Why
|
||||
|
||||
The generators (Story 06) walk Payload configs to produce `compliance/*.yml`. Without backfill, the retention-policy generator warns every collection is missing `purgeSchedule`, and `data-map.yml` has no entries. The backfill also validates that the type primitives from Story 01 compile correctly in real collection files.
|
||||
|
||||
## Done when
|
||||
|
||||
- `auth.users`: `displayName` tagged `{ category: "identification-username", purpose: ["service-delivery"], exportable: true, restrictable: true }`. `custom.retention`: `postDeletion: { duration: "P30D", trigger: "after-deletion", action: "hard-delete" }`, `purgeSchedule: "daily"`. `PAYLOAD_AUTH_PII_DEFAULTS` covers `email`/`password`/`salt`/`hash` automatically — no `custom.authPii` override needed.
|
||||
- `blog.articles`: `custom.retention` with `purgeSchedule` declared.
|
||||
- `marketing-pages.site-settings` + `marketing-pages.pages`: `custom.retention` with `purgeSchedule` declared.
|
||||
- `media.media`: `custom.retention` with `purgeSchedule`; `uploadedBy` tagged `{ category: "identification-username", ... }` if the field exists in the collection.
|
||||
- `navigation.header`: `custom.retention` with `purgeSchedule` declared.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass after each individual commit.
|
||||
|
||||
## In scope
|
||||
|
||||
- Payload collection config files within: `packages/auth/`, `packages/blog/`, `packages/marketing-pages/`, `packages/media/`, `packages/navigation/`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- PII tags on fields not clearly identified as PII in the PRD (template default is conservative).
|
||||
- Custom `authPii` overrides — not needed unless non-default auth fields are present.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `custom.pii` (`displayName` as `identification-username`) + `custom.retention` (daily purge, 30-day post-deletion hard-delete) to `auth` feature's Payload users collection — `PAYLOAD_AUTH_PII_DEFAULTS` covers email/credentials — all gates pass on this commit.
|
||||
- [x] Add `custom.retention` to `blog` feature's articles collection — all gates pass on this commit.
|
||||
- [x] Add `custom.retention` to `marketing-pages` feature's site-settings and pages collections — all gates pass on this commit.
|
||||
- [x] Add `custom.retention` (and `custom.pii` on `uploadedBy` if the field exists) to `media` feature's media collection — all gates pass on this commit.
|
||||
- [x] Add `custom.retention` to `navigation` feature's header collection — all gates pass on this commit.
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
id: 06-compliance-generator-scripts
|
||||
epic: compliance-manifests-pii-retention-subprocessors
|
||||
title: Compliance generator scripts (emit-data-map, emit-retention-policy, emit-sub-processors, emit-all)
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: [01-pii-retention-type-primitives, 05-backfill-template-collections]
|
||||
blocks: [07-pre-commit-and-ci-integration, 08-docs-compliance-reference-files]
|
||||
created: 2026-05-18T17:52:09Z
|
||||
updated: 2026-05-18T20:12:13.181Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add four ESM scripts under `scripts/compliance/` and wire them as `pnpm compliance:*` package scripts in root `package.json`. Each emitter walks the relevant source (Payload collections or library traces), produces deterministic YAML, supports `--check` (diff against committed file, exit non-zero on mismatch) and `--print` (stdout) modes, and ships with unit tests. The final task commits the initial generated `compliance/*.yml` artifacts.
|
||||
|
||||
## Why
|
||||
|
||||
The generators are the runtime bridge between source declarations (Payload configs, ADR-022 library traces) and audit evidence (`compliance/*.yml`). Without them, the pre-commit hook and CI integration (Story 07) have nothing to invoke, and `compliance/` stays empty.
|
||||
|
||||
## Done when
|
||||
|
||||
- `scripts/compliance/emit-data-map.mjs`: walks Payload collections, applies `PAYLOAD_AUTH_PII_DEFAULTS` + `custom.authPii` overrides, emits deterministic `compliance/data-map.yml`, supports `--check` / `--print`; unit tests cover happy path, `--check` match, `--check` mismatch (readable diff), empty-collections, auth-defaults applied, and `authPii` override applied.
|
||||
- `scripts/compliance/emit-retention-policy.mjs`: walks collections, validates `purgeSchedule` present per collection (exit non-zero + hint if missing), emits `compliance/retention-policy.yml`, supports `--check` / `--print`; unit tests cover required fields validation and diff modes.
|
||||
- `scripts/compliance/emit-sub-processors.mjs`: walks `docs/library-decisions/*.md`, filters `is-sub-processor: true`, merges `compliance/sub-processors.manual.yml` (if present, with `source: manual` flag), emits sorted `compliance/sub-processors.yml`, supports `--check` / `--print`; unit tests cover discriminated-union parsing, absent manual file graceful skip, and merge.
|
||||
- `scripts/compliance/emit-all.mjs`: orchestrates all three in `--check` mode, exits non-zero if any generator fails.
|
||||
- Root `package.json` gains scripts: `compliance:data-map`, `compliance:retention-policy`, `compliance:sub-processors`, `compliance:emit-all`.
|
||||
- Initial `compliance/data-map.yml`, `compliance/retention-policy.yml`, `compliance/sub-processors.yml` generated and committed.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `scripts/compliance/emit-data-map.mjs` + tests.
|
||||
- `scripts/compliance/emit-retention-policy.mjs` + tests.
|
||||
- `scripts/compliance/emit-sub-processors.mjs` + tests.
|
||||
- `scripts/compliance/emit-all.mjs`.
|
||||
- Root `package.json` script entries.
|
||||
- Initial `compliance/*.yml` files committed.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Pre-commit hook wiring (Story 07).
|
||||
- CI integration (Story 07).
|
||||
- `compliance/sub-processors.manual.yml` — consumer-authored; generator handles its absence gracefully.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `scripts/compliance/emit-data-map.mjs` + unit tests + `compliance:data-map` root package script — walks Payload collections, applies auth PII defaults and `authPii` overrides, deterministic YAML output, `--check` and `--print` modes — all gates pass on this commit.
|
||||
- [x] Add `scripts/compliance/emit-retention-policy.mjs` + unit tests + `compliance:retention-policy` root package script — validates `purgeSchedule` on every collection, deterministic YAML output, `--check` and `--print` modes — all gates pass on this commit.
|
||||
- [x] Add `scripts/compliance/emit-sub-processors.mjs` + unit tests + `compliance:sub-processors` root package script — parses `is-sub-processor` discriminated union from library traces, merges manual entries with `source: manual` flag, sorted deterministic YAML output, `--check` and `--print` modes — all gates pass on this commit.
|
||||
- [x] Add `scripts/compliance/emit-all.mjs` orchestrator + `compliance:emit-all` root package script, run `pnpm compliance:emit-all` to produce and commit initial `compliance/data-map.yml`, `compliance/retention-policy.yml`, and `compliance/sub-processors.yml` — all gates pass on this commit.
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
id: 07-pre-commit-and-ci-integration
|
||||
epic: compliance-manifests-pii-retention-subprocessors
|
||||
title: Pre-commit hook and CI integration for compliance drift detection
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: [06-compliance-generator-scripts]
|
||||
blocks: []
|
||||
created: 2026-05-18T17:52:09Z
|
||||
updated: 2026-05-18T20:16:22.912Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Wire `pnpm compliance:emit-all` into the pre-commit hook (conditional — only when staged files are Payload configs or library traces) and add `pnpm compliance:emit-all --check` as a hard-fail step in the CI validate job, positioned after `pnpm conformance` and before `pnpm coverage:diff`.
|
||||
|
||||
## Why
|
||||
|
||||
The generators alone don't prevent drift — a developer could edit a Payload config and never regenerate. The pre-commit hook auto-regenerates and auto-stages `compliance/*.yml`; CI catches any slip-through on the PR. Together they form the E3 and E5 latency layers for compliance drift.
|
||||
|
||||
## Done when
|
||||
|
||||
- `.husky/pre-commit` gains a conditional step that: checks whether any staged file matches `packages/*/src/integrations/cms/**/*.ts`, `docs/library-decisions/*.md`, or `compliance/*.yml`; if so, runs `pnpm compliance:emit-all` and `git add compliance/`. Non-matching commits incur only the ~10ms detection cost.
|
||||
- `.github/workflows/ci.yml` validate job gains a `Compliance manifest drift check` step running `pnpm compliance:emit-all --check`, positioned after `pnpm conformance`. CI failure message includes the fix command (`pnpm compliance:emit-all`).
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `.husky/pre-commit` — conditional compliance regeneration step.
|
||||
- `.github/workflows/ci.yml` — validate job `compliance:emit-all --check` step.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Generator scripts themselves (Story 06).
|
||||
- The `--no-verify` bypass — repo policy already prohibits it; CI re-checks provide the safety net.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add conditional `pnpm compliance:emit-all` step to `.husky/pre-commit` (staged-file pattern guard matching Payload configs, library traces, and `compliance/*.yml`; auto-stages generated files via `git add compliance/`) and add `pnpm compliance:emit-all --check` hard-fail step to `.github/workflows/ci.yml` validate job with failure message pointing to the fix command — all gates pass on this commit.
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
id: 08-docs-compliance-reference-files
|
||||
epic: compliance-manifests-pii-retention-subprocessors
|
||||
title: docs/compliance reference example files and README
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: docs
|
||||
depends-on: [06-compliance-generator-scripts]
|
||||
blocks: []
|
||||
created: 2026-05-18T17:52:09Z
|
||||
updated: 2026-05-18T20:23:30.582Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add `docs/compliance/data-map.example.yml`, `docs/compliance/retention-policy.example.yml`, `docs/compliance/sub-processors.example.yml`, and `docs/compliance/README.md` explaining the `docs/compliance/` (templates / examples) vs root `compliance/` (live generated artifacts) split and how to operate the generators.
|
||||
|
||||
## Why
|
||||
|
||||
Without documentation, a downstream consumer editing `compliance/*.yml` manually won't understand which files are generated vs hand-authored, what fields each entry requires, or how to run the generators. The `docs/compliance/` folder becomes the canonical onboarding reference for the compliance module.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/compliance/data-map.example.yml` shows a complete data-map entry with every field (`category`, `purpose`, `exportable`, `restrictable`, optional `retention`) populated and annotated.
|
||||
- `docs/compliance/retention-policy.example.yml` shows a complete retention-policy entry (`activeRetention`, `postDeletion`, `purgeSchedule`, optional `coldArchive`).
|
||||
- `docs/compliance/sub-processors.example.yml` shows both a trace-backed entry (`is-sub-processor: true` with all conditional fields) and a `source: manual` hand-authored entry.
|
||||
- `docs/compliance/README.md` explains: what each YAML file contains, how they are generated, the `docs/compliance/` (examples) vs `compliance/` (live) split, when and how to author `compliance/sub-processors.manual.yml`, and how to run `pnpm compliance:emit-all`.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/compliance/data-map.example.yml`
|
||||
- `docs/compliance/retention-policy.example.yml`
|
||||
- `docs/compliance/sub-processors.example.yml`
|
||||
- `docs/compliance/README.md`
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `compliance/sub-processors.manual.yml` — consumer-authored artifact; not shipped by the template.
|
||||
- Runbooks, privacy policies, pre-launch checklist — Epic D territory.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `docs/compliance/data-map.example.yml`, `docs/compliance/retention-policy.example.yml`, `docs/compliance/sub-processors.example.yml`, and `docs/compliance/README.md` explaining the docs/compliance (templates) vs compliance/ (live artifacts) split, generator usage, and manual sub-processor entry authoring — all gates pass on this commit.
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
id: compliance-manifests-pii-retention-subprocessors
|
||||
prd: docs/work/prds/compliance-manifests-pii-retention-subprocessors.prd.md
|
||||
title: Declarative compliance manifests (PII + retention + sub-processors) — Epic A of ADR-025
|
||||
type: epic
|
||||
status: done
|
||||
features:
|
||||
[core-shared, core-eslint, auth, blog, media, marketing-pages, navigation]
|
||||
created: 2026-05-18T17:52:09Z
|
||||
updated: 2026-05-18T20:23:30.582Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Ship the declarative compliance manifests + generators so downstream consumers get a complete, automatically-validated PII inventory, retention policy, and sub-processor record by editing source-of-truth Payload configs and ADR-022 library traces. Drift detection runs in pre-commit + CI; the consumer's `compliance/` directory becomes audit evidence.
|
||||
|
||||
## Stories
|
||||
|
||||
- [x] [01 — PII and retention type primitives in core-shared](01-pii-retention-type-primitives/_story.md)
|
||||
- [x] [02 — ESLint rule pii-declaration-must-be-complete](02-eslint-rule-pii-declaration-complete/_story.md)
|
||||
- [x] [03 — ADR-022 amendment and evaluate-library skill update](03-adr-022-amendment-and-evaluate-library-skill/_story.md)
|
||||
- [x] [04 — Background retention purge job in core-shared](04-retention-purge-job/_story.md)
|
||||
- [x] [05 — Backfill existing template collections](05-backfill-template-collections/_story.md)
|
||||
- [x] [06 — Compliance generator scripts](06-compliance-generator-scripts/_story.md)
|
||||
- [x] [07 — Pre-commit hook and CI integration](07-pre-commit-and-ci-integration/_story.md)
|
||||
- [x] [08 — docs/compliance reference files and README](08-docs-compliance-reference-files/_story.md)
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: 01-subject-linkage-types
|
||||
epic: dsr-consent-and-cookie-banner
|
||||
title: Subject-linkage types in core-shared
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: []
|
||||
blocks: [02-audit-enum-amendment]
|
||||
created: 2026-05-19T12:00:00Z
|
||||
updated: 2026-05-19T10:13:17.502Z
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- [x] 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.
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
id: 02-audit-enum-amendment
|
||||
epic: dsr-consent-and-cookie-banner
|
||||
title: Audit action enum amendment (ADR-018)
|
||||
type: technical-story
|
||||
status: done
|
||||
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-19T10:29:21.283Z
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- [x] 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.
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
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: done
|
||||
feature: core-consent
|
||||
depends-on: [02-audit-enum-amendment]
|
||||
blocks: [04-core-consent-implementation]
|
||||
created: 2026-05-19T12:00:00Z
|
||||
updated: 2026-05-19T11:54:41.015Z
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- [x] 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.
|
||||
- [x] 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.
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
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: done
|
||||
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-19T13:27:09.005Z
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- [x] 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.
|
||||
- [x] 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.
|
||||
- [x] 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.
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: 05-core-consent-react
|
||||
epic: dsr-consent-and-cookie-banner
|
||||
title: core-consent React subpath — ConsentProvider + useConsent()
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-consent
|
||||
depends-on: [04-core-consent-implementation]
|
||||
blocks: [09-cookie-consent-banner]
|
||||
created: 2026-05-19T12:00:00Z
|
||||
updated: 2026-05-19T19:21:30.198Z
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- [x] 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.
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
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: done
|
||||
feature: core-dsr
|
||||
depends-on: [02-audit-enum-amendment]
|
||||
blocks: [07-core-api-router-composition]
|
||||
created: 2026-05-19T12:00:00Z
|
||||
updated: 2026-05-19T20:39:14.972Z
|
||||
---
|
||||
|
||||
## 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. 15–18 + 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
|
||||
|
||||
- [x] 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.
|
||||
- [x] 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.
|
||||
- [x] 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.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
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: done
|
||||
feature: core-api
|
||||
depends-on: [04-core-consent-implementation, 06-core-dsr]
|
||||
blocks: [11-documentation]
|
||||
created: 2026-05-19T12:00:00Z
|
||||
updated: 2026-05-19T20:52:13.004Z
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- [x] 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.
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: 08-core-ui-scaffold
|
||||
epic: dsr-consent-and-cookie-banner
|
||||
title: core-ui scaffold
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-ui
|
||||
depends-on: []
|
||||
blocks: [09-cookie-consent-banner]
|
||||
created: 2026-05-19T12:00:00Z
|
||||
updated: 2026-05-19T21:03:59.344Z
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- [x] 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).
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: 09-cookie-consent-banner
|
||||
epic: dsr-consent-and-cookie-banner
|
||||
title: CookieConsentBanner component in core-ui
|
||||
type: user-story
|
||||
status: done
|
||||
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-19T21:38:48.887Z
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- [x] 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.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: 10-auth-signup-migration
|
||||
epic: dsr-consent-and-cookie-banner
|
||||
title: auth signUp anonymous consent migration
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: auth
|
||||
depends-on: [04-core-consent-implementation]
|
||||
blocks: [11-documentation]
|
||||
created: 2026-05-19T12:00:00Z
|
||||
updated: 2026-05-19T21:56:58.992Z
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- [x] 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.
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
id: 11-documentation
|
||||
epic: dsr-consent-and-cookie-banner
|
||||
title: Documentation — DSR guide, consent guide, glossary, CLAUDE.md
|
||||
type: technical-story
|
||||
status: done
|
||||
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-19T22:09:30.310Z
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- [x] 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.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
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: done
|
||||
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-19T22:09:30.310Z
|
||||
---
|
||||
|
||||
## 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. 15–18 + 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
|
||||
|
||||
- [x] [01 — Subject-linkage types in core-shared](01-subject-linkage-types/_story.md)
|
||||
- [x] [02 — Audit action enum amendment (ADR-018)](02-audit-enum-amendment/_story.md)
|
||||
- [x] [03 — core-consent foundation: types + brand + withConsent + conformance + ESLint rule](03-core-consent-foundation/_story.md)
|
||||
- [x] [04 — core-consent implementation: Payload impl + DI + migration helpers + tRPC router](04-core-consent-implementation/_story.md)
|
||||
- [x] [05 — core-consent React subpath: ConsentProvider + useConsent()](05-core-consent-react/_story.md)
|
||||
- [x] [06 — core-dsr: scaffold + interfaces + Payload impls + handlers + dsrRouter](06-core-dsr/_story.md)
|
||||
- [x] [07 — core-api router composition: dsrRouter + consentRouter into appRouter](07-core-api-router-composition/_story.md)
|
||||
- [x] [08 — core-ui scaffold](08-core-ui-scaffold/_story.md)
|
||||
- [x] [09 — CookieConsentBanner component in core-ui](09-cookie-consent-banner/_story.md)
|
||||
- [x] [10 — auth signUp anonymous consent migration](10-auth-signup-migration/_story.md)
|
||||
- [x] [11 — Documentation: DSR guide + consent guide + glossary + CLAUDE.md](11-documentation/_story.md)
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
id: 01-trace-schema-foundation
|
||||
epic: library-evaluation-policy
|
||||
title: Trace schema module + docs/library-decisions/ foundation
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: scripts
|
||||
depends-on: []
|
||||
blocks:
|
||||
[
|
||||
02-pre-commit-check-script,
|
||||
04-evaluate-library-skill,
|
||||
07-generator-pre-shipped-traces,
|
||||
08-backfill-traces,
|
||||
]
|
||||
created: 2026-05-14T06:52:02+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Create the shared Zod-validated trace-schema module (`scripts/library-decisions/schema.mjs`) that every enforcement layer imports, and establish `docs/library-decisions/` with a `_template.md` schema reference that documents the required frontmatter + heading shape for all future traces.
|
||||
|
||||
## Why
|
||||
|
||||
All four enforcement layers (skill, pre-commit check, generator templates, sandcastle reviewer) need a single authoritative definition of what a valid library trace looks like. Without a shared module, each layer would re-implement the parse/validate logic independently and drift. The `_template.md` gives human contributors and agents a copy-pasteable starting point.
|
||||
|
||||
## Done when
|
||||
|
||||
- `scripts/library-decisions/schema.mjs` exists and exports: (1) a Zod schema validating the full trace frontmatter (all fields from ADR-022 §4 including nested `filter-results` object), (2) a `parseTrace(filePath)` function that reads + validates a `.md` file's frontmatter, (3) a `validateTrace(raw)` function for validating already-parsed objects.
|
||||
- `scripts/library-decisions/schema.test.mjs` covers: valid trace round-trips without error; missing required field throws; unknown filter key rejected; invalid enum value rejected; `accepted-cves` optional field accepted.
|
||||
- `docs/library-decisions/_template.md` exists with the complete frontmatter schema (all fields, all enums documented) and all required section headings (`## Filter: <name>` × 8 + `## Prompt: <name>` × 3) in the machine-checkable order from ADR-022.
|
||||
- `docs/library-decisions/` directory is committed (can be just `_template.md` + `.gitkeep` if no traces yet).
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `scripts/library-decisions/schema.mjs` — Zod schema + parse/validate exports.
|
||||
- `scripts/library-decisions/schema.test.mjs` — unit tests (vitest or node:test; match the pattern used by `scripts/work/` tests).
|
||||
- `docs/library-decisions/_template.md` — schema reference document.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The pre-commit check script (`check.mjs`) — Story 02.
|
||||
- The skill itself — Story 04.
|
||||
- Actual trace files (backfill) — Story 08.
|
||||
- Generator template changes — Story 07.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Create `scripts/library-decisions/schema.mjs` with Zod frontmatter schema (all fields from ADR-022 §4: `package`, `version`, `tier`, `decision`, `date`, `deciders`, `adr`, `filter-results` nested object with all 8 filter keys and their enum values, `verification-commands`, `accepted-cves` optional), plus `parseTrace(filePath)` and `validateTrace(raw)` exports; write `schema.test.mjs` covering valid round-trip, missing-field rejection, invalid-enum rejection, and optional `accepted-cves`; create `docs/library-decisions/_template.md` with full frontmatter schema + all 11 required headings in ADR-022 order; all gates pass on this single commit.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: 02-pre-commit-check-script
|
||||
epic: library-evaluation-policy
|
||||
title: Pre-commit check script for library trace presence
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: scripts
|
||||
depends-on: [01-trace-schema-foundation]
|
||||
blocks: [06-sandcastle-reviewer-prompt]
|
||||
created: 2026-05-14T06:52:02+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Write `scripts/library-decisions/check.mjs` — the script that walks staged `package.json` diffs, derives the tier of each affected package, and fails the commit when a new runtime dependency in a feature- or core-tier package has no sibling approved trace staged. Wire it into `.husky/pre-commit` as step 4.
|
||||
|
||||
## Why
|
||||
|
||||
Human and agent reviewers cannot reliably check trace presence during code review. The pre-commit hook is the last mechanical gate before a dep reaches the repo; it runs unconditionally, composes with `--no-verify` protection already in the bash-guard hook, and gives the committer immediate actionable feedback.
|
||||
|
||||
## Done when
|
||||
|
||||
- `scripts/library-decisions/check.mjs` exists and: (1) reads `git diff --cached --name-only -- '**/package.json'`; (2) for each staged `package.json`, derives tier from path (`apps/*` → app, `packages/core-*` → core, `packages/*` → feature); (3) for each newly added line in `dependencies` (not `devDependencies` / `peerDependencies`), checks that `docs/library-decisions/*-<name>.md` is also staged with `decision: approved`; (4) exits 1 with a per-package error report + pointer to the skill when any check fails; (5) app-tier and devdep additions exit 0 silently.
|
||||
- `.husky/pre-commit` invokes `node scripts/library-decisions/check.mjs` after the existing state-sync guard.
|
||||
- `scripts/library-decisions/check.test.mjs` covers (using a temp git repo fixture): new feature-tier dep without trace → exit 1; new feature-tier dep with approved trace staged → exit 0; new feature-tier dep with rejected-decision trace staged → exit 1; new app-tier dep → exit 0; new devdep → exit 0; multi-file diff with mixed pass/fail → exit 1 with per-package report; `peerDependencies`-only change → exit 0.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `scripts/library-decisions/check.mjs` — the check script (imports `schema.mjs` from Story 01 for trace validation).
|
||||
- `scripts/library-decisions/check.test.mjs` — integration tests using a temp git repo fixture (mirror pattern from `scripts/work/state-sync-guard.mjs` tests).
|
||||
- `.husky/pre-commit` — one added line.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Sandcastle reviewer prompt integration — Story 06.
|
||||
- `--staged-against <base>` flag for CI/sandcastle use — added in Story 06 when the reviewer prompt is written.
|
||||
- `pnpm libs check` ergonomic wrapper — deferred per PRD.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Write `scripts/library-decisions/check.mjs` (imports schema from Story 01; parses `git diff --cached` output; tier derivation from path; staged-trace presence + `decision: approved` check; exit-1 report with skill pointer); wire into `.husky/pre-commit`; write `check.test.mjs` integration tests with temp git repo fixture covering all 7 cases from Done when; all gates pass on this single commit.
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
id: 03-claude-hooks
|
||||
epic: library-evaluation-policy
|
||||
title: Claude PreToolUse / PostToolUse hooks for library-policy nudge
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: []
|
||||
blocks: []
|
||||
created: 2026-05-14T06:52:02+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Write `.claude/hooks/library-policy-nudge.sh` — a single hook script that dispatches on `tool_use_type` to handle both `PreToolUse` (Bash invocations matching `pnpm add` / `pnpm i <pkg>`) and `PostToolUse` (Edit/Write on any `**/package.json`). On match, emit a non-blocking system-reminder pointing the agent at the `evaluate-library` skill with the exact invocation pattern. Register the hook in `.claude/settings.json` following the same pattern as `generator-first-nudge.sh`.
|
||||
|
||||
## Why
|
||||
|
||||
Agents (and developers) running `pnpm add` by reflex bypass the policy before the pre-commit gate fires. The PreToolUse hook injects the skill reminder _before_ the install runs — the cheapest possible intervention point. The PostToolUse hook catches the rarer case where an agent edits `package.json` directly without running the install command.
|
||||
|
||||
## Done when
|
||||
|
||||
- `.claude/hooks/library-policy-nudge.sh` exists, is executable, dispatches on `CLAUDE_TOOL_USE_TYPE` (or equivalent hook env var), and emits the reminder to stdout when a `pnpm add`/`pnpm i <pkg>` bash command is detected or when an Edit/Write tool targets a path matching `**/package.json`.
|
||||
- The hook is registered in `.claude/settings.json` (or `.claude/settings.local.json` if per-repo convention) under both `PreToolUse` and `PostToolUse` event types, matching the registration pattern of `generator-first-nudge.sh`.
|
||||
- The reminder text includes the literal string `/evaluate-library` and the argument template `<name> --tier <feature|core|app> --target <package-path>` so the agent sees the exact invocation.
|
||||
- Bash smoke tests (same style as any existing hook tests): pipe a mocked Claude hook payload `{ "tool_input": { "command": "pnpm add foo" } }` into the script and assert stdout contains the skill-reminder marker; pipe a payload for `pnpm add -D foo` (devdep) and assert no reminder emitted; pipe an Edit payload on `src/feature.manifest.ts` and assert no reminder emitted.
|
||||
- `pnpm lint && pnpm fallow:audit` pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `.claude/hooks/library-policy-nudge.sh` — the hook script.
|
||||
- `.claude/settings.json` (or equivalent) — hook registration lines.
|
||||
- Bash smoke tests for the hook script.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Hook auto-deny or blocking behavior — the hook is advisory only (non-blocking system-reminder).
|
||||
- Hooks for `npm install` / `yarn add` — the repo is pnpm-only.
|
||||
- Integration with the skill implementation (Story 04) — the hook emits a text reminder; the skill is a separate file.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Investigate how `generator-first-nudge.sh` is registered (read `.claude/settings.json` and the existing hook script) to confirm the env-var names, payload shape, and registration keys; then write `.claude/hooks/library-policy-nudge.sh` + register it in the settings file + write bash smoke tests covering `pnpm add <pkg>` (reminder), `pnpm add -D <pkg>` (no reminder), Edit on non-package.json (no reminder), Edit on `package.json` (reminder); all gates pass on this single commit.
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
id: 04-evaluate-library-skill
|
||||
epic: library-evaluation-policy
|
||||
title: evaluate-library skill (SKILL.md + supporting files)
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: [01-trace-schema-foundation]
|
||||
blocks: [05-human-guide]
|
||||
created: 2026-05-14T06:52:02+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Create the `.claude/skills/evaluate-library/` directory with `SKILL.md` (the authoritative agent runbook), `POLICY.md` (ADR-022 mirror for quick reference), `TRACE-TEMPLATE.md` (showing the YAML frontmatter + heading shape the skill must emit), and an `EXAMPLES/` directory with two worked cases (one approved, one rejected). The skill must be invocable as `/evaluate-library <name> --tier <feature|core|app> --target <package-path>`.
|
||||
|
||||
## Why
|
||||
|
||||
Without a deterministic skill runbook, every agent evaluating a library does so ad hoc — different filters, different order, inconsistent trace format. The skill is the single source of truth for the 8-filter + 3-prompt sequence, the collect-cheap-skip-expensive ordering, and the trace write step. It also provides the canonical invocation the Claude hook emits.
|
||||
|
||||
## Done when
|
||||
|
||||
- `.claude/skills/evaluate-library/SKILL.md` exists and covers: invocation signature (`/evaluate-library <name> --tier <tier> --target <pkg-path>`), the 8 filters in collect-cheap-skip-expensive order (license, types, shadow-check, boundary-fit run to completion; maintenance, CVE scan, EU residency, named-consumer short-circuit after first reject), the 3 prompts, trace-write step (unconditional at evaluation end, including rejections), and the fail/skip sentinel for skipped expensive filters.
|
||||
- `.claude/skills/evaluate-library/POLICY.md` summarises ADR-022 in ≤2 pages — the filters, the tier trigger, the trace schema fields, the four enforcement layers.
|
||||
- `.claude/skills/evaluate-library/TRACE-TEMPLATE.md` shows the complete YAML frontmatter (all fields, real sentinel values for skipped filters) + all 11 required headings in order.
|
||||
- `.claude/skills/evaluate-library/EXAMPLES/` contains at least two worked trace files: one `decision: approved` trace and one `decision: rejected` trace (use `trpc-to-openapi` as the rejected example per the PRD, with `named-consumer: fail` and prose citing the grill-session conversation as provenance).
|
||||
- The skill is listed in `.claude/settings.json` (or wherever skills are registered) so `/evaluate-library` resolves to the SKILL.md via the Skill tool.
|
||||
- `pnpm lint && pnpm fallow:audit` pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `.claude/skills/evaluate-library/SKILL.md`
|
||||
- `.claude/skills/evaluate-library/POLICY.md`
|
||||
- `.claude/skills/evaluate-library/TRACE-TEMPLATE.md`
|
||||
- `.claude/skills/evaluate-library/EXAMPLES/approved-example.md`
|
||||
- `.claude/skills/evaluate-library/EXAMPLES/rejected-trpc-to-openapi.md`
|
||||
- Skill registration in `.claude/settings.json` (match existing skill registration pattern).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Automated tests for the skill (it is a prose runbook; correctness is verified by the success criterion in the PRD — running it against `trpc-to-openapi` produces the documented trace).
|
||||
- The schema module (Story 01) — already landed.
|
||||
- The human guide with worked examples for non-agent readers (Story 05).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Investigate the existing skill registration pattern (read `.claude/settings.json` and one existing skill's SKILL.md to confirm format and registration key); then write all five skill files (`SKILL.md`, `POLICY.md`, `TRACE-TEMPLATE.md`, `EXAMPLES/approved-example.md`, `EXAMPLES/rejected-trpc-to-openapi.md`) and register the skill; all gates pass on this single commit.
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: 05-human-guide
|
||||
epic: library-evaluation-policy
|
||||
title: Human reading-room guide — docs/guides/adding-a-library.md
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: docs
|
||||
depends-on: [04-evaluate-library-skill]
|
||||
blocks: [09-claude-md-update]
|
||||
created: 2026-05-14T06:52:02+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Write `docs/guides/adding-a-library.md` — the human-readable guide explaining the library evaluation policy with worked examples (one approved, one rejected), the tier trigger, the four enforcement layers, and how to invoke the skill. The guide targets a maintainer reading the repo for the first time.
|
||||
|
||||
## Why
|
||||
|
||||
ADR-022 is the source of truth but is written for decision-record density, not onboarding. The guide translates the policy into a narrative that answers "why does this exist?" and "what do I actually do?" before a developer encounters the pre-commit gate for the first time. Worked examples anchor the abstract filter list to concrete outcomes.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/guides/adding-a-library.md` exists with: (1) a "Why this exists" section explaining the uncodified-surface problem and the three signals from the PRD; (2) the tier trigger (feature/core packages require traces; app-tier does not; devdeps exempt); (3) the four enforcement layers (Claude hook → skill → pre-commit → sandcastle) in latency order; (4) step-by-step "how to add a library" walkthrough pointing at the `/evaluate-library` skill; (5) a worked approved example (brief — the full trace lives in `EXAMPLES/` from Story 04); (6) a worked rejected example (`trpc-to-openapi`, `named-consumer: fail`); (7) a link to ADR-022 and `docs/library-decisions/_template.md`.
|
||||
- `pnpm lint && pnpm fallow:audit` pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/guides/adding-a-library.md` — the guide document.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The skill itself (Story 04) — already landed.
|
||||
- CLAUDE.md update (Story 09) — that bullet points here and to ADR-022 but lands separately.
|
||||
- Changing any existing guide or ADR.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Write `docs/guides/adding-a-library.md` with all seven sections from Done when (why, tier trigger, four layers, how-to walkthrough, worked approved + rejected examples, cross-links); all gates pass on this single commit.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: 06-sandcastle-reviewer-prompt
|
||||
epic: library-evaluation-policy
|
||||
title: Sandcastle reviewer prompt — Library-trace check section
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: [02-pre-commit-check-script]
|
||||
blocks: []
|
||||
created: 2026-05-14T06:52:02+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Append a "Library-trace check" section to `.sandcastle/reviewer.prompt.md` instructing the reviewer agent to run `node scripts/library-decisions/check.mjs --staged-against <base>` before issuing its verdict, and add the `--staged-against <base>` flag to `check.mjs` so it can compare against a given base ref rather than only the git index.
|
||||
|
||||
## Why
|
||||
|
||||
The sandcastle reviewer runs in a clean sandbox where `git diff --cached` may not reflect the full branch diff. The `--staged-against <base>` flag allows the reviewer to pass the PR's base branch as the comparison point, giving the same check a CI-compatible code path. Without this, the fourth enforcement layer is advisory only — it has no mechanical check to back it up.
|
||||
|
||||
## Done when
|
||||
|
||||
- `scripts/library-decisions/check.mjs` accepts a `--staged-against <base>` flag; when present, compares `git diff <base>...HEAD -- '**/package.json'` instead of `git diff --cached`.
|
||||
- `check.test.mjs` has a new test case: `--staged-against main` mode with a new feature-tier dep and no trace → exit 1.
|
||||
- `.sandcastle/reviewer.prompt.md` contains a "Library-trace check" section (appended, not replacing existing content) instructing the reviewer to run `node scripts/library-decisions/check.mjs --staged-against <base-branch>` and reject the slice if it exits non-zero.
|
||||
- `pnpm lint && pnpm test && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `scripts/library-decisions/check.mjs` — add `--staged-against` flag.
|
||||
- `scripts/library-decisions/check.test.mjs` — add test for the new flag.
|
||||
- `.sandcastle/reviewer.prompt.md` — append the Library-trace check section.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Changing the reviewer prompt's existing sections.
|
||||
- The pre-commit (index-mode) check behavior — already in Story 02.
|
||||
- CI integration (GitHub Actions) — deferred.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `--staged-against <base>` flag to `check.mjs` (switches from `git diff --cached` to `git diff <base>...HEAD`); add a test covering `--staged-against` mode (temp git repo fixture, new feature-tier dep, no trace → exit 1); append "Library-trace check" section to `.sandcastle/reviewer.prompt.md` with the `node scripts/library-decisions/check.mjs --staged-against <base>` invocation and reject instruction; all gates pass on this single commit.
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: 07-generator-pre-shipped-traces
|
||||
epic: library-evaluation-policy
|
||||
title: Generator templates — pre-shipped traces for optional core packages
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: tooling
|
||||
depends-on: [01-trace-schema-foundation]
|
||||
blocks: []
|
||||
created: 2026-05-14T06:52:02+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Update the five optional-core generator templates (`events`, `realtime`, `audit`, `trpc`, `ui`) so that when `pnpm turbo gen core-package <name>` runs, it copies pre-written `decision: approved` library traces into `docs/library-decisions/` for every direct runtime dependency of that core package. Update the corresponding `__snapshots__` files so the snapshot tests cover the new trace files.
|
||||
|
||||
## Why
|
||||
|
||||
A developer who scaffolds an optional core via the generator should not immediately face a pre-commit failure for its bundled deps — the generator is the policy-compliant path, so the traces should land by construction. Without pre-shipped traces, the very act of using the generator would trigger the enforcement gate it's supposed to clear.
|
||||
|
||||
## Done when
|
||||
|
||||
- Each of the five generator templates (`turbo/generators/templates/core-package/{events,realtime,audit,trpc,ui}/`) contains a `docs/library-decisions/` subtree with one `.md` trace file per direct runtime dependency of that core, dated at generation time (use the template variable for date, or freeze to the scaffold date with a comment), `decision: approved`, and citing the relevant ADR (`events` → ADR-015, `realtime` → ADR-016, `audit` → ADR-018; `trpc` and `ui` cite their closest ADR or `null` if none).
|
||||
- The generator copies these files into the workspace when run.
|
||||
- The `turbo/generators/__snapshots__/core-package/<name>.snapshot.json` files are updated to include the new trace files; `pnpm turbo gen core-package events` (or any other optional core) passes the existing snapshot test.
|
||||
- `pnpm lint && pnpm fallow:audit` pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `turbo/generators/templates/core-package/events/` — pre-shipped trace(s).
|
||||
- `turbo/generators/templates/core-package/realtime/` — pre-shipped trace(s).
|
||||
- `turbo/generators/templates/core-package/audit/` — pre-shipped trace(s).
|
||||
- `turbo/generators/templates/core-package/trpc/` — pre-shipped trace(s).
|
||||
- `turbo/generators/templates/core-package/ui/` — pre-shipped trace(s).
|
||||
- Snapshot JSON updates for all five cores.
|
||||
- Generator copy logic (if not already handled by the template mechanism).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Backfill traces for already-installed optional cores in the live workspace — Story 08.
|
||||
- Generator templates for non-core-package generators (feature, event, job, realtime, component) — no runtime deps are emitted by those generators.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Inventory the direct runtime deps of each of the five optional core generator templates (read each template's `package.json`); write one approved trace file per dep in the correct generator template subtree with ADR citation; update the five snapshot JSON files to include the new trace file entries; verify `pnpm turbo gen core-package events` (or equivalent dry-run) matches the updated snapshot; all gates pass on this single commit.
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
id: 08-backfill-traces
|
||||
epic: library-evaluation-policy
|
||||
title: Backfill library traces for existing feature- and core-tier runtime deps
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: docs
|
||||
depends-on: [01-trace-schema-foundation]
|
||||
blocks: []
|
||||
created: 2026-05-14T06:52:02+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Write approved library trace files dated 2026-05-14 in `docs/library-decisions/` for every existing runtime dependency in feature- and core-tier packages, grouped by ADR provenance into four commits. No package.json is changed; these commits are pure trace-file additions.
|
||||
|
||||
## Why
|
||||
|
||||
Without backfill, every existing dep becomes a pre-commit-hook failure the first time someone touches a `package.json` — the enforcement gate would fire retroactively. Backfill establishes the baseline so the gate is additive (new deps require traces) rather than disruptive (old deps fail immediately). Grouping by ADR cluster makes the commit history readable and keeps each commit focused on a coherent rationale.
|
||||
|
||||
## Done when
|
||||
|
||||
- All runtime deps in `packages/` (feature- and core-tier) have a corresponding `docs/library-decisions/YYYY-MM-DD-<name>.md` with `decision: approved`, `date: 2026-05-14`, and the relevant `adr` citation (or `null` for un-cited deps).
|
||||
- Four commits land, one per cluster:
|
||||
- **ADR-002 cluster**: `inversify`, `reflect-metadata`.
|
||||
- **ADR-014 cluster**: `@sentry/node`, `@sentry/nextjs`, `@sentry/react`, and any other Sentry packages present.
|
||||
- **ADR-017 cluster**: `@opentelemetry/api`, `@opentelemetry/sdk-node`, and any other OTel packages present.
|
||||
- **Un-cited cluster**: `payload`, `@trpc/server`, `zod`, `superjson`, and any remaining runtime deps not covered by an ADR.
|
||||
- All trace files pass `validateTrace()` from `schema.mjs` (Story 01).
|
||||
- `pnpm lint && pnpm fallow:audit` pass after all four commits.
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/library-decisions/<date>-<name>.md` trace files — one per dep.
|
||||
- Four conventional commits: `chore(deps): backfill library traces for <cluster>`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Deps in `apps/*` — app-tier is out of scope per the PRD.
|
||||
- devDeps in any tier — exempt from traces.
|
||||
- Changing any `package.json` — backfill is trace-only.
|
||||
- Optional core packages not yet installed in the workspace — covered by Story 07 (generator pre-shipped traces) when they are scaffolded.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Inventory all runtime deps in `packages/` (run `jq '.dependencies // {} | keys' packages/*/package.json packages/core-*/package.json` or equivalent); write approved trace files for the ADR-002 cluster (`inversify`, `reflect-metadata`) in `docs/library-decisions/`; commit as `chore(deps): backfill library traces for ADR-002 cluster`.
|
||||
- [x] Write approved trace files for the ADR-014 cluster (Sentry packages); commit as `chore(deps): backfill library traces for ADR-014 cluster`.
|
||||
- [x] Write approved trace files for the ADR-017 cluster (OpenTelemetry packages); commit as `chore(deps): backfill library traces for ADR-017 cluster`.
|
||||
- [x] Write approved trace files for the un-cited cluster (`payload`, `@trpc/server`, `zod`, `superjson`, and any remaining runtime deps); commit as `chore(deps): backfill library traces for un-cited cluster`; all gates pass after this final commit.
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: 09-claude-md-update
|
||||
epic: library-evaluation-policy
|
||||
title: CLAUDE.md Key Conventions — library policy bullet
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: docs
|
||||
depends-on: [05-human-guide]
|
||||
blocks: []
|
||||
created: 2026-05-14T06:52:02+02:00
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add one bullet to the "Key Conventions" section of `CLAUDE.md` pointing agents and developers at ADR-022, the `evaluate-library` skill, and `docs/guides/adding-a-library.md`. No other changes to `CLAUDE.md`.
|
||||
|
||||
## Why
|
||||
|
||||
`CLAUDE.md` is the first file agents and developers read. Without a Key Conventions entry, the library evaluation policy is invisible to any agent starting a fresh session — it may run `pnpm add` without knowing the policy exists, and only hit the hook or pre-commit gate after the fact. The bullet closes the discoverability gap.
|
||||
|
||||
## Done when
|
||||
|
||||
- `CLAUDE.md` Key Conventions section contains a bullet: _"New runtime dependencies in feature- or core-tier packages require a library trace at `docs/library-decisions/<date>-<name>.md` produced by the `/evaluate-library` skill — see ADR-022 and `docs/guides/adding-a-library.md`."_ (exact wording may vary; substance must include ADR-022, the skill, and the guide path).
|
||||
- No other changes to `CLAUDE.md`.
|
||||
- `pnpm lint && pnpm fallow:audit` pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `CLAUDE.md` — one bullet in Key Conventions.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `docs/glossary.md` — the glossary entries for **Library trace** and **Pre-shipped trace** landed during the 2026-05-14 grill session and are already present.
|
||||
- Any other documentation changes.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add the one-line library-policy bullet to CLAUDE.md Key Conventions (after confirming glossary entries are already present); all gates pass on this single commit.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
id: library-evaluation-policy
|
||||
prd: docs/work/prds/library-evaluation-policy.prd.md
|
||||
title: Library evaluation policy — skill, traces, enforcement stack
|
||||
type: epic
|
||||
status: done
|
||||
features: [scripts, tooling, docs]
|
||||
created: 2026-05-14T00:00:00Z
|
||||
updated: 2026-05-14T19:21:52.308Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Implement a four-layer enforcement stack — Claude hook, skill, pre-commit hook, sandcastle reviewer prompt — that makes every new runtime dependency in a feature- or core-tier package produce a permanent **library trace** at `docs/library-decisions/<YYYY-MM-DD>-<package-name>.md`. Rejection traces are first-class records. Codifies ADR-022.
|
||||
|
||||
## Why
|
||||
|
||||
The repo's narrow third-party surface is uncodified. New dependencies enter via `pnpm add` with no checkpoint. Three signals exposed the gap: a near-miss adding a build-time-only library, post-hoc ADR records (002/014/017), and a silent EU-data-residency risk from US-only SaaS defaults. The enforcement stack mirrors the 5-gate conformance pattern — same vocabulary, same agent feedback loop.
|
||||
|
||||
## Stories
|
||||
|
||||
- [x] [01 — Trace schema module + docs/library-decisions/ foundation](01-trace-schema-foundation/_story.md)
|
||||
- [x] [02 — Pre-commit check script](02-pre-commit-check-script/_story.md)
|
||||
- [x] [03 — Claude PreToolUse / PostToolUse hooks](03-claude-hooks/_story.md)
|
||||
- [x] [04 — evaluate-library skill](04-evaluate-library-skill/_story.md)
|
||||
- [x] [05 — Human guide: docs/guides/adding-a-library.md](05-human-guide/_story.md)
|
||||
- [x] [06 — Sandcastle reviewer prompt update](06-sandcastle-reviewer-prompt/_story.md)
|
||||
- [x] [07 — Generator pre-shipped traces for optional cores](07-generator-pre-shipped-traces/_story.md)
|
||||
- [x] [08 — Backfill traces for existing runtime deps](08-backfill-traces/_story.md)
|
||||
- [x] [09 — CLAUDE.md Key Conventions bullet](09-claude-md-update/_story.md)
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
id: 01-scaffold-core-analytics-package
|
||||
epic: product-analytics-channel
|
||||
title: Scaffold @repo/core-analytics package with IAnalytics and NoopAnalytics
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-analytics
|
||||
depends-on: []
|
||||
blocks:
|
||||
[
|
||||
02-recording-analytics,
|
||||
03-analyzed-brand-and-with-analytics-wrapper,
|
||||
06-analytics-protocol-bind-context,
|
||||
08-react-provider,
|
||||
]
|
||||
created: 2026-05-18T12:00:00Z
|
||||
updated: 2026-05-18T11:33:06.880Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Produce a green `@repo/core-analytics` package — generator-scaffolded skeleton, then fleshed out with `IAnalytics` interface (`track`, `identify`, `pageView`, `flush`), `NoopAnalytics` default implementation, supporting types (`AnalyticsAttributeValue`, `AnalyticsUser`), and full method-level test coverage. The package is vendor-neutral: no third-party analytics SDK is bundled.
|
||||
|
||||
## Why
|
||||
|
||||
All downstream stories depend on `IAnalytics` existing as the contract surface. Landing this first means every subsequent story compiles and imports from a real package rather than a placeholder.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-analytics/` exists as a workspace package `@repo/core-analytics`.
|
||||
- `IAnalytics`, `AnalyticsAttributeValue`, `AnalyticsUser` are exported from the package root.
|
||||
- `NoopAnalytics` implements `IAnalytics`; `flush()` resolves with a microtask (`Promise.resolve()`).
|
||||
- Sibling tests cover all four interface methods on `NoopAnalytics`.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-analytics/` — package scaffold, `IAnalytics` interface, `AnalyticsAttributeValue` + `AnalyticsUser` types, `NoopAnalytics` impl, sibling tests, root barrel export.
|
||||
- No subpath exports in this story (`./react` lands in Story 08).
|
||||
- No `withAnalytics` wrapper in this story (Story 03).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `RecordingAnalytics` (Story 02).
|
||||
- `Analyzed` brand / `withAnalytics` wrapper (Story 03).
|
||||
- React provider (Story 08).
|
||||
- Backend vendor integration — template ships Noop only.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Run `pnpm turbo gen core-package analytics` to scaffold `@repo/core-analytics` — verify the generator output compiles and all gates pass on this commit alone.
|
||||
- [x] Replace generator placeholder content with `IAnalytics` interface (`track`, `identify`, `pageView`, `flush`), `AnalyticsAttributeValue` + `AnalyticsUser` types, and `NoopAnalytics` implementation (`flush()` returns `Promise.resolve()`); add sibling tests covering all four methods; export everything from the package root barrel — all gates pass on this commit.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
id: 02-recording-analytics
|
||||
epic: product-analytics-channel
|
||||
title: Add RecordingAnalytics to @repo/core-testing
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-testing
|
||||
depends-on: [01-scaffold-core-analytics-package]
|
||||
blocks: [08-react-provider]
|
||||
created: 2026-05-18T12:01:00Z
|
||||
updated: 2026-05-18T11:38:17.680Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add `RecordingAnalytics` to `@repo/core-testing` as an in-memory test double that implements `IAnalytics`, parallel to `RecordingAuditLog`. Consumers use it in unit tests and the React provider test.
|
||||
|
||||
## Why
|
||||
|
||||
Every story that needs to assert analytics behaviour (including the React provider test in Story 08) requires a deterministic implementation. `RecordingAnalytics` provides the same recording double pattern already established by `RecordingAuditLog`.
|
||||
|
||||
## Done when
|
||||
|
||||
- `RecordingAnalytics` implements `IAnalytics` and records calls to `tracked`, `identified`, `pageViewed` arrays.
|
||||
- `flush()` resolves with a microtask and clears or drains the in-memory buffer.
|
||||
- Exported from `@repo/core-testing`'s barrel.
|
||||
- Sibling test covers all four methods and the flush drain behaviour.
|
||||
- All gates pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-testing/src/recording-analytics.ts` + sibling test.
|
||||
- Export line in `packages/core-testing/src/index.ts`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- React provider wiring (Story 08).
|
||||
- Any vendor-specific recording behaviour.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `RecordingAnalytics` implementing `IAnalytics` to `@repo/core-testing` — recorded arrays for `track`, `identify`, `pageView`; `flush()` returns `Promise.resolve()`; sibling test covers all methods and flush; export added to barrel — all gates pass on this commit.
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: 03-analyzed-brand-and-with-analytics-wrapper
|
||||
epic: product-analytics-channel
|
||||
title: Add Analyzed brand and withAnalytics wrapper
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: [01-scaffold-core-analytics-package]
|
||||
blocks: [04-manifest-schema-and-wire-use-case]
|
||||
created: 2026-05-18T12:02:00Z
|
||||
updated: 2026-05-18T11:55:08.463Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add the `Analyzed<F>` brand to `core-shared/conformance/` and the `withAnalytics(analytics, factory)` higher-order wrapper to `@repo/core-analytics`. Together they enable brand-based conformance detection at DI bind time, mirroring the `Audited` / `withAudit` pattern.
|
||||
|
||||
## Why
|
||||
|
||||
`wireUseCase` (Story 04) and `assertFeatureConformance` (Story 05) both key off the `Analyzed` brand at runtime. The brand must exist in `core-shared` (so the assertion layer can import it without a circular dep) while the wrapper lives in `core-analytics` (analytics is optional; can't pollute core-shared with the implementation).
|
||||
|
||||
## Done when
|
||||
|
||||
- `Analyzed<F> = F & { readonly __analyzed: true }` exists in `packages/core-shared/src/conformance/brands.ts` and `isAnalyzed` guard exists in `packages/core-shared/src/conformance/brand-runtime.ts`.
|
||||
- `withAnalytics(analytics, factory)` lives in `packages/core-analytics/src/with-analytics.ts`, attaches `Analyzed` via `attachBrand` from `core-shared/conformance/brand-runtime`, and is exported from `@repo/core-analytics`.
|
||||
- `with-analytics.test.ts` asserts brand is present after wrapping and absent before.
|
||||
- All gates pass on each commit independently.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-shared/src/conformance/brands.ts` — `Analyzed<F>` type addition.
|
||||
- `packages/core-shared/src/conformance/brand-runtime.ts` — `isAnalyzed` guard.
|
||||
- `packages/core-core-shared/src/conformance/index.ts` — export additions.
|
||||
- `packages/core-analytics/src/with-analytics.ts` + sibling test.
|
||||
- `packages/core-analytics/src/index.ts` — export addition.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `wireUseCase` composition (Story 04).
|
||||
- `assertFeatureConformance` extension (Story 05).
|
||||
- `analyticsEvents` manifest field (Story 04).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `Analyzed<F>` brand type to `packages/core-shared/src/conformance/brands.ts` and `isAnalyzed(f): f is Analyzed<F>` guard to `brand-runtime.ts`; export both from the conformance index — all gates pass on this commit.
|
||||
- [x] Add `withAnalytics(analytics, factory)` wrapper to `packages/core-analytics/src/with-analytics.ts` using `attachBrand` from `core-shared/conformance/brand-runtime` + `with-analytics.test.ts` asserting brand attached after wrapping and absent before; export from `@repo/core-analytics` root barrel — all gates pass on this commit.
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
id: 04-manifest-schema-and-wire-use-case
|
||||
epic: product-analytics-channel
|
||||
title: Extend manifest schema and wireUseCase with analyticsEvents
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: [03-analyzed-brand-and-with-analytics-wrapper]
|
||||
blocks:
|
||||
[
|
||||
05-assert-feature-conformance-analyzed,
|
||||
07-eslint-rule-no-undeclared-analytics-event,
|
||||
]
|
||||
created: 2026-05-18T12:03:00Z
|
||||
updated: 2026-05-18T15:15:39.064Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add `analyticsEvents: string[]` (default `[]`) to the per-use-case manifest schema in `define-feature.ts`, update the feature generator template to emit the field, and extend `wireUseCase` to compose `withAnalytics` when an `analytics` arg is provided and the use case declares at least one analytics event.
|
||||
|
||||
## Why
|
||||
|
||||
The manifest field is the declaration surface the ESLint rule (Story 07) and boot assertion (Story 05) both read from. `wireUseCase` is the composition site that attaches the `Analyzed` brand; it must compose analytics in the right position (`factory → withAnalytics → withAudit → withCapture → withSpan`).
|
||||
|
||||
## Done when
|
||||
|
||||
- `useCases.<name>.analyticsEvents: string[]` is valid in every feature manifest; existing manifests stay green with an absent or empty field.
|
||||
- The feature generator template emits `analyticsEvents: []` for each scaffolded use case.
|
||||
- `wireUseCase({ ..., analytics? })` composes `withAnalytics` when `analytics` is provided; existing callers without `analytics` are unaffected.
|
||||
- `wire-use-case.test.ts` covers the analytics path (brand present) and the no-analytics path (brand absent).
|
||||
- All gates pass on each commit independently.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-shared/src/conformance/define-feature.ts` — schema extension.
|
||||
- Feature generator template (`turbo/generators/`) — `analyticsEvents: []` added to use-case scaffold output.
|
||||
- `packages/core-shared/src/conformance/wire-use-case.ts` — `analytics?` arg + `withAnalytics` composition.
|
||||
- `packages/core-shared/src/conformance/wire-use-case.test.ts` — analytics path coverage.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `assertFeatureConformance` extension (Story 05).
|
||||
- ESLint rule (Story 07).
|
||||
- Migrating existing feature manifests (out of scope per PRD).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `analyticsEvents: string[]` (default `[]`) to the use-case schema in `define-feature.ts` and update the feature generator template to emit the field — existing manifests remain valid; all gates pass on this commit.
|
||||
- [x] Extend `wireUseCase` to accept optional `analytics` arg and compose `withAnalytics` (innermost, before `withAudit`) when provided + update `wire-use-case.test.ts` with analytics path (Analyzed brand present) and no-analytics path (Analyzed brand absent) — all gates pass on this commit.
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: 05-assert-feature-conformance-analyzed
|
||||
epic: product-analytics-channel
|
||||
title: Extend assertFeatureConformance to check Analyzed brand
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: [04-manifest-schema-and-wire-use-case]
|
||||
blocks:
|
||||
[
|
||||
06-analytics-protocol-bind-context,
|
||||
07-eslint-rule-no-undeclared-analytics-event,
|
||||
]
|
||||
created: 2026-05-18T12:04:00Z
|
||||
updated: 2026-05-18T15:36:52.400Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Extend `assertFeatureConformance` to reject a binding at boot time when the manifest's use case declares `analyticsEvents.length > 0` but the bound function does not carry the `Analyzed` brand. Error message names the use case and the missing brand.
|
||||
|
||||
## Why
|
||||
|
||||
This is the boot-time gate that prevents analytics events from being declared in the manifest but silently skipped at runtime. Mirrors the existing `Audited` check for the same guarantee at the same latency.
|
||||
|
||||
## Done when
|
||||
|
||||
- `assertFeatureConformance` throws `ConformanceError` naming the missing `Analyzed` brand when `analyticsEvents.length > 0` and the bound function is not `isAnalyzed`.
|
||||
- A synthetic conformance test (parallel to `assert-bindings.test.ts`) covers: passes when Analyzed present + events declared, throws with message naming `Analyzed` when events declared + brand missing, passes when events empty + brand absent.
|
||||
- All gates pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-shared/src/conformance/assert-bindings.ts` — Analyzed check addition.
|
||||
- Conformance test file for the new assertion path.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `BindContext.analytics` field (Story 06).
|
||||
- ESLint rule (Story 07).
|
||||
- Any template feature wiring analytics (out of scope per PRD).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `Analyzed` brand check to `assertFeatureConformance` when `manifest.useCases[name].analyticsEvents.length > 0` + synthetic conformance test asserting `ConformanceError` with message naming `Analyzed` for unwrapped bindings — all gates pass on this commit.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
id: 06-analytics-protocol-bind-context
|
||||
epic: product-analytics-channel
|
||||
title: Add AnalyticsProtocol and BindContext.analytics
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: [05-assert-feature-conformance-analyzed]
|
||||
blocks: [09-documentation]
|
||||
created: 2026-05-18T12:05:00Z
|
||||
updated: 2026-05-18T15:40:20.464Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add `AnalyticsProtocol` to `core-shared/di/bind-protocols.ts` and an optional `analytics?: AnalyticsProtocol` field to `BindContext`. This is the DI seam that lets feature binders receive an analytics implementation at bind time without importing the concrete class.
|
||||
|
||||
## Why
|
||||
|
||||
Feature binders that need analytics accept `ctx.analytics` from the aggregator's `BindContext`. Without the protocol type and context field, there's no typed path for passing analytics through the DI layer. Mirrors how `IEventBus`, `IAuditLog`, and `IJobQueue` are threaded through `BindContext`.
|
||||
|
||||
## Done when
|
||||
|
||||
- `AnalyticsProtocol` structural type exists in `packages/core-shared/src/di/bind-protocols.ts` and is exported.
|
||||
- `BindContext` (and `BindProductionContext`) gain `analytics?: AnalyticsProtocol`.
|
||||
- Existing binder call-sites compile without changes (field is optional).
|
||||
- All gates pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-shared/src/di/bind-protocols.ts` — `AnalyticsProtocol` type.
|
||||
- `packages/core-shared/src/di/bind-context.ts` — `analytics?:` field on both context shapes.
|
||||
- Exports from `@repo/core-shared/di` barrel.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Wiring analytics in any actual feature binder (no template feature adopts analytics per PRD).
|
||||
- `IAnalytics` class methods — `AnalyticsProtocol` is the structural type used for DI; it mirrors the `IAnalytics` shape but lives in `core-shared` to avoid a circular dependency.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `AnalyticsProtocol` structural type to `packages/core-shared/src/di/bind-protocols.ts` and `analytics?: AnalyticsProtocol` to `BindContext` + `BindProductionContext`; export from the `@repo/core-shared/di` barrel — all gates pass on this commit.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: 07-eslint-rule-no-undeclared-analytics-event
|
||||
epic: product-analytics-channel
|
||||
title: Add no-undeclared-analytics-event ESLint rule to @repo/core-eslint
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-eslint
|
||||
depends-on: [05-assert-feature-conformance-analyzed]
|
||||
blocks: [09-documentation]
|
||||
created: 2026-05-18T12:06:00Z
|
||||
updated: 2026-05-18T15:44:55.963Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add the `conformance/no-undeclared-analytics-event` rule to `@repo/core-eslint` at warn severity. The rule finds `analytics.track("X", ...)` string-literal calls in `*.use-case.ts` files and warns when `"X"` is not declared in the file's feature manifest under `analyticsEvents`. Mirrors `no-undeclared-audit` and `no-undeclared-event-publish`.
|
||||
|
||||
## Why
|
||||
|
||||
Boot-time conformance (Story 05) catches missing brands at process start, but it can't catch event-slug typos or new undeclared slugs introduced during development before the app is ever booted. The ESLint rule provides sub-second feedback in the editor and in CI, closing the latency gap.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-eslint/rules/no-undeclared-analytics-event.js` exists and passes RuleTester fixtures.
|
||||
- Fixtures cover: declared slug → no warning, undeclared slug → warn, non-use-case file → no-op, manifest with no use cases → no-op.
|
||||
- `_manifest-ast.js` parses `analyticsEvents` arrays (extends existing AST helper).
|
||||
- Rule registered in `plugin.js` and `base.js` at `"warn"` severity.
|
||||
- `pnpm lint` runs the rule; all gates pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-eslint/rules/no-undeclared-analytics-event.js` + RuleTester test file.
|
||||
- `packages/core-eslint/rules/_manifest-ast.js` — `analyticsEvents` parsing addition.
|
||||
- `packages/core-eslint/plugin.js` + `packages/core-eslint/base.js` — rule registration at warn.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Migrating existing use cases (no template feature declares analytics events per PRD).
|
||||
- Auto-fix — warn only, no `--fix` path in this slice.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `no-undeclared-analytics-event.js` rule to `@repo/core-eslint` — rule implementation cross-checking `analytics.track("X", ...)` literal slug against manifest `analyticsEvents`, extend `_manifest-ast.js` to parse the field, RuleTester fixtures (declared passes, undeclared warns, non-use-case no-op, no-manifest no-op), register in `plugin.js` + `base.js` at `"warn"` — all gates pass on this commit.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: 08-react-provider
|
||||
epic: product-analytics-channel
|
||||
title: Add React provider to @repo/core-analytics
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-analytics
|
||||
depends-on: [02-recording-analytics]
|
||||
blocks: [09-documentation]
|
||||
created: 2026-05-18T12:07:00Z
|
||||
updated: 2026-05-18T15:56:54.277Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add a `./react` subpath export to `@repo/core-analytics` containing `<AnalyticsProvider value={IAnalytics}>` and `useAnalytics(): IAnalytics`. `useAnalytics()` throws a clear error when called outside a provider. No auto-wired router events.
|
||||
|
||||
## Why
|
||||
|
||||
Server-side use cases receive analytics through `BindContext.analytics`. Client-side components need an equivalent contract surface so `analytics.track(...)` reads the same whether called from a use case or a React component. The provider bridges `IAnalytics` from DI land to React's context tree without coupling consumers to a specific vendor.
|
||||
|
||||
## Done when
|
||||
|
||||
- `@repo/core-analytics/react` subpath exports `AnalyticsProvider` and `useAnalytics`.
|
||||
- `useAnalytics()` throws a named error (`AnalyticsContextError` or similar) when called outside `<AnalyticsProvider>`.
|
||||
- React Testing Library test: render a child inside `<AnalyticsProvider value={recordingAnalytics}>`, child calls `useAnalytics().track("test.event")`, assert `recordingAnalytics.tracked` contains the event.
|
||||
- All gates pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `packages/core-analytics/src/react/analytics-provider.tsx` + `useAnalytics.ts` (or colocated).
|
||||
- `packages/core-analytics/src/react/index.ts` — subpath barrel.
|
||||
- `package.json` `exports` map: `"./react": "./dist/react/index.js"` (or equivalent for the build config).
|
||||
- React Testing Library test.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Auto-wiring Next App Router or TanStack Router route-change hooks (deferred per PRD).
|
||||
- SSR hydration concerns — provider is a pure context wrapper; consumers handle hydration.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `AnalyticsProvider` + `useAnalytics()` to `packages/core-analytics/src/react/`, wire `./react` subpath export in `package.json`, write React Testing Library test using `RecordingAnalytics` asserting `track` flows through context — all gates pass on this commit.
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
id: 09-documentation
|
||||
epic: product-analytics-channel
|
||||
title: Documentation — analytics.md, conformance-quickref, CLAUDE.md, template-tiers
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: docs
|
||||
depends-on:
|
||||
[
|
||||
06-analytics-protocol-bind-context,
|
||||
07-eslint-rule-no-undeclared-analytics-event,
|
||||
08-react-provider,
|
||||
]
|
||||
blocks: []
|
||||
created: 2026-05-18T12:08:00Z
|
||||
updated: 2026-05-18T16:05:30.519Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Write `docs/guides/analytics.md` and update `docs/guides/conformance-quickref.md`, `CLAUDE.md`, and `docs/architecture/template-tiers.md` to reflect the completed channel. All docs reference the final shipped shape.
|
||||
|
||||
## Why
|
||||
|
||||
Docs land last (per PRD sequencing) so they can reference the real API rather than draft shapes. The conformance-quickref and CLAUDE.md rule-count bump are load-bearing for agents that use those files as context pointers.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/guides/analytics.md` exists and documents: server-side wiring path (`BindContext.analytics` → feature binder), client-side wiring path (`<AnalyticsProvider>` + `useAnalytics()`), consumer vendor evaluation step (ADR-022 `/evaluate-library` gate), and PII boundary deferral (ADR-024 §"PII boundary").
|
||||
- `docs/guides/conformance-quickref.md` rule table shows 7 ESLint rules (adds `no-undeclared-analytics-event`) and drift patterns include analytics-event sprawl.
|
||||
- `CLAUDE.md` rule count updated from 6 to 7 in the conformance ESLint rules list.
|
||||
- `docs/architecture/template-tiers.md` lists `core-analytics` in the optional-cores table.
|
||||
- All gates pass on each commit independently.
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/guides/analytics.md` — new guide.
|
||||
- `docs/guides/conformance-quickref.md` — rule table + drift patterns update.
|
||||
- `CLAUDE.md` — conformance rule count line update.
|
||||
- `docs/architecture/template-tiers.md` — optional-cores list update.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Glossary update (already done in the ADR-024 commit per PRD).
|
||||
- ADR-024 itself (already exists).
|
||||
- Storybook component or demo app (deferred per PRD).
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Write `docs/guides/analytics.md` covering server-side wiring (`BindContext.analytics`), client-side wiring (`<AnalyticsProvider>` + `useAnalytics()`), vendor evaluation step (ADR-022), and PII boundary note — all gates pass on this commit.
|
||||
- [x] Update `docs/guides/conformance-quickref.md` (seventh rule + analytics-event drift pattern), `CLAUDE.md` (6 → 7 conformance ESLint rules), and `docs/architecture/template-tiers.md` (add `core-analytics` to optional-cores list) — all gates pass on this commit.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
id: product-analytics-channel
|
||||
prd: docs/work/prds/product-analytics-channel.prd.md
|
||||
title: Product analytics as a fourth capture channel (ADR-024 implementation)
|
||||
type: epic
|
||||
status: done
|
||||
features: [core-analytics, core-shared, core-testing, core-eslint]
|
||||
created: 2026-05-18T12:00:00Z
|
||||
updated: 2026-05-18T16:05:30.519Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Ship `@repo/core-analytics` as an optional core package that codifies product analytics as the fourth capture channel alongside `ITracer`, `ILogger`, and `IAuditLog`. Adds `IAnalytics` interface, `NoopAnalytics` + `RecordingAnalytics` implementations, `Analyzed` brand, `withAnalytics` wrapper, `analyticsEvents` manifest field, `assertFeatureConformance` extension, `AnalyticsProtocol` in `BindContext`, `no-undeclared-analytics-event` ESLint rule, and a React provider scaffold — all mirroring the audit channel shape with ADR-024's three deliberate divergences.
|
||||
|
||||
## Why
|
||||
|
||||
Consumers routinely bolt analytics SDKs on at the React component layer, bypassing manifests, brands, and the five-gate conformance system. Codifying analytics as a channel with the same structural shape as `core-audit` means the conformance gates extend to a fourth signal at zero new gate count.
|
||||
|
||||
## Stories
|
||||
|
||||
- [x] [01 — Scaffold @repo/core-analytics package](01-scaffold-core-analytics-package/_story.md)
|
||||
- [x] [02 — Add RecordingAnalytics to @repo/core-testing](02-recording-analytics/_story.md)
|
||||
- [x] [03 — Add Analyzed brand and withAnalytics wrapper](03-analyzed-brand-and-with-analytics-wrapper/_story.md)
|
||||
- [x] [04 — Extend manifest schema and wireUseCase with analyticsEvents](04-manifest-schema-and-wire-use-case/_story.md)
|
||||
- [x] [05 — Extend assertFeatureConformance to check Analyzed brand](05-assert-feature-conformance-analyzed/_story.md)
|
||||
- [x] [06 — Add AnalyticsProtocol and BindContext.analytics](06-analytics-protocol-bind-context/_story.md)
|
||||
- [x] [07 — Add no-undeclared-analytics-event ESLint rule](07-eslint-rule-no-undeclared-analytics-event/_story.md)
|
||||
- [x] [08 — Add React provider to @repo/core-analytics](08-react-provider/_story.md)
|
||||
- [x] [09 — Documentation](09-documentation/_story.md)
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: 01-rate-limit-type-primitives
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: Rate-limit type primitives and manifest field
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: []
|
||||
blocks:
|
||||
[
|
||||
02-rate-limit-implementations,
|
||||
03-no-undeclared-rate-limit-eslint-rule,
|
||||
04-with-rate-limit-wrapper-and-conformance,
|
||||
]
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T08:24:33.292Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add the foundational TypeScript types for the rate-limit primitive — `IRateLimit`, `RateLimitBudget`, `RateLimitDecision`, and the `RateLimited` brand — to `core-shared`, and extend the feature manifest schema with `rateLimit?: RateLimitBudget[]` so every subsequent story has a stable, schema-valid type surface to build on.
|
||||
|
||||
## Why
|
||||
|
||||
Manifest-first ordering requires types before implementations and lint rules. The `RateLimited` brand must exist in `core-shared/conformance/brands.ts` before `assertFeatureConformance` can enforce it; `RateLimitBudget` must be a valid manifest field before any feature can declare rate-limit gates; `IRateLimit` must be exported before any implementation or wrapper can reference it. All three land together because they are mutually referential and individually incomplete.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-shared/src/rate-limit/rate-limit.interface.ts` exports `IRateLimit`, `RateLimitBudget`, `RateLimitDecision`.
|
||||
- `packages/core-shared/src/conformance/brands.ts` exports `RateLimited<F>` brand and `isRateLimited(fn): boolean` helper.
|
||||
- `UseCaseManifest` in `packages/core-shared/src/conformance/define-feature.ts` gains `rateLimit?: RateLimitBudget[]` (absent defaults to `[]`).
|
||||
- New types exported from the `core-shared` barrel.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `IRateLimit` interface: `consume(budgetName, key, weight?): Promise<RateLimitDecision>` and `reset(budgetName, key): Promise<void>`.
|
||||
- `RateLimitBudget` type: `{ name: string; window: string; budget: number }`.
|
||||
- `RateLimitDecision` type: `{ allowed: boolean; remaining: number; resetAt: Date }`.
|
||||
- `RateLimited<F>` brand in `conformance/brands.ts` following the `Captured`, `ConsentChecked` pattern.
|
||||
- `isRateLimited(fn): boolean` brand-check helper.
|
||||
- `rateLimit?: RateLimitBudget[]` field added to `UseCaseManifest` in `define-feature.ts`.
|
||||
- Barrel export from `core-shared`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Implementations (`NoopRateLimit`, `InMemoryRateLimit`, `RecordingRateLimit`) — Story 02.
|
||||
- `withRateLimit` wrapper, `assertFeatureConformance` extension, `wireUseCase` extension — Story 04.
|
||||
- ESLint rule — Story 03.
|
||||
- Auth backfill — Story 05.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `IRateLimit`, `RateLimitBudget`, `RateLimitDecision` types in `packages/core-shared/src/rate-limit/rate-limit.interface.ts`; add `RateLimited<F>` brand + `isRateLimited` helper in `packages/core-shared/src/conformance/brands.ts`; extend `UseCaseManifest` in `packages/core-shared/src/conformance/define-feature.ts` with `rateLimit?: RateLimitBudget[]`; export new types from the `core-shared` barrel; all gates pass.
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
id: 02-rate-limit-implementations
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: Rate-limit implementations — Noop, InMemory, Recording
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: [01-rate-limit-type-primitives]
|
||||
blocks:
|
||||
[
|
||||
04-with-rate-limit-wrapper-and-conformance,
|
||||
05-auth-signin-rate-limit-backfill,
|
||||
]
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T08:38:25.271Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Provide three working `IRateLimit` implementations that cover dev, test, and production-delegate scenarios: `NoopRateLimit` (always-allow, zero overhead), `InMemoryRateLimit` (per-process fixed-window with check-at-read expiry), and `RecordingRateLimit` (test helper in `core-testing` that captures all calls for assertions).
|
||||
|
||||
## Why
|
||||
|
||||
The `withRateLimit` wrapper (Story 04) and the `auth.signIn` backfill (Story 05) both need concrete classes to wire. `NoopRateLimit` is also the required default in `BindContext` so apps without a wired rate-limit implementation boot safely. `RecordingRateLimit` is the testing primitive every future rate-limited use-case test will use; landing it here alongside the production impls keeps the testing toolkit coherent.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-shared/src/rate-limit/noop-rate-limit.ts` exports `NoopRateLimit` with unit test: `consume` always resolves `{ allowed: true, remaining: Infinity, resetAt: epoch }`, `reset` is a no-op.
|
||||
- `packages/core-shared/src/rate-limit/in-memory-rate-limit.ts` exports `InMemoryRateLimit` using check-at-read fixed-window expiry (no `setTimeout`); unit test: per-bucket tracking, decrement on `consume`, reset at window boundary (synthetic clock), explicit `reset` call restores budget to declared value.
|
||||
- `packages/core-testing/src/rate-limit/recording-rate-limit.ts` exports `RecordingRateLimit` capturing `consume` + `reset` invocation arguments; sibling test verifies capture correctness.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass after each task.
|
||||
|
||||
## In scope
|
||||
|
||||
- `NoopRateLimit` — always-allow impl, no state, no timers.
|
||||
- `InMemoryRateLimit` — `Map`-backed, per-bucket `{ count, resetAt }`, check-at-read expiry, clock injection via constructor arg for testability.
|
||||
- `RecordingRateLimit` — in `packages/core-testing`; exposes `consumeCalls` + `resetCalls` accessors for test assertions.
|
||||
- Barrel exports from `core-shared` and `core-testing`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Redis-backed or any distributed impl — deferred, consumer wires via ADR-022 library evaluation.
|
||||
- Token-bucket algorithm — InMemoryRateLimit uses fixed-window for simplicity; documented as dev-only.
|
||||
- `withRateLimit` wrapper — Story 04.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Implement `NoopRateLimit` in `packages/core-shared/src/rate-limit/noop-rate-limit.ts` with sibling unit test (consume always allowed, reset no-op); implement `InMemoryRateLimit` in `packages/core-shared/src/rate-limit/in-memory-rate-limit.ts` with sibling unit test (per-bucket tracking, check-at-read expiry via injected clock, explicit reset restores budget); export both from `core-shared` barrel; all gates pass.
|
||||
- [x] Implement `RecordingRateLimit` in `packages/core-testing/src/rate-limit/recording-rate-limit.ts` capturing `consume` + `reset` call arguments verbatim with `consumeCalls` + `resetCalls` accessors; sibling test verifies capture correctness; export from `core-testing` barrel; all gates pass.
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: 03-no-undeclared-rate-limit-eslint-rule
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: "`no-undeclared-rate-limit` ESLint rule"
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-eslint
|
||||
depends-on: [01-rate-limit-type-primitives]
|
||||
blocks: [05-auth-signin-rate-limit-backfill]
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T08:45:03.170Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add the `no-undeclared-rate-limit` ESLint rule (warn severity) that catches `rateLimit.consume("X", _)` calls where `"X"` is not declared in the manifest's `rateLimit` array, and declared budget names that are never consumed in the use-case body — giving AI agents and developers lint-time enforcement of rate-limit drift.
|
||||
|
||||
## Why
|
||||
|
||||
Without a lint rule, an agent could add a `consume("foo", ...)` call that has no corresponding manifest declaration, or declare a budget that silently goes unused. The ESLint rule closes this gap at the same latency as the existing `no-undeclared-audit` and `no-undeclared-consent-check` rules, making the rate-limit channel structurally consistent with the rest of the conformance system.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-eslint/rules/no-undeclared-rate-limit.js` is registered at warn severity in `plugin.js` + `base.js`.
|
||||
- `packages/core-eslint/rules/_manifest-ast.js` parser extracts the `rateLimit` field from a feature manifest.
|
||||
- RuleTester fixtures cover: matching `budgetName` in call and manifest (pass), `budgetName` in call absent from manifest (warn), declared budget never consumed in use-case body (warn), non-use-case file (no-op).
|
||||
- Prior-art shape mirrors `no-undeclared-audit.js` and `no-undeclared-consent-check.js`.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `no-undeclared-rate-limit.js` rule implementation.
|
||||
- `_manifest-ast.js` extension for `rateLimit` field extraction.
|
||||
- Rule registration in `plugin.js` + `base.js` at warn severity.
|
||||
- RuleTester fixtures (declared/undeclared/unused/non-use-case).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- CLAUDE.md + `conformance-quickref.md` rule-count bump (12 → 13) — Story 11.
|
||||
- `withRateLimit` wrapper — Story 04.
|
||||
- Auth backfill applying the rule — Story 05.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `no-undeclared-rate-limit` rule in `packages/core-eslint/rules/no-undeclared-rate-limit.js` (warn severity); extend `packages/core-eslint/rules/_manifest-ast.js` to extract the `rateLimit` field; register the rule in `plugin.js` + `base.js`; add RuleTester fixtures: declared budget + matching call (pass), undeclared budget name in call (warn), declared budget never consumed (warn), non-use-case file (no-op); all gates pass.
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
id: 04-with-rate-limit-wrapper-and-conformance
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: "`withRateLimit` wrapper and conformance extensions"
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: [01-rate-limit-type-primitives, 02-rate-limit-implementations]
|
||||
blocks: [05-auth-signin-rate-limit-backfill]
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T09:05:31.838Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Ship the `withRateLimit` wrapper (attaches `RateLimited` brand at DI bind time), extend `wireUseCase` to compose it innermost when `rateLimit.length > 0`, extend `assertFeatureConformance` to require the brand when a manifest declares rate-limit budgets, and add `rateLimit?: IRateLimit` to `BindContext` defaulting to `NoopRateLimit` — completing the full conformance enforcement loop for rate-limit.
|
||||
|
||||
## Why
|
||||
|
||||
The brand enforcement loop must be complete before any feature can declare `rateLimit` in its manifest and claim conformance. A partial landing (e.g. wrapper without `assertFeatureConformance`, or `BindContext` without a default) leaves the conformance layer in an undefined state: either boot assertions silently skip the brand check, or apps fail to boot because `ctx.rateLimit` is undefined. All four changes land together as a single coherent slice.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-shared/src/rate-limit/with-rate-limit.ts` exports `withRateLimit(rateLimit, factory)` attaching the `RateLimited` brand; unit tests assert brand is attached, factory passthrough is preserved, and wrapper composes correctly with other wrappers in the canonical order.
|
||||
- `wireUseCase` in `packages/core-shared/src/conformance/wire-use-case.ts` accepts optional `rateLimit?: IRateLimit` and composes `withRateLimit` innermost (after `withConsent`) when `manifest.rateLimit.length > 0`; existing `wireUseCase` tests remain green.
|
||||
- `assertFeatureConformance` in `packages/core-shared/src/conformance/assert-bindings.ts` requires the `RateLimited` brand when `manifest.useCases[name].rateLimit.length > 0`; synthetic fixture test asserts `ConformanceError` is thrown when the brand is absent.
|
||||
- `packages/core-shared/src/di/bind-context.ts` gains `rateLimit?: IRateLimit`; the app aggregator in `apps/web-next/src/server/bind-production.ts` (and equivalents) defaults to `new NoopRateLimit()` when the consumer does not wire a backend.
|
||||
- Canonical wrapper composition order confirmed: `withSpan → withCapture → withAudit → withAnalytics → withConsent → withRateLimit → factory(deps)`.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `withRateLimit(rateLimit, factory)` wrapper in `core-shared/rate-limit/with-rate-limit.ts`.
|
||||
- `wireUseCase` extension accepting `rateLimit?: IRateLimit` and composing `withRateLimit` innermost.
|
||||
- `assertFeatureConformance` extension: require `RateLimited` brand when `rateLimit.length > 0`.
|
||||
- Synthetic fixture test for the boot assertion failure case.
|
||||
- `BindContext.rateLimit?: IRateLimit` — app aggregators default to `new NoopRateLimit()`.
|
||||
- Mirror prior art: `with-capture.ts` / `with-consent.ts` wrapper pattern; `wire-use-case.ts` + `assert-bindings.ts` extension pattern.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `NoopRateLimit` implementation — Story 02 (required as dependency).
|
||||
- Auth backfill — Story 05.
|
||||
- ESLint rule — Story 03.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Implement `withRateLimit(rateLimit, factory)` in `packages/core-shared/src/rate-limit/with-rate-limit.ts` attaching the `RateLimited` brand; extend `wireUseCase` to accept optional `rateLimit?: IRateLimit` and compose `withRateLimit` innermost when `manifest.rateLimit.length > 0`; extend `assertFeatureConformance` to require `RateLimited` brand when `rateLimit.length > 0` with a synthetic fixture test asserting `ConformanceError` on absent brand; add `rateLimit?: IRateLimit` to `BindContext` and default it to `new NoopRateLimit()` in app aggregators; unit tests for wrapper brand attachment and factory passthrough; all gates pass.
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: 05-auth-signin-rate-limit-backfill
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: auth.signIn rate-limit backfill
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: auth
|
||||
depends-on:
|
||||
[
|
||||
01-rate-limit-type-primitives,
|
||||
02-rate-limit-implementations,
|
||||
03-no-undeclared-rate-limit-eslint-rule,
|
||||
04-with-rate-limit-wrapper-and-conformance,
|
||||
]
|
||||
blocks: []
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T09:27:20.152Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Apply the rate-limit primitive to `auth.signIn` as the canonical reference example that every downstream consumer will copy — manifest declaration, dual `consume` calls in the use-case body with `TooManyRequestsError` throws, binder wiring, and extended tests — demonstrating end-to-end credential-stuffing defence that passes lint and conformance.
|
||||
|
||||
## Why
|
||||
|
||||
`auth.signIn` is the highest-risk write path in the template. Without rate-limit gates, credential-stuffing and account-enumeration windows stay open until a real consumer notices their auth logs. Backfilling this use case with the complete pattern (manifest → use-case body → binders → tests) also validates that all four preceding stories form a coherent system: if any type, impl, ESLint rule, or wrapper is mis-shaped, this story's conformance gate will surface it.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/auth/src/feature.manifest.ts` `signIn` entry declares `rateLimit: [{ name: "ip", window: "1m", budget: 5 }, { name: "account", window: "1h", budget: 10 }]`.
|
||||
- `signInUseCase` deps include `rateLimit: IRateLimit`; body calls `rateLimit.consume("ip", \`signIn:ip:${input.clientIp}\`)` and `rateLimit.consume("account", \`signIn:account:${input.email}\`)`, throwing `TooManyRequestsError`on`!allowed`.
|
||||
- `packages/auth/src/di/bind-production.ts` + `bind-dev-seed.ts` pass `ctx.rateLimit ?? new NoopRateLimit()` into signIn's `wireUseCase`.
|
||||
- Existing signIn unit tests extended: `RecordingRateLimit` asserts both `consume` calls captured; `InMemoryRateLimit` at budget 1 asserts second call throws `TooManyRequestsError`.
|
||||
- `no-undeclared-rate-limit` ESLint rule passes (no warnings) on both `"ip"` and `"account"` call sites.
|
||||
- `assertFeatureConformance` boot assertion passes (signIn is `RateLimited` branded).
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `feature.manifest.ts` — `rateLimit` declaration for `signIn`.
|
||||
- `sign-in.use-case.ts` — `rateLimit: IRateLimit` in deps, dual `consume` + `TooManyRequestsError` throw.
|
||||
- `TooManyRequestsError` class (if not already present in `auth/entities/errors/`) — add alongside other auth errors.
|
||||
- `bind-production.ts` + `bind-dev-seed.ts` — wire `ctx.rateLimit ?? new NoopRateLimit()` into signIn.
|
||||
- Unit tests: `RecordingRateLimit` dual-consume assertion; `InMemoryRateLimit` budget-1 rejection.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `auth.signUp` rate-limit — not declared in the PRD; consumer adds when needed.
|
||||
- Rate-limit for any other auth use case — beyond the canonical example scope.
|
||||
- Redis-backed wiring — consumer adds via ADR-022 library evaluation.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add `rateLimit: [{ name: "ip", window: "1m", budget: 5 }, { name: "account", window: "1h", budget: 10 }]` to the `signIn` entry in `packages/auth/src/feature.manifest.ts`; add `rateLimit: IRateLimit` to `signInUseCase` deps; add `TooManyRequestsError` to auth error types if absent; insert dual `rateLimit.consume` calls with `TooManyRequestsError` throws in the use-case body; update `bind-production.ts` + `bind-dev-seed.ts` to pass `ctx.rateLimit ?? new NoopRateLimit()` into signIn's `wireUseCase`; extend signIn unit tests with `RecordingRateLimit` dual-consume assertion and `InMemoryRateLimit` budget-1 rejection; all gates pass.
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
id: 06-security-headers-core-module
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: Security headers core module
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: []
|
||||
blocks: [07-security-header-adapters]
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T09:35:46.924Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add the framework-agnostic security headers module to `core-shared/security` — `SecurityHeadersConfig` types, `generateNonce()` cryptographic helper, and `buildSecurityHeaders()` pure builder that returns all six headers with mode-aware CSP and URL validation for allowlisted origins.
|
||||
|
||||
## Why
|
||||
|
||||
The per-framework adapters (Story 07) and app-wiring stories (08, 09) all depend on this pure builder. Extracting the builder into `core-shared` (must-have) ensures all three template apps and any future framework adapters share a single, tested implementation of the header set, without duplicating CSP string construction or nonce generation logic.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-shared/src/security/security-types.ts` exports `SecurityHeadersConfig` and `CspMode`.
|
||||
- `packages/core-shared/src/security/nonce.ts` exports `generateNonce()` returning a cryptographically random base64-encoded 16-byte string.
|
||||
- `packages/core-shared/src/security/build-security-headers.ts` exports `buildSecurityHeaders(opts: SecurityHeadersConfig): Record<string, string>` emitting all six headers (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Content-Security-Policy); prod CSP includes nonce in `script-src 'strict-dynamic' 'nonce-{NONCE}'`; dev CSP is permissive (`'unsafe-inline' 'unsafe-eval' ws: localhost:* 127.0.0.1:*`); `allowedConnectOrigins` entries validated via `URL` constructor, throwing `InvalidSecurityHeadersConfig` on malformed input.
|
||||
- Unit tests: expected header set per mode, nonce threading into CSP `script-src`, dev vs prod CSP shape, `allowedConnectOrigins` / `allowedImgOrigins` / `allowedFontOrigins` applied to correct CSP directives, URL validation error on malformed origin, `generateNonce` randomness (two calls differ).
|
||||
- Types exported from `core-shared` barrel.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `security-types.ts` — `SecurityHeadersConfig`, `CspMode` types.
|
||||
- `nonce.ts` — `generateNonce()` using `crypto.randomBytes(16).toString("base64")`.
|
||||
- `build-security-headers.ts` — pure builder, six headers, prod/dev CSP, `allowedConnectOrigins` / `allowedImgOrigins` / `allowedFontOrigins` applied to CSP directives, URL validation with `InvalidSecurityHeadersConfig` error.
|
||||
- Unit tests for all three files.
|
||||
- Barrel export from `core-shared`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Framework-specific adapter subpaths (`core-shared/security/next`, `core-shared/security/tanstack`) — Story 07.
|
||||
- App middleware wiring — Stories 08 and 09.
|
||||
- CSP report-uri collector endpoint — deferred, documented in guide.
|
||||
- Storybook CSP — explicitly out of Epic C scope.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Create `packages/core-shared/src/security/security-types.ts` with `SecurityHeadersConfig` + `CspMode` types; implement `packages/core-shared/src/security/nonce.ts` with `generateNonce()` using `crypto.randomBytes`; implement `packages/core-shared/src/security/build-security-headers.ts` emitting all six headers with prod/dev CSP mode, nonce threading into `script-src`, `allowedConnectOrigins` / `allowedImgOrigins` / `allowedFontOrigins` applied to CSP, URL validation throwing `InvalidSecurityHeadersConfig` on malformed origins; unit tests covering header set, CSP variants, nonce threading, allowlist CSP directives, URL validation error, nonce randomness; export from `core-shared` barrel; all gates pass.
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: 07-security-header-adapters
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: Per-framework security header adapters
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on: [06-security-headers-core-module]
|
||||
blocks: [08-app-wiring-web-next, 09-app-wiring-web-tanstack-and-cms]
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T09:58:23.659Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Ship the two framework-specific adapter subpaths — `core-shared/security/next` (Next.js middleware + `getNonce()` Server Component helper) and `core-shared/security/tanstack` (TanStack Start server middleware + request-context nonce extractor) — so the app-wiring stories can wire them end-to-end without touching the underlying header builder.
|
||||
|
||||
## Why
|
||||
|
||||
Adapters follow the `core-analytics/react` subpath pattern established in the codebase: framework-specific code lives in a subpath export so the core module remains importable without dragging in framework dependencies. Each adapter generates a per-request nonce, calls `buildSecurityHeaders`, sets all six headers on the response, and forwards the nonce via `x-nonce` for downstream Server Component access. Landing the adapters before app wiring keeps the integration commits thin.
|
||||
|
||||
## Done when
|
||||
|
||||
- `packages/core-shared/src/security/next/index.ts` exports a Next.js middleware function and `getNonce()` helper; adapter tests assert all six headers set on response, `x-nonce` present, `getNonce()` reads the value from `headers()`.
|
||||
- `packages/core-shared/src/security/tanstack/index.ts` exports a TanStack Start server middleware and a nonce extractor for server context; adapter tests assert equivalent header + nonce behaviour.
|
||||
- Both subpaths declared in `packages/core-shared/package.json` `exports` map.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass after each task.
|
||||
|
||||
## In scope
|
||||
|
||||
- `core-shared/security/next` — middleware calling `generateNonce()` + `buildSecurityHeaders({ mode: process.env.NODE_ENV === "production" ? "production" : "development", nonce })`, setting headers on the response, writing nonce to `x-nonce` response header; `getNonce()` reading `x-nonce` from `headers()` for use in Server Components.
|
||||
- `core-shared/security/tanstack` — equivalent using TanStack Start's server middleware API; nonce extractor for TanStack Server context.
|
||||
- `package.json` subpath exports for both adapters.
|
||||
- Adapter unit tests for each.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- App middleware wiring, Sentry nonce init, layout nonce threading — Stories 08 and 09.
|
||||
- CMS adapter — `apps/cms` uses the framework-agnostic `buildSecurityHeaders` directly (Story 09).
|
||||
- Storybook CSP — explicitly out of Epic C scope.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Implement `packages/core-shared/src/security/next/index.ts`: Next.js middleware calling `generateNonce()` + `buildSecurityHeaders`, setting all six headers and `x-nonce` on the response, plus `getNonce()` helper reading `x-nonce` from Next.js `headers()`; add subpath to `package.json` exports; adapter tests asserting all headers present, nonce in response headers, `getNonce()` reads it; all gates pass.
|
||||
- [x] Implement `packages/core-shared/src/security/tanstack/index.ts`: TanStack Start server middleware equivalent (generate nonce, set headers + `x-nonce`) plus request-context nonce extractor; add subpath to `package.json` exports; adapter tests asserting equivalent header + nonce behaviour; all gates pass.
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: 08-app-wiring-web-next
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: "App wiring: web-next"
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: web-next
|
||||
depends-on: [07-security-header-adapters]
|
||||
blocks: []
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T10:14:20.860Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Wire the security headers middleware end-to-end in `apps/web-next` — middleware chain, nonce-aware Sentry browser SDK init, and nonce threaded into the document head — producing a Next.js app that emits all six headers with per-request CSP nonces and no CSP violations in the browser console.
|
||||
|
||||
## Why
|
||||
|
||||
`apps/web-next` is the primary template app; getting it wired first validates the Next.js adapter in a real app context, including the Sentry nonce contract and the `<Script nonce={nonce}>` threading pattern that consumers will copy. The Sentry init integration is non-trivial enough (ADR-014 nonce contract, `replayIntegration` + `feedbackIntegration` both need the nonce) that it justifies its own story to get right before the parallel web-tanstack story begins.
|
||||
|
||||
## Done when
|
||||
|
||||
- `apps/web-next/middleware.ts` invokes the `core-shared/security/next` middleware and chains it with existing auth checks (security headers apply before auth redirects).
|
||||
- `apps/web-next/instrumentation-client.ts` reads nonce via `getNonce()` and passes it to `Sentry.init({ integrations: [replayIntegration({ nonce }), feedbackIntegration({ nonce })] })`.
|
||||
- `apps/web-next/app/layout.tsx` threads nonce from `getNonce()` into `<Script nonce={nonce}>` for any inline scripts in the document head.
|
||||
- Middleware test asserts: all six headers present in response, CSP shape matches prod template for `NODE_ENV=production`, CSP shape is permissive for `NODE_ENV=development`, `x-nonce` header present.
|
||||
- No CSP violations appear in browser console when running `pnpm dev` against `localhost:3000`.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `apps/web-next/middleware.ts` — security headers middleware chained with existing auth middleware.
|
||||
- `apps/web-next/instrumentation-client.ts` — nonce-aware `Sentry.init`.
|
||||
- `apps/web-next/app/layout.tsx` — nonce threaded into `<Script>` tags.
|
||||
- Middleware test: six headers + CSP shape per mode + `x-nonce` presence.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- TanStack Start and CMS wiring — Story 09.
|
||||
- CSP report-uri collector — deferred.
|
||||
- Storybook CSP — explicitly out of Epic C scope.
|
||||
- HSTS preload list submission — consumer/legal action.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Wire `core-shared/security/next` middleware in `apps/web-next/middleware.ts` chained before existing auth checks; update `apps/web-next/instrumentation-client.ts` to read nonce via `getNonce()` and pass to `Sentry.init` replay + feedback integrations; thread nonce from `getNonce()` into `<Script nonce={nonce}>` in `apps/web-next/app/layout.tsx`; add middleware test asserting all six headers, prod/dev CSP shape, and `x-nonce` present in response; all gates pass.
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
id: 09-app-wiring-web-tanstack-and-cms
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: "App wiring: web-tanstack and cms"
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: web-tanstack
|
||||
depends-on: [07-security-header-adapters]
|
||||
blocks: []
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T11:22:09.324Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Wire the security headers middleware in `apps/web-tanstack` (TanStack Start server middleware + nonce-aware Sentry init) and `apps/cms` (Payload Express middleware) so all three template apps emit the six security headers by default.
|
||||
|
||||
## Why
|
||||
|
||||
Completing the three-app wiring fulfils the PRD success criterion that a consumer picking any template app gets compliant default headers without writing middleware. The two tasks are independent and can land in either order; grouping them in one story reflects that they share the same depends-on (Story 07) and both close the "all apps wired" milestone together.
|
||||
|
||||
## Done when
|
||||
|
||||
- `apps/web-tanstack/app.config.ts` registers the `core-shared/security/tanstack` server middleware.
|
||||
- The web-tanstack client init file reads nonce from request context and passes it to `Sentry.init` replay + feedback integrations (mirroring the web-next pattern).
|
||||
- `apps/cms` Payload config wires the `core-shared/security` Express middleware; CMS responses emit all six headers (no nonce needed — server-side only app).
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass after each task.
|
||||
|
||||
## In scope
|
||||
|
||||
- `apps/web-tanstack/app.config.ts` — `core-shared/security/tanstack` server middleware registration.
|
||||
- web-tanstack client init file — nonce-aware Sentry init (nonce from request context via tanstack adapter's extractor).
|
||||
- `apps/cms` Payload config — Express middleware from `core-shared/security` (framework-agnostic builder; no nonce needed for CMS).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- web-next wiring — Story 08.
|
||||
- CSP report-uri collector — deferred.
|
||||
- Storybook CSP — explicitly out of Epic C scope.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Register `core-shared/security/tanstack` server middleware in `apps/web-tanstack/app.config.ts`; update the web-tanstack client init file with nonce-aware Sentry init reading nonce from request context via the tanstack adapter's nonce extractor; all gates pass.
|
||||
- [x] Wire `core-shared/security` Express middleware in `apps/cms` Payload config (no nonce needed — CMS is server-side only); all gates pass.
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: 10-sbom-ci-workflow
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: SBOM CI workflow and ADR-023 amendment
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: ci
|
||||
depends-on: []
|
||||
blocks: []
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T11:33:07.860Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Amend `.github/workflows/release-please.yml` to generate a CycloneDX SBOM and upload it as a GitHub release asset whenever release-please cuts a release, and capture the concrete workflow step as an amendment to ADR-023 §10.
|
||||
|
||||
## Why
|
||||
|
||||
Consumers pursuing SOC 2 / ISO 27001 / FedRAMP / EU CRA must answer "what's in version X" without inventory inspection. A CycloneDX SBOM attached to every GitHub release gives auditors a machine-readable, per-release artifact. `pnpm dlx` avoids adding `@cyclonedx/cyclonedx-npm` to the lockfile (CI-only tool per ADR-022). The Renovate-pinned SHA on `softprops/action-gh-release` follows the established ADR-023 pattern.
|
||||
|
||||
## Done when
|
||||
|
||||
- `.github/workflows/release-please.yml` has a conditional step that runs `pnpm dlx @cyclonedx/cyclonedx-npm --output-file sbom-<tag>.cdx.json --output-format json` when `steps.release.outputs.releases_created == 'true'`.
|
||||
- A `softprops/action-gh-release@<SHA>` step (Renovate-managed SHA per ADR-023) uploads the SBOM JSON file as a release asset with `tag_name: ${{ steps.release.outputs.tag_name }}`.
|
||||
- `docs/decisions/adr-023-ci-security-and-supply-chain.md` contains a new amendment subsection capturing the SBOM step's concrete shape and rationale.
|
||||
- Local validation: `pnpm dlx @cyclonedx/cyclonedx-npm --output-file sbom-test.cdx.json` succeeds and produces valid CycloneDX JSON.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass.
|
||||
|
||||
## In scope
|
||||
|
||||
- `.github/workflows/release-please.yml` — conditional SBOM generation + upload steps.
|
||||
- `softprops/action-gh-release@<SHA>` with Renovate-managed SHA (choose a recent stable release; Renovate will keep it current).
|
||||
- `docs/decisions/adr-023-ci-security-and-supply-chain.md` — amendment subsection §10 SBOM.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Per-PR SBOM generation — release-only per PRD.
|
||||
- SBOM signing / SLSA provenance attestation — bare CycloneDX only; attestation is a future PRD.
|
||||
- Per-package SBOMs — root SBOM covers all workspace packages; industry practice for monorepos.
|
||||
- `@cyclonedx/cyclonedx-npm` added to `package.json` — invoked via `pnpm dlx` only.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Add conditional SBOM generation step (`pnpm dlx @cyclonedx/cyclonedx-npm --output-file sbom-${{ steps.release.outputs.tag_name }}.cdx.json --output-format json`) and upload step (`softprops/action-gh-release@<SHA>` with `files:` pointing to the SBOM and `tag_name:` from release-please output) to `.github/workflows/release-please.yml`; add amendment subsection to `docs/decisions/adr-023-ci-security-and-supply-chain.md` documenting the concrete step shape and rationale; all gates pass.
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: 11-documentation
|
||||
epic: security-headers-rate-limit-sbom
|
||||
title: Documentation and conformance reference updates
|
||||
type: technical-story
|
||||
status: done
|
||||
feature: core-shared
|
||||
depends-on:
|
||||
[
|
||||
05-auth-signin-rate-limit-backfill,
|
||||
08-app-wiring-web-next,
|
||||
09-app-wiring-web-tanstack-and-cms,
|
||||
10-sbom-ci-workflow,
|
||||
]
|
||||
blocks: []
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T11:49:44.873Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Write the consumer-facing cookbooks (`security-headers.md`, `rate-limiting.md`) and update the cross-cutting reference docs (glossary, CLAUDE.md, conformance quickref) so downstream consumers, AI agents, and compliance officers can discover and apply security headers, rate-limiting, and SBOM from a single reading session.
|
||||
|
||||
## Why
|
||||
|
||||
Implementations are complete after Story 10, but the institutional knowledge of how to use them, customize them, and understand their conformance contracts lives only in code. The cookbooks close that gap by walking concrete wiring steps, key-naming conventions, and verification workflows. The CLAUDE.md and conformance-quickref updates keep the conformance rule count accurate (12 → 13) and surface the `rateLimit` manifest field in the authoritative reference every agent reads at session start.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/guides/security-headers.md` covers: per-framework middleware wiring (Next.js, TanStack, CMS), nonce threading for consumer-added inline scripts, CSP allowlist customization (`allowedConnectOrigins` etc.), Sentry nonce integration steps, securityheaders.com verification workflow.
|
||||
- `docs/guides/rate-limiting.md` covers: manifest `rateLimit` field declaration, canonical key-naming convention `<feature>:<scope>:<key>`, multi-budget patterns, `InMemoryRateLimit` for dev/test, guidance on wiring a production backend via `BindContext.rateLimit`.
|
||||
- `docs/glossary.md` has entries for: `IRateLimit`, `RateLimited` (brand), `withRateLimit`, `SecurityHeadersConfig`, `buildSecurityHeaders`, `SBOM` (CycloneDX context), `nonce` (CSP context).
|
||||
- `CLAUDE.md` conformance rule count updated 12 → 13; `rateLimit?: RateLimitBudget[]` documented in the manifest field table.
|
||||
- `docs/guides/conformance-quickref.md` updated to list `no-undeclared-rate-limit` as the 13th rule.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass after each task.
|
||||
|
||||
## In scope
|
||||
|
||||
- `docs/guides/security-headers.md` (new file).
|
||||
- `docs/guides/rate-limiting.md` (new file).
|
||||
- `docs/glossary.md` — new entries only; no existing entry edits.
|
||||
- `CLAUDE.md` — conformance rule count bump (12 → 13) + `rateLimit` manifest field row.
|
||||
- `docs/guides/conformance-quickref.md` — add `no-undeclared-rate-limit` entry.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- ADR-023 amendment — Story 10.
|
||||
- Any new code changes — docs only; all implementation is done.
|
||||
- Compliance fill-in docs (incident runbook, password policy) — Epic D.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Write `docs/guides/security-headers.md` (per-framework wiring, nonce threading for consumer inline scripts, CSP allowlist customization, Sentry nonce integration, securityheaders.com verification) and `docs/guides/rate-limiting.md` (manifest field, key-naming convention `<feature>:<scope>:<key>`, multi-budget patterns, dev/staging/prod backend wiring); all gates pass.
|
||||
- [x] Add entries for `IRateLimit`, `RateLimited` brand, `withRateLimit`, `SecurityHeadersConfig`, `buildSecurityHeaders`, `SBOM`, `nonce` (CSP context) to `docs/glossary.md`; update `CLAUDE.md` conformance rule count 12 → 13 and add `rateLimit?: RateLimitBudget[]` to the manifest field documentation; add `no-undeclared-rate-limit` as the 13th rule in `docs/guides/conformance-quickref.md`; all gates pass.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
id: security-headers-rate-limit-sbom
|
||||
prd: docs/work/prds/security-headers-rate-limit-sbom.prd.md
|
||||
title: Security headers + rate-limit primitive + SBOM in CI — Epic C of ADR-025
|
||||
type: epic
|
||||
status: done
|
||||
features:
|
||||
[core-shared, core-testing, core-eslint, auth, web-next, web-tanstack, cms]
|
||||
created: 2026-05-20T00:00:00Z
|
||||
updated: 2026-05-20T11:49:44.873Z
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Ship three hardening primitives — framework-agnostic security header middleware, a manifest-declared rate-limit conformance channel, and per-release SBOM evidence — so downstream consumers get compliant default headers, lint-enforced rate-limit gates, and CycloneDX audit artifacts without inventing any of them.
|
||||
|
||||
## Why
|
||||
|
||||
Security scanners flag the absence of HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and CSP on every template app response. Auth endpoints (signIn, signUp) ship without rate-limit declarations, leaving credential-stuffing windows open until a consumer notices their auth logs. Consumers pursuing SOC 2 / ISO 27001 / FedRAMP must invent SBOM tooling and bolt it into their release flow. ADR-025 settled the strategy; this epic is the implementation.
|
||||
|
||||
## Stories
|
||||
|
||||
- [x] [01 — Rate-limit type primitives and manifest field](01-rate-limit-type-primitives/_story.md)
|
||||
- [x] [02 — Rate-limit implementations: Noop, InMemory, Recording](02-rate-limit-implementations/_story.md)
|
||||
- [x] [03 — `no-undeclared-rate-limit` ESLint rule](03-no-undeclared-rate-limit-eslint-rule/_story.md)
|
||||
- [x] [04 — `withRateLimit` wrapper and conformance extensions](04-with-rate-limit-wrapper-and-conformance/_story.md)
|
||||
- [x] [05 — auth.signIn rate-limit backfill](05-auth-signin-rate-limit-backfill/_story.md)
|
||||
- [x] [06 — Security headers core module](06-security-headers-core-module/_story.md)
|
||||
- [x] [07 — Per-framework security header adapters](07-security-header-adapters/_story.md)
|
||||
- [x] [08 — App wiring: web-next](08-app-wiring-web-next/_story.md)
|
||||
- [x] [09 — App wiring: web-tanstack and cms](09-app-wiring-web-tanstack-and-cms/_story.md)
|
||||
- [x] [10 — SBOM CI workflow and ADR-023 amendment](10-sbom-ci-workflow/_story.md)
|
||||
- [x] [11 — Documentation and conformance reference updates](11-documentation/_story.md)
|
||||
Reference in New Issue
Block a user