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>
This commit is contained in:
@@ -9,6 +9,7 @@ const nextConfig = {
|
|||||||
"@repo/core-api",
|
"@repo/core-api",
|
||||||
"@repo/core-audit",
|
"@repo/core-audit",
|
||||||
"@repo/core-cms",
|
"@repo/core-cms",
|
||||||
|
"@repo/core-consent",
|
||||||
"@repo/core-shared",
|
"@repo/core-shared",
|
||||||
"@repo/marketing-pages",
|
"@repo/marketing-pages",
|
||||||
"@repo/media",
|
"@repo/media",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-05-19T10:39:50.009Z",
|
"generatedAt": "2026-05-19T11:00:08.934Z",
|
||||||
"commit": "f5d08dc",
|
"commit": "9cb2fa3",
|
||||||
"repo": {
|
"repo": {
|
||||||
"statements": 96.5,
|
"statements": 96.5,
|
||||||
"branches": 91.82,
|
"branches": 91.82,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# @repo/core-consent
|
# @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
|
## Structure
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ src/
|
|||||||
- `withdraw(category)` — record consent withdrawal for a category
|
- `withdraw(category)` — record consent withdrawal for a category
|
||||||
- `getCategories()` — list all known consent states
|
- `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:
|
`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)`
|
`withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)`
|
||||||
|
|||||||
@@ -5,19 +5,16 @@ import { attachBrand } from "@repo/core-shared/conformance";
|
|||||||
export type { ConsentChecked };
|
export type { ConsentChecked };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use-case wrapper applied at DI bind time. The wrapper is a thin closure
|
* Use-case wrapper applied at DI bind time. Attaches the `__consentChecked`
|
||||||
* that forwards to `fn` unchanged and carries the `__consentChecked` brand.
|
* 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
|
* 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
|
* `fn` reference is not mutated — important when the same factory output is
|
||||||
* used elsewhere unwrapped (dev-seed paths, tests).
|
* used elsewhere unwrapped (dev-seed paths, tests).
|
||||||
*
|
*
|
||||||
* Composition order (innermost to outermost):
|
* Composition order (innermost to outermost):
|
||||||
* withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)
|
* 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<Args extends unknown[], R>(
|
export function withConsent<Args extends unknown[], R>(
|
||||||
consent: IConsent,
|
consent: IConsent,
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import path from "node:path";
|
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";
|
import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node";
|
||||||
|
|
||||||
export default mergeConfig(
|
export default mergeConfig(nodeVitestConfig, {
|
||||||
nodeVitestConfig,
|
resolve: {
|
||||||
defineConfig({
|
alias: { "@": path.resolve(__dirname, "./src") },
|
||||||
test: {
|
},
|
||||||
include: ["src/**/*.test.ts"],
|
});
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: { "@": path.resolve(__dirname, "./src") },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function withCapture<Args extends unknown[], R>(
|
|||||||
"__instrumented",
|
"__instrumented",
|
||||||
"__audited",
|
"__audited",
|
||||||
"__analyzed",
|
"__analyzed",
|
||||||
|
"__consentChecked",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const wrapped: (...args: Args) => Promise<R> = async (...args) => {
|
const wrapped: (...args: Args) => Promise<R> = async (...args) => {
|
||||||
|
|||||||
@@ -782,17 +782,40 @@ import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js";
|
|||||||
},
|
},
|
||||||
printAnalyticsNextSteps,
|
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", {
|
plop.setGenerator("core-package", {
|
||||||
description:
|
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: [
|
prompts: [
|
||||||
{
|
{
|
||||||
type: "list",
|
type: "list",
|
||||||
name: "name",
|
name: "name",
|
||||||
message: "Which optional core package?",
|
message: "Which optional core package?",
|
||||||
choices: ["analytics", "audit", "events", "realtime", "trpc", "ui"],
|
choices: [
|
||||||
|
"analytics",
|
||||||
|
"audit",
|
||||||
|
"consent",
|
||||||
|
"events",
|
||||||
|
"realtime",
|
||||||
|
"trpc",
|
||||||
|
"ui",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: (answers) => {
|
actions: (answers) => {
|
||||||
@@ -1517,6 +1540,30 @@ function printAnalyticsNextSteps(): string {
|
|||||||
].join("\n");
|
].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: {
|
function coreUiComponentActions(a: {
|
||||||
tier: "atom" | "molecule" | "organism";
|
tier: "atom" | "molecule" | "organism";
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import baseConfig from "@repo/core-eslint/base";
|
||||||
|
|
||||||
|
export default baseConfig;
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// placeholder — populated by story 03-core-consent-foundation
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/core-typescript/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": ["//"],
|
||||||
|
"tags": ["core"]
|
||||||
|
}
|
||||||
@@ -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") },
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user