Commit Graph

45 Commits

Author SHA1 Message Date
a540f3afb1 feat(web-tanstack): wire security headers middleware and nonce threading
Register core-shared/security/tanstack server middleware in app.config.ts
as a Nitro/H3 hook that emits the six security headers and forwards the
per-request nonce. Update instrumentation-client to read the nonce from
<meta name="csp-nonce"> and pass it to initSentryClientReact.

Add nonce support to initSentryClientReact (feedbackIntegration receives
styleNonce/scriptNonce), mirroring the initSentryClient pattern already
in place for web-next.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:33:09 +00:00
b681e906ea feat(web-next): wire security headers middleware and nonce threading
- Add apps/web-next/middleware.ts calling withSecurityHeaders() from
  core-shared/security/next; exports matcher config excluding static assets
- Update layout.tsx to call getNonce() and render <meta name="csp-nonce">
  so client-side JS can read the per-request nonce
- Update instrumentation-client.ts to read nonce from csp-nonce meta tag
  and pass it to initSentryClient for feedbackIntegration CSP compliance
- Add nonce option to initSentryClient (InitClientOpts.nonce) and thread
  styleNonce + scriptNonce into feedbackIntegration when provided
- Add middleware test asserting all six headers, prod/dev CSP shape, and
  x-nonce presence; add feedbackIntegration nonce tests to core-shared

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:10:12 +00:00
a48af7e91c feat(core-shared): add TanStack Start security header adapter
Exports withSecurityHeaders() and getNonce() from the
./security/tanstack subpath. withSecurityHeaders() returns all six
security headers plus x-nonce for use inside a TanStack/Nitro H3
server middleware; getNonce() reads x-nonce from the node request
headers forwarded by that middleware.

Mirrors the ./security/next adapter pattern while staying free of
any @tanstack/start dependency — the adapter works with plain H3
IncomingMessage types that TanStack Start exposes at wiring time
(Story 09).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 09:56:03 +00:00
a736ed621d feat(core-shared): add Next.js security header middleware adapter
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>
2026-05-20 09:46:07 +00:00
96274ba856 feat(core-shared): add security headers module with CSP builder and nonce util
Adds framework-agnostic security headers module to core-shared/security:
- SecurityHeadersConfig + CspMode types
- generateNonce() using crypto.randomBytes(16)
- buildSecurityHeaders() emitting all six headers (HSTS, X-Frame-Options,
  X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP) with
  prod (strict-dynamic + nonce threading) and dev (unsafe-inline/eval +
  ws/localhost) CSP modes; URL validation throwing InvalidSecurityHeadersConfig
  on malformed allowedConnect/Img/FontOrigins
- Full unit test suite (24 tests, 100% coverage on runtime files)
- Exported from core-shared barrel and ./security subpath

Blocks story 07 (framework adapters) and stories 08-09 (app wiring).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 09:33:13 +00:00
b61bb0c11e feat(auth): add signIn rate-limit backfill with dual ip/account budgets
Wires the rate-limit primitive end-to-end through auth.signIn as the
canonical credential-stuffing defence example:

- manifest: rateLimit [ip 5/1m, account 10/1h] on signIn use case
- use case: rateLimit: IRateLimit dep; dual consume + TooManyRequestsError
- binders: ctx.rateLimit ?? new NoopRateLimit() in bind-production + bind-dev-seed
- tRPC: TooManyRequestsError → TOO_MANY_REQUESTS error code in authProcedure
- tests: RecordingRateLimit dual-consume assertion; InMemoryRateLimit
  budget-1 ip + account rejection; coverage 100% on use-cases layer
- ESLint: _manifest-ast.js extractRateLimitNames handles RateLimitBudget
  objects ({name,window,budget}) in addition to plain string literals,
  no-undeclared-rate-limit passes on both "ip" and "account" call sites

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 09:22:41 +00:00
cb61f51ee1 feat(core-shared): add withRateLimit wrapper and conformance enforcement
- Add withRateLimit(rateLimit, fn) in rate-limit/with-rate-limit.ts,
  attaching the RateLimited brand at DI bind time
- Extend wireUseCase to accept optional rateLimit?: IRateLimit and
  compose withRateLimit innermost (before analytics/audit); propagate
  __rateLimited through analytics + audit inline wrappers
- Extend withSpan and withCapture PROPAGATED_BRANDS to include
  __rateLimited so the outermost binding carries the brand
- Extend assertFeatureConformance to require __rateLimited brand when
  manifest.useCases[name].rateLimit.length > 0; refactored into
  helper functions to stay within complexity thresholds
- Add rateLimit?: IRateLimit to BindContext; default to NoopRateLimit
  in web-next bindAllProduction and bindAllDevSeed aggregators
- Unit tests for withRateLimit brand attachment, factory passthrough,
  and composition; synthetic fixture tests for conformance errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 09:00:31 +00:00
18cef2f889 feat(core-eslint): add no-undeclared-rate-limit conformance rule
Adds the `no-undeclared-rate-limit` ESLint rule (warn severity) that
enforces rate-limit drift at lint time:

- Warns when rateLimit.consume("X", _) is called inside a use-case but
  "X" is absent from manifest.useCases[name].rateLimit
- Warns when a declared rateLimit budget has no matching consume call
  in the use-case body (unusedDeclaration)
- Is a no-op outside use-case files

Extends _manifest-ast.js to extract the rateLimit[] field from both
parseManifestUseCases and parseManifestFully. Updates _manifest-ast
tests to include the new field in expected shapes. Registers the rule
at warn severity in plugin.js and base.js. Adds RuleTester fixtures
for all four cases (declared+matching, undeclared, unused, non-use-case).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 08:43:30 +00:00
5ddc8e6484 feat(core-shared): add NoopRateLimit and InMemoryRateLimit implementations
NoopRateLimit always allows with Infinity remaining — zero-overhead default
for apps without a wired rate-limit impl. InMemoryRateLimit uses a Map-backed
fixed-window with check-at-read expiry and an injected clock for testability.
Both exported from the core-shared barrel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 08:30:35 +00:00
c62de6fce9 feat(core-shared): add rate-limit type primitives and manifest field
Adds IRateLimit, RateLimitBudget, RateLimitDecision to a new
rate-limit sub-module; adds RateLimited<F> brand and isRateLimited
predicate following the Captured/ConsentChecked pattern; extends
UseCaseManifest with rateLimit?: readonly RateLimitBudget[] so
features can declare rate-limit gates in their manifests.

Exports new types from @repo/core-shared/rate-limit and
@repo/core-shared/conformance. Blocks stories 02, 03, and 04.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 08:21:42 +00:00
6b66064386 feat(auth): migrate anonymous consent on signUp when cc_consent cookie present
Adds ConsentFactoryProtocol / ConsentGrantMeta / ConsentProtocol to
core-shared/di/bind-protocols so feature binders can wire per-user
consent without a hard dep on the optional @repo/core-consent package.
BindContext gains an optional consentFactory? field following the same
pattern as bus?, auditLog?, etc.

signUpUseCase gains a 4th optional dep (consentFactory). When present
and the input includes a cookieHeader containing cc_consent=<categories>,
the use case calls consent.grant for each category with
method:"signup-migration" and returns a clearCookie payload (Max-Age:0)
so the anonymous cookie is cleared on the HTTP response.

Tests use RecordingConsent from @repo/core-testing to assert migration
call shape and cookie-clear; no-cookie and no-factory branches are also
covered. All coverage bands hold at 100% for use-cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:52:08 +00:00
051bfbf062 test(core-ui): add axe-core a11y assertions to CookieConsentBanner
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>
2026-05-19 21:36:26 +00:00
cbe7412a58 fix(scripts): exempt Storybook stories from coverage:diff gate
Story files are excluded from vitest by design (they run in Storybook
runner, not vitest). Add *.stories.{ts,tsx} to ALLOWED_GLOBS so the
L1 diff gate doesn't flag them as "new untested file".

Also add error-handling test for useOptionalConsent rethrow path
(cookie-consent-banner lines 52-53) achieving 100% statement coverage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:24:02 +00:00
1b6f2d1e36 feat(core-ui): add CookieConsentBanner headless component
Implements the EU-compliant cookie consent banner with:
- modal + banner variants, CNIL equal-prominence Reject/Accept buttons
- granular category toggles (essential non-toggleable)
- __consent_state cookie management (SameSite=Lax, Secure, 1-yr, _v:1)
- render-prop overrides: renderHeader, renderCategoryRow, renderActions
- useConsent() integration when ConsentProvider is present
- CookieConsentBannerLoader SSR-safe wrapper
- RTL behavioral tests: Reject All, Save Selected, ESC=Reject, focus-trap
- Storybook stories for modal, banner, render-prop, and a11y tab demo
- jsdom configured with HTTPS origin for Secure cookie testing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:21:05 +00:00
759fd4cbb1 feat(core-api): compose dsrRouter + consentRouter into appRouter
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>
2026-05-19 20:49:20 +00:00
6fd0a7ad4b test(core-dsr): cover dsrRouter singleton guard proxy path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:36:42 +00:00
cbea635fac fix(core-dsr): flip processingRestrictedAt in soft-delete for self role
PayloadDataDelete.deleteSubjectData('soft') was NULLing PII fields and
emitting RESTRICT audit entries, but never setting processingRestrictedAt
on self-kind rows — violating GDPR Art. 17 restriction semantics.

softRedactOwnerRows now accepts optional extraData; processOwnerRows
passes { processingRestrictedAt: new Date().toISOString() } when
kind === 'self' and mode === 'soft'. Owner-kind rows are intentionally
excluded (restriction flag belongs on the subject record, not owned rows).

Added two dedicated tests: one asserting the field is present for self,
one asserting it is absent for owner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:12:41 +00:00
6606b59d1e feat(core-dsr): Payload impls, recording doubles, DI binders, contract tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:05:06 +00:00
e378c950a9 feat(core-dsr): scaffold package + GDPR DSR interfaces and types
- Add pnpm turbo gen core-package dsr generator template and register
  dsr in CORE_PACKAGE_GENERATORS / choices list
- Run generator to produce packages/core-dsr/ shell
- Define IDataExport (Art. 15/20), IDataDelete (Art. 17),
  IDataRectify (Art. 16), IProcessingRestriction (Art. 18) interfaces
- Add UserDataBundle and DeletionCertificate types in dsr-types.ts
- Ship core-dsr/contexts/user-data.jsonld schema.org JSON-LD @context
- Wire @repo/core-dsr into transpilePackages (web-next)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 19:32:03 +00:00
9b6deac954 feat(core-consent): add ConsentProvider + useConsent() React subpath
Adds the ./react subpath to @repo/core-consent following the same
pattern as @repo/core-analytics/react:

- ConsentProvider wraps IConsent in React context
- useConsent() returns the injected IConsent or throws ConsentContextError
- RTL tests cover context propagation, grant/withdraw delegation,
  isGranted state reflection, getCategories, and missing-provider error
- package.json: ./react export + React optional peerDep + RTL devDeps
- tsconfig: extend react-library.json, include .tsx
- vitest.config: jsdom environment for .test.tsx + jsdom setup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 19:19:47 +00:00
e53f35a0c5 feat(core-consent): add handlers and consentRouter tRPC router
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>
2026-05-19 13:22:33 +00:00
b24f0666eb feat(core-consent): add extractAnonymousConsent and migrateAnonymousConsent helpers
Migration helpers let the auth signUp flow transfer anonymous cookie consent
to an authenticated user's record. extractAnonymousConsent parses the raw
Cookie header; migrateAnonymousConsent calls IConsent.grant with
method: "signup-migration" for each granted category, making the migration
traceable in the audit log. No-op when the consent cookie is absent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 12:59:53 +00:00
500a163e90 test(core-consent): add missing test coverage and fix diff allowlist
Add symbols.test.ts to cover CONSENT_SYMBOLS constant.
Add recording-consent.test.ts to core-testing (follows the pattern
of all other recording doubles which each have a sibling test file).
Refactor payload-consent.test.ts to extract a makeConsent() helper,
reducing clone groups from 7 to 2.
Add new test to cover grantedAt=null branch in deserializeEntry (withdraw
before any grant → persisted as null → loaded as undefined).
Extend coverage/diff.mjs allowlist for packages/core-testing/ since that
tooling package does not install @vitest/coverage-v8 and therefore produces
no lcov data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 12:48:50 +00:00
7dd46b68b2 feat(core-consent): add PayloadConsent, RecordingConsent and DI binders
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>
2026-05-19 12:39:31 +00:00
b992fee088 feat(core-consent): extend generator with consent template + fix withCapture brand propagation
- Add consent to CORE_PACKAGE_GENERATORS in turbo/generators/config.ts so
  pnpm turbo gen core-package consent is a valid command (not hand-rollable)
- Create turbo/generators/templates/core-package/consent/ mirroring the
  analytics template shape (AGENTS.md, package.json, tsconfig, turbo, vitest,
  eslint, src/index.ts scaffolds)
- Regenerate packages/core-consent/ from the new template (replaces the
  previous hand-rolled attempt that violated the generator-first rule)
- Add __consentChecked to withCapture PROPAGATED_BRANDS so the brand bubbles
  through the full withSpan→withCapture wrapper chain to the outermost binding
  that assertFeatureConformance reads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 11:01:17 +00:00
9cb2fa321c feat(core-consent): scaffold package with types, IConsent, withConsent brand wrapper
- 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>
2026-05-19 10:40:40 +00:00
dd339b11b1 feat(core-shared): extend audit action enum with consent and restriction types
Adds CONSENT_GRANT, CONSENT_WITHDRAW, RESTRICT, UNRESTRICT to the
AuditAction closed enum per GDPR Art. 7 and Art. 18 requirements.

core-consent and core-dsr optional cores (Epic B Stories 03/06) emit
these action types via core-audit's IAuditLog channel; the values must
exist in core-shared's enum before either optional core can be built.
No change to IAuditLog's interface surface — new values flow through
AuditEntry.action automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 10:16:30 +00:00
f8bb2f4094 feat(core-shared): add subject-linkage types and extend PII defaults
Introduces SubjectLinkKind, SubjectLink, and CollectionSubject types to
packages/core-shared/src/payload/subject-linkage-types.ts, establishes the
ambient CollectionCustom.subject declaration (parallel to custom.pii / custom.retention
from Epic A), and extends PAYLOAD_AUTH_PII_DEFAULTS with processingRestrictedAt and
consentState as DSR-managed excluded fields. Applies the first canonical usage of
custom.subject = { kind: "self", field: "id" } on the auth users collection.
2026-05-19 10:11:09 +00:00
879b0215c3 docs(compliance): add docs/compliance reference examples and README
Adds docs/compliance/ as the canonical onboarding reference for the
compliance module, covering every field in each generated YAML artifact
with inline annotations and explaining the docs/compliance/ (examples)
vs compliance/ (live artifacts) split.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:22:01 +00:00
cc2bf44fd2 feat(scripts): add emit-data-map compliance script + tests
Adds scripts/compliance/emit-data-map.mjs which walks Payload collection
configs (packages/*/integrations/cms/collections/*.ts), applies
PAYLOAD_AUTH_PII_DEFAULTS + custom.authPii overrides, and emits a
deterministic YAML PII inventory at compliance/data-map.yml.

Supports --print (stdout) and --check (diff vs committed, exit 1 on
mismatch) modes. Ships with 26 unit tests covering happy path, auth
defaults, authPii overrides, --check match/mismatch, and empty
collections. Wired as `compliance:data-map` root package script.

Adds @typescript-eslint/parser to root devDependencies (already in
workspace via core-eslint, now made explicit for scripts/ usage).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 19:44:55 +00:00
a32ff12a69 feat(auth): add PII and retention metadata to users collection
Tag displayName as identification-username PII and declare daily purge
with 30-day post-deletion hard-delete retention. PAYLOAD_AUTH_PII_DEFAULTS
covers email/credentials automatically — no authPii override needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 19:03:58 +00:00
7ea38bc5b9 feat(core-shared): add retention-purge background job
Adds retention-purge.job.ts to packages/core-shared/src/payload/retention-purge/.
Walks every Payload collection's custom.retention.purgeSchedule, registers one
scheduled job per collection via IJobQueue, queries rows past their activeRetention
period (by createdAt for from-creation, updatedAt for from-last-access), and applies
pseudonymize (null PII fields) or hard-delete per postDeletion.action. Emits one
IAuditLog.record entry per processed row; gracefully skips when auditLog is absent.

Unit tests cover: schedule registration, trigger-type routing, hard-delete branch,
pseudonymize branch, audit emission, graceful auditLog skip, no-activeRetention
short-circuit, and input validation error cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:56:14 +00:00
1eb32ab23b feat(core-eslint): add pii-declaration-must-be-complete rule
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>
2026-05-18 18:33:48 +00:00
0d4be0a4f4 fix(coverage): exempt .d.ts files from diff coverage gate
Ambient declaration files have no runtime code so v8 coverage
never generates DA records for them. Without an allowlist entry,
coverage:diff reports no-coverage-data for every .d.ts in the
diff. Add /\.d\.ts$/ to ALLOWED_GLOBS with a companion test.

Also configure @vitest/coverage-v8 for core-shared and add
targeted vitest exclusions for infrastructure files that are not
unit-testable (DI symbols, interface files, tRPC context, Sentry
SDK init) — bringing core-shared into the L2 aggregate and making
the L1 diff gate enforce coverage on new executable code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:26:50 +00:00
a94e8032b5 feat(core-shared): add PII and retention type primitives
Introduces PiiCategory, DataProcessingPurpose, RetentionTrigger,
RetentionAction, FieldPii, FieldRetention, PAYLOAD_AUTH_PII_DEFAULTS,
PurgeSchedule, and CollectionRetention in core-shared/payload/.
Augments payload's FieldCustom and CollectionCustom interfaces via
ambient declaration so downstream collection configs gain typed
custom.pii and custom.retention / custom.authPii fields.

Credential fields (password, salt, hash, resetPasswordToken,
resetPasswordExpiration, loginAttempts, lockUntil, apiKey, apiKeyIndex)
are null in PAYLOAD_AUTH_PII_DEFAULTS to exclude security material
from DPA mapping. Adds @vitest/coverage-v8 and coverage exclusions
for boilerplate infrastructure files so coverage:diff is gated on
new executable code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:23:24 +00:00
a7e0bf290d feat(core-analytics): add React provider and useAnalytics hook
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>
2026-05-18 15:54:30 +00:00
92b17f7d64 feat(core-shared): add Analyzed brand check to assertFeatureConformance
Mirrors the existing Audited check: when a use case declares
analyticsEvents.length > 0 and the bound function lacks the __analyzed
brand, assertFeatureConformance throws ConformanceError at boot time.

Adds three synthetic conformance tests: passes when brand present +
events declared, throws naming Analyzed when events declared + brand
missing, passes when events empty + brand absent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:19:29 +00:00
c7bdf7cf3b feat(core-analytics): add withAnalytics wrapper and Analyzed brand export
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>
2026-05-18 11:52:44 +00:00
563eab06a6 feat(core-analytics): add IAnalytics interface, types, and NoopAnalytics
Replaces generator placeholder with IAnalytics interface (track, identify,
pageView, flush), AnalyticsAttributeValue + AnalyticsUser types, and
NoopAnalytics implementation. Adds sibling tests covering all four methods
with 100% coverage. All conformance + coverage gates pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:31:58 +00:00
e17d60b8ac feat(navigation): update feature generator templates to emit wireUseCase
Replace inline withSpan(withCapture(factory(deps))) form in the binder
templates with wireUseCase({...}) calls so newly scaffolded features are
consistent with the migrated production features.

Also add assertFeatureConformance to bind-dev-seed.ts.hbs (aligns with
the migrated auth/navigation pattern) and fix bind-dev-seed.test.ts.hbs
to call binders with the ctx object form (BindContext) instead of the old
two-argument (tracer, logger) form.

Verified by running turbo gen feature testfeature and confirming:
  - Generated binders use wireUseCase for use cases
  - All 5 conformance gates pass on the scaffold
  - Scaffold cleaned up post-verification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 18:36:30 +00:00
81a791c5fd refactor(navigation): migrate binders to wireUseCase for getHeader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 18:08:42 +00:00
b93ce25b2a refactor(media): migrate binders to wireUseCase for all 3 use cases
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:59:59 +00:00
88b41798d6 refactor(auth): migrate use-case binders to wireUseCase
Replace inline withSpan + withCapture blocks for signIn, signUp, and
signOut use cases in both bind-production.ts and bind-dev-seed.ts with
wireUseCase calls. Removes 27 lines of boilerplate per binder file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:51:16 +00:00
bf0b049583 feat(coverage): L0 unification — close test gaps in nav, media, mp
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>
2026-05-13 16:39:08 +02:00
bd5a077227 feat(coverage): pnpm coverage:aggregate + L2 implementation
Lands L2 of the agent-first coverage architecture (ADR-020) — the
aggregated trend store.

Script: scripts/coverage/aggregate.mjs (zero-dep Node ESM)
  - discoverLcovs: walks packages/* and apps/* for coverage/lcov.info
  - normalizeLcov: rewrites SF entries from package-relative (vitest's
    output) to repo-relative, so the merged file matches git diff paths
  - summarizeLcov: computes statement/branch/function/line percentages
    from LF/LH/BRF/BRH/FNF/FNH summary records
  - aggregate: merges all lcovs and returns mergedLcov + summary
  - Writes coverage/lcov.info (gitignored — large) and
    coverage/summary.json (committed — trend via git log -- ...) with
    timestamp, short commit SHA, repo + per-package percentages

Test surface: scripts/coverage/aggregate.test.mjs (10 tests, all green)
  - Fixtures at __fixtures__/aggregate-pkg-a.lcov +
    aggregate-pkg-b.lcov (synthetic, structured to make percentages
    deterministic)
  - Covers: path normalization (prefix, absolute, double-prefix
    avoidance), summary computation (percentages, zero-division,
    rounding), discovery (packages + apps, missing dirs), full
    aggregation in a tmp repo

Wired:
  - root package.json adds "coverage:aggregate" script
  - .gitignore restructured: per-package coverage/ stays ignored,
    aggregated /coverage/ ignored EXCEPT summary.json (committed for
    trend) and .gitkeep markers

L1 allowlist fix folded in (scripts/coverage/diff.mjs):
  - The previous (^|/)coverage/ regex accidentally caught
    scripts/coverage/* — replaced with anchored patterns
    (^coverage/, ^packages/*/coverage/, ^apps/*/coverage/)
  - Allowlist scripts/ and turbo/generators/ since they're dev tooling
    tested via node --test, outside vitest's v8 lcov pipeline

Smoke-tested end-to-end:
  - pnpm coverage:aggregate merged 3 lcovs (auth + media + navigation
    from this session's earlier runs), repo coverage 95.22% statements
  - pnpm coverage:diff against HEAD~1 with the new merged lcov reports
    PASS — all 6 diff files correctly allowlisted

First committed snapshot of coverage/summary.json lands with this
commit, anchoring the trend history at this state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:10:22 +02:00