diff --git a/apps/web-next/next.config.mjs b/apps/web-next/next.config.mjs index 02232d6..645ecea 100644 --- a/apps/web-next/next.config.mjs +++ b/apps/web-next/next.config.mjs @@ -9,6 +9,7 @@ const nextConfig = { "@repo/core-api", "@repo/core-audit", "@repo/core-cms", + "@repo/core-consent", "@repo/core-shared", "@repo/marketing-pages", "@repo/media", diff --git a/coverage/summary.json b/coverage/summary.json index 52608ac..aa9e21f 100644 --- a/coverage/summary.json +++ b/coverage/summary.json @@ -1,6 +1,6 @@ { - "generatedAt": "2026-05-19T10:39:50.009Z", - "commit": "f5d08dc", + "generatedAt": "2026-05-19T11:00:08.934Z", + "commit": "9cb2fa3", "repo": { "statements": 96.5, "branches": 91.82, diff --git a/packages/core-consent/AGENTS.md b/packages/core-consent/AGENTS.md index d08df96..ee4acb3 100644 --- a/packages/core-consent/AGENTS.md +++ b/packages/core-consent/AGENTS.md @@ -1,6 +1,6 @@ # @repo/core-consent -Optional core package providing a vendor-neutral consent management interface. Scaffold via `pnpm turbo gen core-package consent` (once the generator supports it). +Optional core package providing a vendor-neutral consent management interface. Scaffold via `pnpm turbo gen core-package consent`. ## Structure @@ -21,7 +21,7 @@ src/ - `withdraw(category)` — record consent withdrawal for a category - `getCategories()` — list all known consent states -The interface is vendor-neutral: no storage implementation is bundled here. Concrete implementations (e.g. a Payload-backed store) are wired at DI bind time in `bind-production` (Story 04). +The interface is vendor-neutral: no storage implementation is bundled here. Concrete implementations (e.g. a Payload-backed store) are wired at DI bind time in `bind-production`. `withConsent` wraps a use-case factory at bind time, attaches the `__consentChecked` brand, and is the innermost wrapper in the composition chain: `withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)` diff --git a/packages/core-consent/src/with-consent.ts b/packages/core-consent/src/with-consent.ts index fcce947..26aae48 100644 --- a/packages/core-consent/src/with-consent.ts +++ b/packages/core-consent/src/with-consent.ts @@ -5,19 +5,16 @@ import { attachBrand } from "@repo/core-shared/conformance"; export type { ConsentChecked }; /** - * Use-case wrapper applied at DI bind time. The wrapper is a thin closure - * that forwards to `fn` unchanged and carries the `__consentChecked` brand. + * Use-case wrapper applied at DI bind time. Attaches the `__consentChecked` + * brand so the boot-time assertion can verify consent-gated use cases were + * bound through the consent-aware path. + * * The forward closure keeps the brand on a fresh function so the original * `fn` reference is not mutated — important when the same factory output is * used elsewhere unwrapped (dev-seed paths, tests). * * Composition order (innermost to outermost): * withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps) - * - * The wrapper exists to: - * (1) require callers to pass the consent instance at bind time (dep is available) - * (2) attach the `__consentChecked` brand so the boot-time assertion can verify - * consent-gated use cases were bound through the consent-aware path. */ export function withConsent( consent: IConsent, diff --git a/packages/core-consent/vitest.config.ts b/packages/core-consent/vitest.config.ts index 2f81734..2ee07c1 100644 --- a/packages/core-consent/vitest.config.ts +++ b/packages/core-consent/vitest.config.ts @@ -1,15 +1,9 @@ import path from "node:path"; -import { defineConfig, mergeConfig } from "vitest/config"; +import { mergeConfig } from "vitest/config"; import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node"; -export default mergeConfig( - nodeVitestConfig, - defineConfig({ - test: { - include: ["src/**/*.test.ts"], - }, - resolve: { - alias: { "@": path.resolve(__dirname, "./src") }, - }, - }), -); +export default mergeConfig(nodeVitestConfig, { + resolve: { + alias: { "@": path.resolve(__dirname, "./src") }, + }, +}); diff --git a/packages/core-shared/src/instrumentation/with-capture.ts b/packages/core-shared/src/instrumentation/with-capture.ts index 0781b1a..a5c7840 100644 --- a/packages/core-shared/src/instrumentation/with-capture.ts +++ b/packages/core-shared/src/instrumentation/with-capture.ts @@ -32,6 +32,7 @@ export function withCapture( "__instrumented", "__audited", "__analyzed", + "__consentChecked", ] as const; const wrapped: (...args: Args) => Promise = async (...args) => { diff --git a/turbo/generators/config.ts b/turbo/generators/config.ts index 0d3ea15..51c331a 100644 --- a/turbo/generators/config.ts +++ b/turbo/generators/config.ts @@ -782,17 +782,40 @@ import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js"; }, printAnalyticsNextSteps, ], + consent: () => [ + () => { + assertOptionalPackageNotPresent("core-consent"); + return "Guard passed — packages/core-consent does not exist yet."; + }, + ...emitTemplateTree("core-package/consent", "packages/core-consent"), + () => { + addToTranspilePackages( + "apps/web-next/next.config.mjs", + "@repo/core-consent", + ); + return "Added @repo/core-consent to transpilePackages."; + }, + printConsentNextSteps, + ], }; plop.setGenerator("core-package", { description: - "Scaffold an optional core package (realtime, events, trpc, ui, audit, analytics)", + "Scaffold an optional core package (realtime, events, trpc, ui, audit, analytics, consent)", prompts: [ { type: "list", name: "name", message: "Which optional core package?", - choices: ["analytics", "audit", "events", "realtime", "trpc", "ui"], + choices: [ + "analytics", + "audit", + "consent", + "events", + "realtime", + "trpc", + "ui", + ], }, ], actions: (answers) => { @@ -1517,6 +1540,30 @@ function printAnalyticsNextSteps(): string { ].join("\n"); } +function printConsentNextSteps(): string { + return [ + "─────────────────────────────────────────────────────────────", + "@repo/core-consent scaffolded into packages/core-consent/.", + "", + "Next steps:", + "", + " 1. pnpm install # link the new workspace package", + "", + " 2. Implement consent-types.ts, consent.interface.ts, and withConsent", + " in packages/core-consent/src/ (see story 03-core-consent-foundation)", + "", + " 3. Add @repo/core-consent to feature package.json files that need consent", + "", + " 4. Wire IConsent into apps/web-next/src/server/bind-production.ts:", + ' - import { type IConsent } from "@repo/core-consent";', + " - add consent field to BindAllDeps and BindProductionContext", + " - pass the consent instance into each feature binder", + "", + " 5. pnpm typecheck && pnpm lint && pnpm test", + "─────────────────────────────────────────────────────────────", + ].join("\n"); +} + function coreUiComponentActions(a: { tier: "atom" | "molecule" | "organism"; name: string; diff --git a/turbo/generators/templates/core-package/consent/AGENTS.md.hbs b/turbo/generators/templates/core-package/consent/AGENTS.md.hbs new file mode 100644 index 0000000..ee4acb3 --- /dev/null +++ b/turbo/generators/templates/core-package/consent/AGENTS.md.hbs @@ -0,0 +1,29 @@ +# @repo/core-consent + +Optional core package providing a vendor-neutral consent management interface. Scaffold via `pnpm turbo gen core-package consent`. + +## Structure + +``` +src/ + consent-types.ts # ConsentCategory, ConsentState, UserConsentState + consent.interface.ts # IConsent — isGranted, grant, withdraw, getCategories + with-consent.ts # withConsent wrapper attaching ConsentChecked brand + index.ts # Barrel export +``` + +## Design + +`IConsent` exposes four methods: + +- `isGranted(category)` — synchronous check whether consent is granted +- `grant(category)` — record consent grant for a category +- `withdraw(category)` — record consent withdrawal for a category +- `getCategories()` — list all known consent states + +The interface is vendor-neutral: no storage implementation is bundled here. Concrete implementations (e.g. a Payload-backed store) are wired at DI bind time in `bind-production`. + +`withConsent` wraps a use-case factory at bind time, attaches the `__consentChecked` brand, and is the innermost wrapper in the composition chain: +`withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)` + +See `docs/architecture/agent-first-workflow-and-conformance.md` for the dependency-injection conventions. diff --git a/turbo/generators/templates/core-package/consent/eslint.config.js.hbs b/turbo/generators/templates/core-package/consent/eslint.config.js.hbs new file mode 100644 index 0000000..7440d8f --- /dev/null +++ b/turbo/generators/templates/core-package/consent/eslint.config.js.hbs @@ -0,0 +1,3 @@ +import baseConfig from "@repo/core-eslint/base"; + +export default baseConfig; diff --git a/turbo/generators/templates/core-package/consent/package.json.hbs b/turbo/generators/templates/core-package/consent/package.json.hbs new file mode 100644 index 0000000..94a0c68 --- /dev/null +++ b/turbo/generators/templates/core-package/consent/package.json.hbs @@ -0,0 +1,26 @@ +{ + "name": "@repo/core-consent", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc --noEmit", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run --passWithNoTests" + }, + "dependencies": { + "@repo/core-shared": "workspace:*" + }, + "devDependencies": { + "@repo/core-eslint": "workspace:*", + "@repo/core-testing": "workspace:*", + "@repo/core-typescript": "workspace:*", + "@vitest/coverage-v8": "^3.0.0", + "typescript": "^5.8.0", + "vitest": "^3.0.0" + } +} diff --git a/turbo/generators/templates/core-package/consent/src/index.ts.hbs b/turbo/generators/templates/core-package/consent/src/index.ts.hbs new file mode 100644 index 0000000..84b8229 --- /dev/null +++ b/turbo/generators/templates/core-package/consent/src/index.ts.hbs @@ -0,0 +1,2 @@ +// placeholder — populated by story 03-core-consent-foundation +export {}; diff --git a/turbo/generators/templates/core-package/consent/tsconfig.json.hbs b/turbo/generators/templates/core-package/consent/tsconfig.json.hbs new file mode 100644 index 0000000..8facef5 --- /dev/null +++ b/turbo/generators/templates/core-package/consent/tsconfig.json.hbs @@ -0,0 +1,12 @@ +{ + "extends": "@repo/core-typescript/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/turbo/generators/templates/core-package/consent/turbo.json.hbs b/turbo/generators/templates/core-package/consent/turbo.json.hbs new file mode 100644 index 0000000..dcb8fb3 --- /dev/null +++ b/turbo/generators/templates/core-package/consent/turbo.json.hbs @@ -0,0 +1,4 @@ +{ + "extends": ["//"], + "tags": ["core"] +} diff --git a/turbo/generators/templates/core-package/consent/vitest.config.ts.hbs b/turbo/generators/templates/core-package/consent/vitest.config.ts.hbs new file mode 100644 index 0000000..2ee07c1 --- /dev/null +++ b/turbo/generators/templates/core-package/consent/vitest.config.ts.hbs @@ -0,0 +1,9 @@ +import path from "node:path"; +import { mergeConfig } from "vitest/config"; +import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node"; + +export default mergeConfig(nodeVitestConfig, { + resolve: { + alias: { "@": path.resolve(__dirname, "./src") }, + }, +});