The retention purge job gated its whole body on activeRetention while
every collection declares only postDeletion, and no app ever called
registerRetentionPurgeJobs — retention was dead end to end (audit
findings A2 + A3). The DSR soft delete now stamps a deletedAt tombstone
on postDeletion collections (kept distinct from processingRestrictedAt
so an Art. 18 restriction never feeds the purge), the job grace-purges
tombstoned rows past postDeletion.duration with the declared action,
core-cms injects the tombstone field + Payload task definitions, and
bindAllProduction enqueues the first purge cycle at boot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tRPC createContext was () => ({}) — the mounted dsr/consent routers
401'd every call and the dsr singleton stub threw (audit finding A11).
createTrpcContext now accepts an app resolveUser hook; web-next resolves
the session cookie through the auth feature's validateSession (denylist
included) plus a role snapshot, and threads bindProductionDsr/Consent
(or dev-seed) bindings into every request. The dsr router resolves its
binding from ctx.dsrBinding per request instead of a throwing proxy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The router previously created its own initTRPC without superjson while
the app router uses the shared transformer-enabled instance — a wire
transformer mismatch that corrupts inputs (audit finding A10). Procedures
now build on @repo/core-shared/trpc/init's t; a real client+fetch-adapter
round-trip test pins Date revival through appRouter-style mounting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@trpc/client resolved to 11.16.0 in some packages while @trpc/server
resolved to 11.17.0 in others, tripping the exact-version peer check.
Update every @trpc/* range in-range so the whole workspace resolves to
one version (11.18.0); install is peer-warning-free again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
core-trpc imported AppRouter (type-only) from core-api, which depends
on the features, which depend on core-trpc — a cycle that killed every
turbo graph walk (lint/build/CI) and an illegal core -> core-composition
boundary edge. The tRPC context is now router-agnostic (AnyTRPCRouter);
useTRPC takes the router type as a generic and each feature exports a
type-only app-slice (e.g. BlogAppSlice) mirroring its mount key, so UI
hooks keep full procedure typing without touching the composition layer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
241 tests across 16 script test files were wired to NO runner (the
generators vitest config never included them and several used node:test
imports that vitest silently skips). New root vitest.scripts.config.mjs
+ pnpm test:scripts + a CI validate step run them all; node:test imports
converted to vitest keeping node:assert. Split out of 8c88a9a where a
concurrent agent's staged files were swept into the marketing-pages
commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stories glob covered only packages/core-ui, so any feature story a
component-must-have-story fix produces would be invisible in Storybook
and skipped by pnpm test:stories. Glob every package's src tree and
declare the workspace dep on @repo/core-ui (the only storied package
today) so turbo's build cache invalidates when its stories change.
Verified: storybook build succeeds, 18 stories in index.json. (S7)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The package's vitest base configs were outside every gate — no tsconfig
and no typecheck script meant tsc never saw them. Add a tsconfig with
allowImportingTsExtensions (vitest.base.jsdom imports ./vitest.base.node.ts
by extension) and an explicit include of the base configs + vitest
config, plus the typescript/@types/node devDeps to run it. (S6)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm audit --prod --audit-level=critical (the new CI gate) failed on
vitest <3.2.6 (critical, arbitrary file read/execute via the UI
server). Bump vitest + @vitest/coverage-v8 in-range across the
workspace and add @vitest/coverage-v8 to every package that runs
vitest but lacked it (core-audit, core-cms, core-eslint, core-testing,
core-trpc, apps/cms, web-next, web-tanstack, turbo/generators) so
'pnpm test -- --coverage' works in every package. No compound test
scripts exist, so the vitest-last pass-through concern does not apply.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
next.js and react-internal.js are plain re-exports of base, so no
rules-of-hooks checking was active anywhere despite three React apps
and core-ui. Wire eslint-plugin-react-hooks in base.js scoped to
**/*.tsx (non-React packages untouched). Fixes the one violation it
surfaced: web-tanstack's root route called Route.useLoaderData inside
an anonymous component callback — extracted to a named RootComponent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The root devDependency carried a "*" wildcard, and ADR-019 documented a
nonexistent ^2.73.0 pin — the package's installed version is 0.5.10.
Pin to ^0.5.10 and correct ADR-019 to match.
- Add @tanstack/start + vinxi to deps so defineConfig is available
- Uncomment defineConfig registration in app.config.ts — middleware
is now actually wired into the Nitro server hook, not just defined
- Update __root.tsx loader to call getNonce(getEvent().node.req)
from @repo/core-shared/security/tanstack so the per-request nonce
is read server-side and injected via <meta name="csp-nonce">
- Update __root.test.tsx: mock provides useLoaderData and asserts
the nonce meta tag is rendered with the correct content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements security/next subpath with withSecurityHeaders() middleware
and getNonce() Server Component helper. Middleware generates a per-request
nonce, calls buildSecurityHeaders, sets all six headers + x-nonce on the
response, and forwards the nonce via request headers for Server Component
access. Adds next as optional peer + dev dependency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous attempt was rejected because the axe-core a11y requirement
had no test infrastructure — ARIA roles were correct but unverified by
a scanner. This adds jest-axe (approved via library-decision trace) and
asserts toHaveNoViolations() for both modal and banner variants.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Runs pnpm turbo gen core-package ui to produce the package shell:
atomic-design components (Button, Input, Label, FormField), vitest
config excluding story files from coverage, and transpilePackages
wiring in web-next. Adds @vitest/coverage-v8 devDep and
label.stories.tsx to satisfy lint/coverage gates.
Also fixes scripts/library-decisions/check.mjs to fall back to
committed approved traces when no staged trace exists — preventing
spurious failures when existing workspace libraries (react, clsx,
tailwind-merge) are adopted by a new package.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add @repo/core-dsr and @repo/core-consent as dependencies and wire
dsrRouter + consentRouter into appRouter via the existing router
composition pattern. Integration tests cover all eight procedures
(dsr.export, dsr.delete, dsr.rectify, dsr.restrict, consent.grant,
consent.withdraw, consent.isGranted, consent.getCategories) with auth
and response-shape assertions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add four protocol-agnostic handlers (export, delete, rectify, restrict)
returning normalized { status, body, headers } responses, and a tRPC
dsrRouter via createDsrRouter(binding) following the factory pattern.
Auth checks: requireAuthenticated middleware gates all four procedures;
cascade-hard delete additionally requires admin role. Integration tests
assert happy-path response shapes, UNAUTHORIZED/FORBIDDEN error codes,
and error passthrough from the DSR service layer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Protocol-agnostic handlers (grant, withdraw, isGranted, getCategories)
in core-consent/handlers/ call IConsent methods and return typed results.
consentRouter uses a consent-specific tRPC context (userId + consentFactory)
so each procedure can resolve the per-user IConsent instance at call time.
Auth middleware guards all four procedures and maps UnauthenticatedError →
UNAUTHORIZED via defineErrorMiddleware from core-shared (no local duplicate).
76 tests passing; new handler and router code at 100% branch coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the Payload-backed IConsent that reads/writes users.consentState
and emits CONSENT_GRANT/CONSENT_WITHDRAW audit entries via injected auditLog.
Adds RecordingConsent test double in core-testing for unit-test injection.
Adds bindProductionConsent/bindDevSeedConsent DI binders and InMemoryConsent
for dev/seed contexts. Contract tests cover grant/withdraw/isGranted round-trip,
audit entry shape, metadata persistence (bannerVersion/policyVersion/method),
and getCategories reflection of state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add packages/core-consent with ConsentCategory, ConsentState,
UserConsentState types and IConsent interface
- Add withConsent wrapper attaching __consentChecked brand at bind time;
unit tests assert brand attachment and factory passthrough
- Add ConsentChecked<F> type to core-shared/conformance/brands.ts and
isConsentChecked helper to brand-runtime.ts
- Extend FeatureManifest with requiresConsent?: readonly string[] field
- Extend assertFeatureConformance to require __consentChecked brand when
requiresConsent.length > 0; synthetic fixture tests cover pass/fail cases
- Propagate __consentChecked in withSpan PROPAGATED_BRANDS so the outermost
binding carries the brand
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the `conformance/pii-declaration-must-be-complete` ESLint rule at
warn severity. The rule detects `custom: { pii: { ... } }` blocks in
Payload config files and warns when any of the four required sub-fields
(`category`, `purpose`, `exportable`, `restrictable`) is missing.
Incomplete PII declarations can produce incorrect audit reports —
sub-second editor feedback catches the gap before it reaches
compliance/data-map.yml.
- Rule + 7 RuleTester fixtures (complete passes, each missing field
warns, non-pii custom block is no-op, malformed custom.pii is no-op)
- Registered in plugin.js + base.js at "warn"
- Conformance rule count bumped 7 → 8 in CLAUDE.md +
conformance-quickref.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a ./react subpath export to @repo/core-analytics containing
<AnalyticsProvider value={IAnalytics}> and useAnalytics(): IAnalytics.
useAnalytics() throws AnalyticsContextError when called outside a provider.
React Testing Library test verifies track() flows through context using
RecordingAnalytics. Switches vitest config to pick up .tsx test files
via environmentMatchGlobs and extends tsconfig to react-library.json
for JSX support.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `withAnalytics(analytics, factory)` to packages/core-analytics —
mirrors the `withAudit` pattern: thin forwarding closure that attaches
the `__analyzed` brand via `attachBrand` from `@repo/core-shared/conformance`
without mutating the original factory. Exports `Analyzed<F>` type and
`withAnalytics` from the `@repo/core-analytics` root barrel.
Adds `with-analytics.test.ts` asserting brand is present after wrapping,
absent on the original fn, output passes through unchanged, and errors
propagate. Adds `@repo/core-shared` as a production dependency.
Also fixes `scripts/library-decisions/check.mjs` to exempt workspace-protocol
entries (`workspace:*`) from the library trace requirement — internal monorepo
packages are not third-party libraries and were incorrectly gated. Adds a
regression test in `check.test.mjs` covering the exemption.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add the `pnpm turbo gen core-package analytics` generator template and
run it to scaffold the @repo/core-analytics workspace package. The
package lands in placeholder state (empty barrel export) ready for the
IAnalytics + NoopAnalytics implementation in the next commit.
Includes:
- turbo/generators/templates/core-package/analytics/ templates
- turbo/generators/config.ts analytics generator registration
- packages/core-analytics/ placeholder scaffold
- apps/web-next/next.config.mjs transpilePackages entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates the shared schema module for library evaluation traces
(ADR-022 §4): Zod-validated frontmatter with all 8 filter fields and
enum constraints, plus parseTrace/validateTrace exports and a custom
YAML frontmatter parser for the nested trace format.
Also adds docs/library-decisions/_template.md with all 11 required
headings (8 Filter + 3 Prompt) in machine-checkable ADR-022 order.
Adds zod as a root devDependency so the script is runnable directly
from the workspace root without a package context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the per-layer threshold gaps surfaced by the 2026-05-13 PRD
audit. After this commit all five features pass their declared
100%/100%/95%/100% bands on entities + use-cases + controllers.
media (was: missing @vitest/coverage-v8 + missing vitest config block +
one controller at 86.66% lines / 75% branches)
- Added @vitest/coverage-v8 dev dep
- Applied the standard helper-driven vitest config
- Declared the coverage section in feature.manifest.ts
- Added 2 tests to list-media.controller.test.ts covering the
InputParseError branch (unknown fields + invalid limit)
- Now: 16 files / 80 tests / 97.12% / controllers 100%
marketing-pages (was: get-site-settings.controller at 93.54% lines /
90.9% branches)
- Added 1 test to get-site-settings.controller.test.ts covering the
InputParseError branch on unknown fields
- Now: 22 files / 68 tests / 95.66% / controllers 100%
navigation (was: entities/errors/common.ts at 50% function hits +
get-header.controller at 86.66% lines / 80% branches)
- Root cause: InputParseError class never instantiated in any test
- Added 2 tests to get-header.controller.test.ts covering the
InputParseError branch + verifying the Zod cause is preserved.
One test exercises both gap files at once (controller throws,
InputParseError class is constructed).
- Wired navigation/vitest.config.ts through the shared helper
- Declared the coverage section in feature.manifest.ts
- Now: 11 files / 45 tests / 98.04% / entities + controllers 100%
All 5 features now drive thresholds from the manifest via the helper.
The duplication problem the keystone eliminates is gone.
Repo-wide via `pnpm coverage:aggregate`:
- statements 95.87% (lh 2994 / lf 3123)
- branches 88.91% (brh 433 / brf 487)
- functions 100% (fnh 142 / fnf 142)
- lines 95.87%
`pnpm coverage:diff -- --base HEAD~1` reports status: pass.
coverage/summary.json refreshed in the same commit so the trend
captures the post-unification state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands L3 of the agent-first coverage architecture (ADR-020) — the
mutation-testing layer. Stryker on entities + use-cases (the pure
business-logic surface) catches the third dimension of test quality:
tests that exist + execute the code but assert nothing.
Deps (root devDependencies):
- @stryker-mutator/core ^8.7.0
- @stryker-mutator/vitest-runner ^8.7.0
Shared base: packages/core-testing/stryker.base.json
- testRunner: vitest (uses each feature's vitest.config.ts)
- mutate: src/entities/** + src/application/use-cases/** (excludes
tests, factories, contracts)
- thresholds: high 90 / low 80 / break 80
- reporters: progress + html + json (reports/mutation/{index.html,
mutation.json})
- incremental mode enabled, concurrency 4, timeout 10s
- exposed via @repo/core-testing/stryker.base.json subpath export
Per-feature config: packages/auth/stryker.config.json
- 4-line file that extends the shared base
- Proof-of-concept; other features get a config when L0 unification
closes their existing test gaps
Driver: scripts/coverage/mutate.mjs (zero-dep Node ESM)
- discoverStrykerConfigs: walks packages/* and apps/* for
stryker.config.json
- Supports --filter <name>, --since <ref> (incremental), --json
- Runs Stryker per-feature via node_modules/.bin/stryker run
- Surfaces per-package pass/fail summary; exits 1 on any failure
- Tests: scripts/coverage/mutate.test.mjs (3 tests, all green)
CI: .github/workflows/mutation-nightly.yml
- Cron at 02:30 UTC + workflow_dispatch with filter input
- Uploads reports/mutation/** as artifact (30-day retention)
- On failure, opens a tracking issue labelled mutation-testing
- permissions: contents: read, issues: write
- 60-min timeout (Stryker is slow by design)
Generator: turbo gen feature now scaffolds stryker.config.json from
turbo/generators/templates/feature/stryker.config.json.hbs — new
features ship mutation-ready out of the box.
Guide: docs/guides/coverage.md L3 section fleshed out with run
syntax, config shape, base config inventory, CI behavior, and a
"what you're looking for" primer on mutation scores.
Lockfile churn: pnpm regenerated the lockfile for the new deps;
~5K-line net reduction is collateral (pnpm version drift) but
mechanical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pnpm install on the merged tree pulled in @opentelemetry/api,
api-logs, context-async-hooks, and sdk-trace-base under
packages/core-audit's devDependencies. The merge commit didn't fully
include the resolved lockfile entries; this commit aligns them so CI
installs cleanly.
Adds @opentelemetry/api-logs ^0.55.0 and @opentelemetry/sdk-logs ^0.55.0
to core-shared dependencies in preparation for OtelLogger (Phase 3).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements OtelTracer as ITracer replacement for SentryTracer. Uses
@opentelemetry/api's trace.getTracer + startActiveSpan, maps span.op to
a span attribute, catches exceptions via recordException + setStatus(ERROR),
and filters null attribute values. Five Vitest tests covering name/attributes,
nesting, exception recording, status mapping, and null filtering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The turbo/generators package shipped without `lint` or `typecheck` scripts,
so `pnpm lint` / `pnpm typecheck` at the root silently skipped it. This
masked 2 ESLint errors (unused imports) and 11 TypeScript errors (relative
imports missing the `.js` extension required by `moduleResolution: NodeNext`,
plus JSON imports missing the `with { type: "json" }` attribute).
- Add `lint` and `typecheck` scripts to turbo/generators/package.json so the
turbo pipeline picks them up (lint: 14/14, was 13/13).
- Add `.js` extensions to 7 relative imports across config.test.ts,
lib/core-package-utils.test.ts, lib/snapshot.test.ts, and the 4 e2e tests.
- Add `with { type: "json" }` attributes to 4 snapshot JSON imports in the
e2e tests.
- Remove unused `existsSync` and `splicePluginImportsAt` imports from
lib/core-package-utils.test.ts.
- Declare `@repo/core-typescript` + `typescript` devDependencies so the
generators package can run `tsc --noEmit` for typecheck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removal across all three consumers:
- apps/storybook: strips @repo/core-ui dep, clears stories glob (now []),
removes globals.css import from preview.ts
- apps/web-next: strips @repo/core-ui dep + transpilePackages entry
- apps/web-tanstack: strips @repo/core-ui dep
- packages/core-ui: deleted entirely (28 files)
No app pages needed surgery — neither web-next nor web-tanstack source
files imported @repo/core-ui directly. The storybook app referenced it
only via the stories glob and the globals.css preview import.
Gates: 40/40 tasks successful, all 4 e2e byte-identical reconstructions
pass (realtime, events, trpc, ui).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds turbo/generators to pnpm-workspace.yaml and declares its actual
dependencies so pnpm turbo boundaries reports zero issues.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Registers pnpm turbo gen core-package with an empty choices list and a
dispatch table that throws for unknown names. Adds vitest config + test
script to turbo/generators and adds it to the pnpm workspace so tests
run via pnpm --filter @repo/turbo-generators test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>