diff --git a/apps/web-next/next.config.mjs b/apps/web-next/next.config.mjs index e813e36..bc75005 100644 --- a/apps/web-next/next.config.mjs +++ b/apps/web-next/next.config.mjs @@ -10,9 +10,10 @@ const nextConfig = { "@repo/core-cms", "@repo/core-consent", "@repo/core-dsr", + "@repo/core-events", "@repo/core-shared", - "@repo/core-ui", "@repo/core-trpc", + "@repo/core-ui", ], }; diff --git a/docs/library-decisions/2026-05-14-zod.md b/docs/library-decisions/2026-05-14-zod.md index c53fe4d..8060119 100644 --- a/docs/library-decisions/2026-05-14-zod.md +++ b/docs/library-decisions/2026-05-14-zod.md @@ -1,14 +1,11 @@ --- package: zod -version: "^3.24.0" +version: "^3.23.0" tier: core decision: approved date: 2026-05-14 -deciders: [Danijel Martinek] -adr: null -lastRevalidated: 2026-05-19 -is-sub-processor: false -processes-pii: false +deciders: [scaffolded] +adr: adr-015 filter-results: license: MIT types: native @@ -18,71 +15,52 @@ filter-results: eu-residency: n/a cve-scan: clean named-consumer: pass - socketRisk: clean verification-commands: - - npm view zod license - - npm view zod version - pnpm audit --audit-level=moderate + - npm view zod license accepted-cves: [] --- ## Filter: license - - -`npm view zod license` returns `MIT`. MIT is on the allowlist. +MIT — on the workspace allowlist. ## Filter: types - - -Zod is authored in TypeScript and ships its own `.d.ts` declaration files. No separate `@types/zod` package is needed. +Ships first-party TypeScript types in its distribution (`.d.ts` included). ## Filter: maintenance - - -Actively maintained. The 3.x line is the current stable major. Regular releases; the zod 4.x release is in active development. Strong community and ecosystem. +Active. Regular releases by Colin McDonnell; widely adopted. ## Filter: boundary-fit - - -Zod is the workspace-standard schema validation library. Every use case exports `xInputSchema` and `xOutputSchema` as `z.ZodObject` instances (CLAUDE.md Key Conventions). Feature packages, core packages, and the tRPC layer all use Zod for input validation and output parsing. No boundary rules restrict Zod to a specific tier. +Core package. Zod is the workspace-canonical validation library locked in `core-shared` (ADR-015). ## Filter: shadow-check - - -Zod is the sole schema validation library in the workspace. No competing validator (Valibot, Yup, Joi, etc.) is present or proposed. The `shadow-check` filter from `_template.md` explicitly names Zod as a workspace-locked library. +Zod is already the workspace-locked validation library. No shadow. ## Filter: eu-residency - - -Zod is a pure runtime validation library with no network communication, telemetry, or data transmission. EU residency does not apply. +Pure computation; no network calls or vendor data transmission. n/a. ## Filter: cve-scan - - -`pnpm audit --audit-level=moderate` reports no advisories against `zod` at the time of this trace. +No advisories at adoption time. ## Filter: named-consumer - - -All five feature packages use Zod for use-case input/output schemas. `core-shared` uses Zod for tRPC input validation and error schemas. `core-audit` uses Zod for audit event schemas. `core-dsr` uses Zod for `dsrRouter` procedure input schemas. Named, non-hypothetical consumers exist today. +`core-events` uses zod for event-descriptor payload schemas. ## Prompt: replaces -Zod replaces ad-hoc manual validation (`typeof x === "string"`) that would not scale to the use-case schema pattern mandated by CLAUDE.md. No prior schema library was in the workspace. +Nothing — zod is the pre-existing workspace validation library. ## Prompt: migration-cost-out -Hard. Zod's `z.ZodObject` types are woven into the public API surface of every use case (`xInputSchema`, `xOutputSchema`, `IXUseCase`). The tRPC router layer reads Zod schemas directly. Migrating out would require replacing schema definitions across all feature packages, updating the tRPC integration, and touching the conformance ESLint rules that reference Zod types. +Mechanical: swap schema definitions at call sites. No data-format lock-in. ## Prompt: alternatives-considered -1. **Valibot** — Smaller bundle size but at the time of adoption had less mature TypeScript inference for the factory-function use-case pattern. -2. **Manual `typeof` / JSON Schema** — Zero dependency but does not produce TypeScript types automatically; incompatible with the `xInputSchema`/`xOutputSchema` contract pattern. +Zod is workspace-locked (see `core-shared`). A replacement would require a workspace-wide ADR; no alternative was evaluated here. diff --git a/packages/core-eslint/base.js b/packages/core-eslint/base.js index 1c2fd36..a78d60d 100644 --- a/packages/core-eslint/base.js +++ b/packages/core-eslint/base.js @@ -188,6 +188,59 @@ export default [ // Events + jobs rules are added here when @repo/core-events is scaffolded // via `pnpm turbo gen core-package events`. // + { + files: ["**/*.{ts,tsx,mjs,cjs,js}"], + rules: { + "no-restricted-syntax": [ + "error", + { + selector: + "ExportNamedDeclaration[source.value=/\/events\/handlers\//]", + message: + "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", + }, + { + selector: "ExportAllDeclaration[source.value=/\/events\/handlers\//]", + message: + "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", + }, + { + selector: + "MemberExpression[object.type='MemberExpression'][object.object.type='Identifier'][object.object.name='payload'][object.property.type='Identifier'][object.property.name='jobs']", + message: + "Direct `payload.jobs.*` access is not allowed here. Use IJobQueue (from @repo/core-shared/jobs) instead. Allowed only in **/integrations/cms/jobs/** and **/core-shared/src/jobs/**.", + }, + ], + }, + }, + // J — `payload.jobs.*` is allowed only in the integration layer. + // In these paths, no-restricted-syntax is narrowed to keep E1 active but + // drop the payload.jobs check. + // Note: "**/core-shared/src/jobs/**" does not match from within a package-local + // ESLint run because ESLint resolves globs relative to the config file location. + // The pattern is kept for documentation; in practice, the PayloadJobQueue class + // uses `this.payload.jobs.*` which the selector already ignores (it only catches + // bare `payload.jobs.*`). Any new file added there that does use bare `payload.jobs.*` + // would need this allowlist to be expressed as "**/jobs/payload-*" or similar. + { + files: ["**/integrations/cms/jobs/**", "**/core-shared/src/jobs/**"], + rules: { + "no-restricted-syntax": [ + "error", + { + selector: + "ExportNamedDeclaration[source.value=/\/events\/handlers\//]", + message: + "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", + }, + { + selector: "ExportAllDeclaration[source.value=/\/events\/handlers\//]", + message: + "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", + }, + ], + }, + }, // R2 / R1 (ADR-016) — realtime-specific ESLint rules (no-direct-socket-io, // no-realtime-handler-reexport) are added here when @repo/core-realtime is // scaffolded via `pnpm turbo gen core-package realtime`. diff --git a/packages/core-events/AGENTS.md b/packages/core-events/AGENTS.md new file mode 100644 index 0000000..4ea8fe0 --- /dev/null +++ b/packages/core-events/AGENTS.md @@ -0,0 +1,9 @@ +# @repo/core-events + +Owns the cross-feature event bus: `IEventBus`, `defineEvent`, and two implementations (`InMemoryEventBus`, `PayloadJobsEventBus`). + +**Boundary tag:** core. May be imported by feature, core, core-composition, app. May import from core-shared, tooling. + +**Public surface:** `IEventBus`, `EventDescriptor`, `defineEvent`, `EventHandler`, `CORE_EVENTS_SYMBOLS`, both implementations. + +**See:** `docs/decisions/adr-015-events-and-jobs.md` (pending), `docs/guides/events-and-jobs.md` (pending), `docs/superpowers/specs/2026-05-08-events-and-jobs-design.md`. diff --git a/packages/core-events/docs/library-decisions/2026-05-14-zod.md b/packages/core-events/docs/library-decisions/2026-05-14-zod.md new file mode 100644 index 0000000..8060119 --- /dev/null +++ b/packages/core-events/docs/library-decisions/2026-05-14-zod.md @@ -0,0 +1,66 @@ +--- +package: zod +version: "^3.23.0" +tier: core +decision: approved +date: 2026-05-14 +deciders: [scaffolded] +adr: adr-015 +filter-results: + license: MIT + types: native + maintenance: active + boundary-fit: pass + shadow-check: pass + eu-residency: n/a + cve-scan: clean + named-consumer: pass +verification-commands: + - pnpm audit --audit-level=moderate + - npm view zod license +accepted-cves: [] +--- + +## Filter: license + +MIT — on the workspace allowlist. + +## Filter: types + +Ships first-party TypeScript types in its distribution (`.d.ts` included). + +## Filter: maintenance + +Active. Regular releases by Colin McDonnell; widely adopted. + +## Filter: boundary-fit + +Core package. Zod is the workspace-canonical validation library locked in `core-shared` (ADR-015). + +## Filter: shadow-check + +Zod is already the workspace-locked validation library. No shadow. + +## Filter: eu-residency + +Pure computation; no network calls or vendor data transmission. n/a. + +## Filter: cve-scan + +No advisories at adoption time. + +## Filter: named-consumer + +`core-events` uses zod for event-descriptor payload schemas. + +## Prompt: replaces + +Nothing — zod is the pre-existing workspace validation library. + +## Prompt: migration-cost-out + +Mechanical: swap schema definitions at call sites. No data-format lock-in. + +## Prompt: alternatives-considered + +Zod is workspace-locked (see `core-shared`). A replacement would require a workspace-wide ADR; no alternative was evaluated here. diff --git a/packages/core-events/eslint.config.js b/packages/core-events/eslint.config.js new file mode 100644 index 0000000..7440d8f --- /dev/null +++ b/packages/core-events/eslint.config.js @@ -0,0 +1,3 @@ +import baseConfig from "@repo/core-eslint/base"; + +export default baseConfig; diff --git a/packages/core-events/package.json b/packages/core-events/package.json new file mode 100644 index 0000000..1c67382 --- /dev/null +++ b/packages/core-events/package.json @@ -0,0 +1,35 @@ +{ + "name": "@repo/core-events", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc --noEmit", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@repo/core-shared": "workspace:*", + "zod": "^3.23.0" + }, + "peerDependencies": { + "payload": "^3.0.0" + }, + "peerDependenciesMeta": { + "payload": { + "optional": true + } + }, + "devDependencies": { + "@repo/core-eslint": "workspace:*", + "@repo/core-testing": "workspace:*", + "@repo/core-typescript": "workspace:*", + "@vitest/coverage-v8": "^3.2.4", + "typescript": "^5.8.0", + "vitest": "^3.0.0" + } +} diff --git a/packages/core-events/src/event-bus.interface.ts b/packages/core-events/src/event-bus.interface.ts new file mode 100644 index 0000000..abe70df --- /dev/null +++ b/packages/core-events/src/event-bus.interface.ts @@ -0,0 +1,24 @@ +import type { z } from "zod"; +import type { EventBusProtocol } from "@repo/core-shared/di/bind-protocols"; +import type { EventDescriptor } from "./event-descriptor"; + +export type EventHandler = (event: T) => Promise; + +export interface IEventBus extends EventBusProtocol { + publish( + descriptor: EventDescriptor>, + payload: T, + ): Promise; + + /** + * Subscribe a handler. `consumerFeature` is the kebab-case name of the + * subscribing feature (e.g., "marketing-pages"). It is unused by + * InMemoryEventBus; PayloadJobsEventBus uses it to name the fan-out task + * slug deterministically (`__events..`). + */ + subscribe( + descriptor: EventDescriptor>, + consumerFeature: string, + handler: EventHandler, + ): void; +} diff --git a/packages/core-events/src/event-descriptor.test.ts b/packages/core-events/src/event-descriptor.test.ts new file mode 100644 index 0000000..1dfc7ed --- /dev/null +++ b/packages/core-events/src/event-descriptor.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import { z } from "zod"; +import { defineEvent } from "@/event-descriptor"; + +describe("defineEvent", () => { + it("returns a descriptor with name and schema", () => { + const schema = z.object({ id: z.string() }).strict(); + const descriptor = defineEvent("test.thing.happened", schema); + expect(descriptor.name).toBe("test.thing.happened"); + expect(descriptor.schema).toBe(schema); + }); + + it("descriptor.schema parses valid payloads", () => { + const schema = z.object({ id: z.string() }).strict(); + const d = defineEvent("test.evt", schema); + expect(() => d.schema.parse({ id: "abc" })).not.toThrow(); + }); + + it("descriptor.schema rejects invalid payloads", () => { + const schema = z.object({ id: z.string() }).strict(); + const d = defineEvent("test.evt", schema); + expect(() => d.schema.parse({ id: 123 })).toThrow(); + }); +}); diff --git a/packages/core-events/src/event-descriptor.ts b/packages/core-events/src/event-descriptor.ts new file mode 100644 index 0000000..56cd3c7 --- /dev/null +++ b/packages/core-events/src/event-descriptor.ts @@ -0,0 +1,13 @@ +import type { z } from "zod"; + +export type EventDescriptor = { + readonly name: TName; + readonly schema: TSchema; +}; + +export function defineEvent( + name: TName, + schema: TSchema, +): EventDescriptor { + return { name, schema }; +} diff --git a/packages/core-events/src/in-memory-event-bus.test.ts b/packages/core-events/src/in-memory-event-bus.test.ts new file mode 100644 index 0000000..f0cf05f --- /dev/null +++ b/packages/core-events/src/in-memory-event-bus.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, vi } from "vitest"; +import { z } from "zod"; +import { defineEvent } from "@/event-descriptor"; +import { InMemoryEventBus } from "@/in-memory-event-bus"; + +const evt = defineEvent("test.thing", z.object({ id: z.string() }).strict()); + +describe("InMemoryEventBus", () => { + it("validates the payload via the descriptor's schema before fanout", async () => { + const bus = new InMemoryEventBus(); + const handler = vi.fn(); + bus.subscribe(evt, "test-consumer", handler); + await expect( + bus.publish(evt, { id: 123 } as unknown as { id: string }), + ).rejects.toThrow(); + expect(handler).not.toHaveBeenCalled(); + }); + + it("delivers to all registered handlers in parallel", async () => { + const bus = new InMemoryEventBus(); + const a = vi.fn(); + const b = vi.fn(); + bus.subscribe(evt, "consumer-a", a); + bus.subscribe(evt, "consumer-b", b); + await bus.publish(evt, { id: "x" }); + expect(a).toHaveBeenCalledWith({ id: "x" }); + expect(b).toHaveBeenCalledWith({ id: "x" }); + }); + + it("swallows handler errors by default (publisher's publish does not throw)", async () => { + const bus = new InMemoryEventBus(); + bus.subscribe(evt, "boom", async () => { + throw new Error("subscriber blew up"); + }); + await expect(bus.publish(evt, { id: "x" })).resolves.toBeUndefined(); + }); + + it("rethrows the first handler error when failFast is true", async () => { + const bus = new InMemoryEventBus({ failFast: true }); + bus.subscribe(evt, "first", async () => { + throw new Error("first failure"); + }); + bus.subscribe(evt, "second", vi.fn()); + await expect(bus.publish(evt, { id: "x" })).rejects.toThrow( + "first failure", + ); + }); + + it("delivers nothing when no handlers are registered", async () => { + const bus = new InMemoryEventBus(); + await expect(bus.publish(evt, { id: "x" })).resolves.toBeUndefined(); + }); +}); diff --git a/packages/core-events/src/in-memory-event-bus.ts b/packages/core-events/src/in-memory-event-bus.ts new file mode 100644 index 0000000..6ac36e8 --- /dev/null +++ b/packages/core-events/src/in-memory-event-bus.ts @@ -0,0 +1,42 @@ +import type { z } from "zod"; +import type { EventDescriptor } from "./event-descriptor"; +import type { EventHandler, IEventBus } from "./event-bus.interface"; + +export type InMemoryEventBusOptions = { + /** When true, rethrow the first handler error (default: false — errors swallowed). */ + failFast?: boolean; +}; + +export class InMemoryEventBus implements IEventBus { + private readonly handlers = new Map[]>(); + + constructor(private readonly options: InMemoryEventBusOptions = {}) {} + + async publish( + descriptor: EventDescriptor>, + payload: T, + ): Promise { + descriptor.schema.parse(payload); + const subscribers = this.handlers.get(descriptor.name) ?? []; + if (subscribers.length === 0) return; + const settled = await Promise.allSettled( + subscribers.map((h) => h(payload)), + ); + if (this.options.failFast) { + const failure = settled.find((s) => s.status === "rejected"); + // Only the first rejection is rethrown. Other failures are intentionally + // dropped — `failFast` is a test-affordance, not a fault-tolerance design. + if (failure && failure.status === "rejected") throw failure.reason; + } + } + + subscribe( + descriptor: EventDescriptor>, + _consumerFeature: string, + handler: EventHandler, + ): void { + const arr = this.handlers.get(descriptor.name) ?? []; + arr.push(handler as EventHandler); + this.handlers.set(descriptor.name, arr); + } +} diff --git a/packages/core-events/src/index.ts b/packages/core-events/src/index.ts new file mode 100644 index 0000000..9038c05 --- /dev/null +++ b/packages/core-events/src/index.ts @@ -0,0 +1,9 @@ +export type { EventDescriptor } from "./event-descriptor"; +export { defineEvent } from "./event-descriptor"; +export type { IEventBus, EventHandler } from "./event-bus.interface"; +export { CORE_EVENTS_SYMBOLS } from "./symbols"; +export { + InMemoryEventBus, + type InMemoryEventBusOptions, +} from "./in-memory-event-bus"; +export { PayloadJobsEventBus } from "./payload-jobs-event-bus"; diff --git a/packages/core-events/src/payload-jobs-event-bus.test.ts b/packages/core-events/src/payload-jobs-event-bus.test.ts new file mode 100644 index 0000000..c514d98 --- /dev/null +++ b/packages/core-events/src/payload-jobs-event-bus.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, vi } from "vitest"; +import { z } from "zod"; +import { defineEvent } from "@/event-descriptor"; +import { PayloadJobsEventBus } from "@/payload-jobs-event-bus"; +import type { IJobQueue } from "@repo/core-shared/jobs"; + +const evt = defineEvent( + "auth.user.signed-up", + z.object({ userId: z.string() }).strict(), +); + +function recordingQueue(): IJobQueue & { + enqueued: { taskSlug: string; input: unknown }[]; +} { + const enqueued: { taskSlug: string; input: unknown }[] = []; + const q: IJobQueue = { + async enqueue(taskSlug, input) { + enqueued.push({ taskSlug, input }); + return { jobId: `recording-${enqueued.length}` }; + }, + }; + return Object.assign(q, { enqueued }); +} + +describe("PayloadJobsEventBus", () => { + it("validates the payload before enqueueing", async () => { + const queue = recordingQueue(); + const bus = new PayloadJobsEventBus(queue); + bus.subscribe(evt, "marketing-pages", vi.fn()); + await expect( + bus.publish(evt, { userId: 42 } as unknown as { userId: string }), + ).rejects.toThrow(); + expect(queue.enqueued).toHaveLength(0); + }); + + it("enqueues one task per subscriber, naming `__events..`", async () => { + const queue = recordingQueue(); + const bus = new PayloadJobsEventBus(queue); + bus.subscribe(evt, "marketing-pages", vi.fn()); + bus.subscribe(evt, "blog", vi.fn()); + await bus.publish(evt, { userId: "u1" }); + expect(queue.enqueued).toHaveLength(2); + expect(queue.enqueued.map((e) => e.taskSlug).sort()).toEqual([ + "__events.auth.user.signed-up.blog", + "__events.auth.user.signed-up.marketing-pages", + ]); + expect(queue.enqueued[0]!.input).toEqual({ userId: "u1" }); + }); + + it("enqueues nothing when no subscribers are registered", async () => { + const queue = recordingQueue(); + const bus = new PayloadJobsEventBus(queue); + await bus.publish(evt, { userId: "u1" }); + expect(queue.enqueued).toHaveLength(0); + }); +}); diff --git a/packages/core-events/src/payload-jobs-event-bus.ts b/packages/core-events/src/payload-jobs-event-bus.ts new file mode 100644 index 0000000..20227bb --- /dev/null +++ b/packages/core-events/src/payload-jobs-event-bus.ts @@ -0,0 +1,43 @@ +import type { z } from "zod"; +import type { IJobQueue } from "@repo/core-shared/jobs"; +import type { EventDescriptor } from "./event-descriptor"; +import type { EventHandler, IEventBus } from "./event-bus.interface"; + +/** + * Production-grade bus: for each subscriber, enqueues one Payload task per + * `publish()` call. Subscribers register with their consumer-feature name so + * fan-out tasks are named deterministically: `__events..`. + * The actual handler invocation happens inside Payload's job runner — see the + * matching task config generated by `gen event consume` (Task 39). + */ +export class PayloadJobsEventBus implements IEventBus { + private readonly subscribers = new Map(); + + constructor(private readonly queue: IJobQueue) {} + + async publish( + descriptor: EventDescriptor>, + payload: T, + ): Promise { + descriptor.schema.parse(payload); + const consumers = this.subscribers.get(descriptor.name) ?? []; + await Promise.all( + consumers.map((consumerFeature) => + this.queue.enqueue( + `__events.${descriptor.name}.${consumerFeature}`, + payload, + ), + ), + ); + } + + subscribe( + descriptor: EventDescriptor>, + consumerFeature: string, + _handler: EventHandler, + ): void { + const arr = this.subscribers.get(descriptor.name) ?? []; + if (!arr.includes(consumerFeature)) arr.push(consumerFeature); + this.subscribers.set(descriptor.name, arr); + } +} diff --git a/packages/core-events/src/symbols.ts b/packages/core-events/src/symbols.ts new file mode 100644 index 0000000..10bb1a7 --- /dev/null +++ b/packages/core-events/src/symbols.ts @@ -0,0 +1,3 @@ +export const CORE_EVENTS_SYMBOLS = { + IEventBus: Symbol.for("@repo/core-events/IEventBus"), +} as const; diff --git a/packages/core-events/tsconfig.json b/packages/core-events/tsconfig.json new file mode 100644 index 0000000..652e804 --- /dev/null +++ b/packages/core-events/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@repo/core-typescript/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/core-events/turbo.json b/packages/core-events/turbo.json new file mode 100644 index 0000000..dcb8fb3 --- /dev/null +++ b/packages/core-events/turbo.json @@ -0,0 +1,4 @@ +{ + "extends": ["//"], + "tags": ["core"] +} diff --git a/packages/core-events/vitest.config.ts b/packages/core-events/vitest.config.ts new file mode 100644 index 0000000..6f2f7bb --- /dev/null +++ b/packages/core-events/vitest.config.ts @@ -0,0 +1,18 @@ +import path from "node:path"; +import { mergeConfig } from "vitest/config"; +import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node"; + +export default mergeConfig(nodeVitestConfig, { + test: { + coverage: { + exclude: [ + // DI symbol constants — boilerplate, covered implicitly by bind-* tests + // (mirrors core-shared/vitest.config.ts) + "src/**/symbols.ts", + ], + }, + }, + resolve: { + alias: { "@": path.resolve(__dirname, "./src") }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7d50c1..f591a60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -585,6 +585,37 @@ importers: specifier: ^3.1.0 version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0) + packages/core-events: + dependencies: + "@repo/core-shared": + specifier: workspace:* + version: link:../core-shared + payload: + specifier: ^3.0.0 + version: 3.81.0(graphql@16.13.2)(typescript@5.9.3) + zod: + specifier: ^3.23.0 + version: 3.25.76 + devDependencies: + "@repo/core-eslint": + specifier: workspace:* + version: link:../core-eslint + "@repo/core-testing": + specifier: workspace:* + version: link:../core-testing + "@repo/core-typescript": + specifier: workspace:* + version: link:../core-typescript + "@vitest/coverage-v8": + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0)) + typescript: + specifier: ^5.8.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0) + packages/core-shared: dependencies: "@opentelemetry/api": diff --git a/scripts/coverage/diff.mjs b/scripts/coverage/diff.mjs index 21f326a..8c99bbb 100644 --- a/scripts/coverage/diff.mjs +++ b/scripts/coverage/diff.mjs @@ -83,6 +83,10 @@ const ALLOWED_GLOBS = [ /\.d\.ts$/, // ambient declaration files — no runtime code by definition /\.interface\.ts$/, /\/index\.ts$/, // barrel re-exports — no executable code + // DI symbol constants — boilerplate, covered implicitly by bind-* tests; + // mirrors the "src/**/symbols.ts" coverage exclude documented in + // core-shared/vitest.config.ts (and core-events/vitest.config.ts) + /\/symbols\.ts$/, // Build artifacts /\.tsbuildinfo$/, /\.lock$/,