@pattern-stack/codegen 0.14.2 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/dist/{job-orchestrator.protocol-CARhMLCO.d.ts → job-orchestrator.protocol-DubMVbm9.d.ts} +1 -1
  3. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +2 -2
  4. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
  5. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.d.ts +1 -1
  6. package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.d.ts +1 -1
  7. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.d.ts +1 -1
  8. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
  9. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +2 -2
  10. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  11. package/dist/runtime/subsystems/bridge/bridge.protocol.d.ts +1 -1
  12. package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +2 -2
  13. package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
  14. package/dist/runtime/subsystems/bridge/generated/registry.d.ts +1 -1
  15. package/dist/runtime/subsystems/bridge/index.d.ts +2 -2
  16. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  17. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +1 -1
  18. package/dist/runtime/subsystems/events/event-registry.d.ts +77 -0
  19. package/dist/runtime/subsystems/events/event-registry.js +1 -0
  20. package/dist/runtime/subsystems/events/event-registry.js.map +1 -0
  21. package/dist/runtime/subsystems/events/events.module.d.ts +26 -1
  22. package/dist/runtime/subsystems/events/events.module.js +6 -5
  23. package/dist/runtime/subsystems/events/events.module.js.map +1 -1
  24. package/dist/runtime/subsystems/events/index.d.ts +1 -0
  25. package/dist/runtime/subsystems/events/index.js +6 -5
  26. package/dist/runtime/subsystems/events/index.js.map +1 -1
  27. package/dist/runtime/subsystems/index.d.ts +2 -2
  28. package/dist/runtime/subsystems/index.js +6 -5
  29. package/dist/runtime/subsystems/index.js.map +1 -1
  30. package/dist/runtime/subsystems/jobs/index.d.ts +2 -2
  31. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  32. package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +2 -2
  33. package/dist/runtime/subsystems/jobs/job-handler.base.js.map +1 -1
  34. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +2 -2
  35. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +2 -2
  36. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +2 -2
  37. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
  38. package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +2 -2
  39. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +2 -2
  40. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +2 -2
  41. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +2 -2
  42. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +2 -2
  43. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +2 -2
  44. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -1
  45. package/dist/runtime/subsystems/jobs/job-worker.d.ts +2 -2
  46. package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
  47. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +2 -2
  48. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  49. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  50. package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +2 -2
  51. package/dist/runtime/subsystems/observability/index.d.ts +2 -2
  52. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +2 -2
  53. package/dist/runtime/subsystems/observability/observability.service.d.ts +2 -2
  54. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +2 -2
  55. package/dist/runtime/subsystems/observability/reporters/index.d.ts +2 -2
  56. package/dist/src/cli/index.js +1332 -1274
  57. package/dist/src/cli/index.js.map +1 -1
  58. package/package.json +1 -1
  59. package/runtime/subsystems/bridge/bridge-delivery-handler.ts +1 -1
  60. package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +1 -1
  61. package/runtime/subsystems/bridge/bridge.protocol.ts +1 -1
  62. package/runtime/subsystems/bridge/event-flow.service.ts +1 -1
  63. package/runtime/subsystems/events/event-registry.ts +77 -0
  64. package/runtime/subsystems/events/events.module.ts +39 -6
  65. package/runtime/subsystems/events/index.ts +12 -0
  66. package/runtime/subsystems/jobs/job-handler.base.ts +1 -1
@@ -6273,8 +6273,8 @@ async function generateScopeEntityType(opts) {
6273
6273
  }
6274
6274
 
6275
6275
  // src/cli/shared/subsystem-barrel-generator.ts
6276
- import fs6 from "fs";
6277
- import path9 from "path";
6276
+ import fs7 from "fs";
6277
+ import path10 from "path";
6278
6278
 
6279
6279
  // src/cli/shared/subsystem-detect.ts
6280
6280
  import fs4 from "fs";
@@ -6856,1384 +6856,1446 @@ async function generateBridgeRegistry(opts) {
6856
6856
  };
6857
6857
  }
6858
6858
 
6859
- // src/cli/shared/subsystem-barrel-generator.ts
6860
- function quoteOpts(opts) {
6861
- const entries = Object.entries(opts).filter(([, v]) => v !== void 0);
6862
- if (entries.length === 0) return "";
6863
- return "{ " + entries.map(([k, v]) => `${k}: ${typeof v === "string" ? `'${v}'` : String(v)}`).join(", ") + " }";
6864
- }
6865
- function jsonToTs(value) {
6866
- if (value === null || value === void 0) return "undefined";
6867
- if (typeof value === "string") return `'${value.replace(/'/g, "\\'")}'`;
6868
- if (typeof value === "number" || typeof value === "boolean") return String(value);
6869
- if (Array.isArray(value)) return `[${value.map(jsonToTs).join(", ")}]`;
6870
- if (typeof value === "object") {
6871
- const entries = Object.entries(value).filter(
6872
- ([, v]) => v !== void 0
6873
- );
6874
- return `{ ${entries.map(([k, v]) => `${k}: ${jsonToTs(v)}`).join(", ")} }`;
6859
+ // src/cli/shared/event-codegen-generator.ts
6860
+ import fs6 from "fs";
6861
+ import path9 from "path";
6862
+
6863
+ // src/parser/load-events.ts
6864
+ import { basename as basename2, resolve as resolve4 } from "path";
6865
+ function loadErrorToIssue2(error) {
6866
+ const issues = [];
6867
+ issues.push({
6868
+ severity: "error",
6869
+ type: "parse_error",
6870
+ message: error.error,
6871
+ path: error.filePath
6872
+ });
6873
+ if (error.details) {
6874
+ for (const detail of error.details) {
6875
+ issues.push({
6876
+ severity: "error",
6877
+ type: "schema_error",
6878
+ message: detail,
6879
+ path: error.filePath
6880
+ });
6881
+ }
6875
6882
  }
6876
- return "undefined";
6883
+ return issues;
6877
6884
  }
6878
- function quoteBullmqDomainOpts(input) {
6879
- const { backend, multiTenant, bullExt } = input;
6880
- if (backend !== "bullmq" || !bullExt) {
6881
- return quoteOpts({ backend, multiTenant });
6882
- }
6883
- const parts = [`backend: 'bullmq'`];
6884
- if (multiTenant) parts.push(`multiTenant: true`);
6885
- parts.push(`extensions: { bullmq: ${jsonToTs(bullExt)} }`);
6886
- return `{ ${parts.join(", ")} }`;
6885
+ function stripYamlExt(file) {
6886
+ const base = basename2(file);
6887
+ if (base.endsWith(".yaml")) return base.slice(0, -".yaml".length);
6888
+ if (base.endsWith(".yml")) return base.slice(0, -".yml".length);
6889
+ return base;
6887
6890
  }
6888
- function workerPoolsClause(cfg, bridgeInstalled) {
6889
- const explicit = cfg?.worker_pools;
6890
- if (Array.isArray(explicit) && explicit.length > 0) {
6891
- const list = explicit.filter((p) => typeof p === "string").map((p) => `'${p}'`).join(", ");
6892
- if (list.length > 0) return `pools: [${list}]`;
6891
+ function loadEvents(eventsDir, entityNames) {
6892
+ const events = [];
6893
+ const issues = [];
6894
+ const resolvedDir = resolve4(eventsDir);
6895
+ let files;
6896
+ try {
6897
+ files = findYamlFiles(resolvedDir);
6898
+ } catch {
6899
+ issues.push({
6900
+ severity: "warning",
6901
+ type: "no_events_dir",
6902
+ message: `No events directory found at: ${resolvedDir}`,
6903
+ path: resolvedDir
6904
+ });
6905
+ return { events, issues };
6893
6906
  }
6894
- if (cfg?.all_pools === true) return "allPools: true";
6895
- if (bridgeInstalled) return "allPools: true";
6896
- return "";
6907
+ if (files.length === 0) {
6908
+ issues.push({
6909
+ severity: "warning",
6910
+ type: "no_files",
6911
+ message: `No event YAML files found in directory: ${resolvedDir}`,
6912
+ path: resolvedDir
6913
+ });
6914
+ return { events, issues };
6915
+ }
6916
+ const entityNameSet = new Set(entityNames);
6917
+ const seenTypes = /* @__PURE__ */ new Map();
6918
+ for (const filePath of files) {
6919
+ const result = loadEventFromYaml(filePath);
6920
+ if (!result.success) {
6921
+ issues.push(...loadErrorToIssue2(result));
6922
+ continue;
6923
+ }
6924
+ const { definition } = result;
6925
+ const baseName = stripYamlExt(filePath);
6926
+ if (baseName !== definition.type) {
6927
+ issues.push({
6928
+ severity: "error",
6929
+ type: "event_filename_mismatch",
6930
+ message: `Event file '${baseName}' must contain 'type: ${baseName}' (found 'type: ${definition.type}')`,
6931
+ path: filePath,
6932
+ suggestion: `Rename the file to '${definition.type}.yaml' or fix the 'type' field to '${baseName}'`
6933
+ });
6934
+ continue;
6935
+ }
6936
+ if (definition.direction === "change" && definition.aggregate !== void 0 && !entityNameSet.has(definition.aggregate)) {
6937
+ issues.push({
6938
+ severity: "error",
6939
+ type: "unknown_aggregate",
6940
+ message: `change event '${definition.type}' references unknown aggregate entity '${definition.aggregate}'`,
6941
+ path: filePath,
6942
+ suggestion: `Define entities/${definition.aggregate}.yaml or fix the aggregate value`
6943
+ });
6944
+ continue;
6945
+ }
6946
+ if (seenTypes.has(definition.type)) {
6947
+ issues.push({
6948
+ severity: "error",
6949
+ type: "duplicate_event_type",
6950
+ message: `Duplicate event type '${definition.type}' (already declared in ${seenTypes.get(definition.type)})`,
6951
+ path: filePath
6952
+ });
6953
+ continue;
6954
+ }
6955
+ seenTypes.set(definition.type, filePath);
6956
+ events.push(definition);
6957
+ }
6958
+ return { events, issues };
6897
6959
  }
6898
- var COMPOSERS = {
6899
- events: ({ moduleImport, cfg }) => {
6900
- const backend = cfg?.backend ?? "drizzle";
6901
- const multiTenant = Boolean(cfg?.multi_tenant);
6902
- return {
6903
- imports: [
6904
- `import { EventsModule } from '${moduleImport("events", "events.module")}';`
6905
- ],
6906
- calls: [
6907
- ` EventsModule.forRoot(${quoteOpts({ backend, multiTenant })}),`
6908
- ]
6909
- };
6910
- },
6911
- jobs: ({ moduleImport, cfg, bridgeInstalled }) => {
6912
- const backend = cfg?.backend ?? "drizzle";
6913
- const multiTenant = Boolean(cfg?.multi_tenant);
6914
- const workerMode = (cfg?.worker_mode ?? "standalone").trim();
6915
- const imports = [
6916
- `import { JobsDomainModule } from '${moduleImport("jobs", "jobs-domain.module")}';`
6917
- ];
6918
- const bullExt = backend === "bullmq" ? cfg?.extensions?.bullmq : void 0;
6919
- const domainOpts = quoteBullmqDomainOpts({ backend, multiTenant, bullExt });
6920
- const calls = [` JobsDomainModule.forRoot(${domainOpts}),`];
6921
- if (workerMode === "embedded") {
6922
- imports.push(
6923
- `import { JobWorkerModule } from '${moduleImport("jobs", "job-worker.module")}';`
6924
- );
6925
- const parts = [`mode: 'embedded'`];
6926
- if (backend === "bullmq") {
6927
- parts.push(`backend: 'bullmq'`);
6928
- if (bullExt) {
6929
- parts.push(`domainModuleExtensions: { bullmq: ${jsonToTs(bullExt)} }`);
6930
- }
6960
+ function desugarEntityEvents(entity) {
6961
+ const entityName = entity.entity.name;
6962
+ const entityEvents = entity.events ?? [];
6963
+ return entityEvents.map((ev) => {
6964
+ const payload = {};
6965
+ for (const [key, typeString] of Object.entries(ev.body)) {
6966
+ if (!isEventFieldType(typeString)) {
6967
+ throw new Error(
6968
+ `Entity '${entityName}' event '${ev.name}' field '${key}' has unknown type '${typeString}' \u2014 expected one of ${EVENT_FIELD_TYPES.join("|")}`
6969
+ );
6931
6970
  }
6932
- const poolsClause = workerPoolsClause(cfg, bridgeInstalled);
6933
- if (poolsClause) parts.push(poolsClause);
6934
- calls.push(` JobWorkerModule.forRoot({ ${parts.join(", ")} }),`);
6935
- }
6936
- return { imports, calls };
6937
- },
6938
- bridge: ({ moduleImport, cfg, mode }) => {
6939
- const backend = cfg?.backend ?? "drizzle";
6940
- const multiTenant = Boolean(cfg?.multi_tenant);
6941
- const imports = [
6942
- `import { BridgeModule } from '${moduleImport("bridge", "bridge.module")}';`
6943
- ];
6944
- if (mode === "package") {
6945
- imports.push(`import { bridgeRegistry } from './bridge-registry';`);
6946
- return {
6947
- imports,
6948
- calls: [
6949
- ` BridgeModule.forRoot({ backend: '${backend}', multiTenant: ${multiTenant}, registry: bridgeRegistry }),`
6950
- ]
6951
- };
6971
+ payload[key] = { type: typeString, nullable: false };
6952
6972
  }
6953
- return {
6954
- imports,
6955
- calls: [
6956
- ` BridgeModule.forRoot(${quoteOpts({ backend, multiTenant })}),`
6957
- ]
6973
+ const def = {
6974
+ type: ev.name,
6975
+ tier: "domain",
6976
+ direction: "change",
6977
+ aggregate: entityName,
6978
+ payload,
6979
+ retry: { attempts: 3, backoff: "exponential" },
6980
+ version: 1,
6981
+ pool: DIRECTION_TO_POOL.change
6958
6982
  };
6959
- },
6960
- integration: ({ moduleImport, cfg }) => {
6961
- const backend = cfg?.backend ?? "drizzle";
6962
- const multiTenant = Boolean(cfg?.multi_tenant);
6983
+ return def;
6984
+ });
6985
+ }
6986
+ function isEventFieldType(s) {
6987
+ return EVENT_FIELD_TYPES.includes(s);
6988
+ }
6989
+
6990
+ // src/cli/shared/event-codegen-generator.ts
6991
+ var PACKAGE_EVENTS_RUNTIME_IMPORT = "@pattern-stack/codegen/runtime/subsystems/events/index";
6992
+ function buildRegistryAugmentationBlock(events) {
6993
+ const sorted = [...events].sort((a, b) => a.type.localeCompare(b.type));
6994
+ const chunks = [];
6995
+ chunks.push(
6996
+ `// Package-mode trigger typing (ADR-037): merge these events into the runtime's`
6997
+ );
6998
+ chunks.push(
6999
+ `// augmentable \`DomainEventRegistry\` so the bridge + \`@JobHandler({ triggers })\``
7000
+ );
7001
+ chunks.push(
7002
+ `// types (which key off the published \`EventTypeName\`) see THIS project's events`
7003
+ );
7004
+ chunks.push(`// with full \`EventOfType<T>\` payload typing.`);
7005
+ chunks.push(`declare module '${PACKAGE_EVENTS_RUNTIME_IMPORT}' {`);
7006
+ chunks.push(` interface DomainEventRegistry {`);
7007
+ for (const ev of sorted) {
7008
+ chunks.push(` '${ev.type}': ${toPascalCase2(ev.type)}Event;`);
7009
+ }
7010
+ chunks.push(` }`);
7011
+ chunks.push(`}`);
7012
+ return chunks.join("\n");
7013
+ }
7014
+ function eventsRuntimeImports(mode) {
7015
+ if (mode === "package") {
6963
7016
  return {
6964
- imports: [
6965
- `import { IntegrationModule } from '${moduleImport("integration", "integration.module")}';`
6966
- ],
6967
- calls: [
6968
- ` IntegrationModule.forRoot(${quoteOpts({ backend, multiTenant })}),`
6969
- ]
7017
+ protocol: PACKAGE_EVENTS_RUNTIME_IMPORT,
7018
+ tokens: PACKAGE_EVENTS_RUNTIME_IMPORT,
7019
+ errors: PACKAGE_EVENTS_RUNTIME_IMPORT
6970
7020
  };
6971
7021
  }
6972
- };
6973
- var PACKAGE2 = "@pattern-stack/codegen";
6974
- function makeModuleImport(mode, subsystemsRel) {
6975
- if (mode === "vendored") {
6976
- return (subsystem, moduleBasename) => `${subsystemsRel}/${subsystem}/${moduleBasename}`;
6977
- }
6978
- return (subsystem) => subsystem === "events" ? `${PACKAGE2}/subsystems` : `${PACKAGE2}/runtime/subsystems/${subsystem}/index`;
7022
+ return {
7023
+ protocol: "../event-bus.protocol",
7024
+ tokens: "../events.tokens",
7025
+ errors: "../events-errors"
7026
+ };
6979
7027
  }
6980
- var COMPOSABLE_ORDER = ["events", "jobs", "bridge", "integration"];
6981
- var HEADER4 = `// AUTO-GENERATED by @pattern-stack/codegen. DO NOT EDIT.
6982
- // Subsystem composition barrel \u2014 reflects \`subsystems.install\` in
6983
- // codegen.config.yaml and the per-subsystem option blocks
6984
- // (\`events:\`, \`jobs:\`, \`bridge:\`, \`integration:\`).
6985
- //
6986
- // Wire into AppModule once:
6987
- //
6988
- // import { SUBSYSTEM_MODULES } from './generated/subsystems';
6989
- // @Module({ imports: [DatabaseModule, ...SUBSYSTEM_MODULES, ...GENERATED_MODULES] })
6990
- //
6991
- // Regenerated by every \`codegen entity new\` / \`codegen subsystem install\`.
6992
-
7028
+ var HEADER4 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
7029
+ // Run \`codegen entity new --all\` to refresh.
6993
7030
  `;
6994
- function buildSubsystemBarrel(installed, config, subsystemsRel, mode = "vendored") {
6995
- const moduleImport = makeModuleImport(mode, subsystemsRel);
6996
- const actable = installed.filter((i) => i.status !== "incomplete");
6997
- const installedNames = new Set(actable.map((i) => i.name));
6998
- const bridgeInstalled = installedNames.has("bridge");
6999
- const emitted = [];
7000
- const skipped = [];
7001
- const allImports = [`import type { DynamicModule } from '@nestjs/common';`];
7002
- const allCalls = [];
7003
- for (const name of COMPOSABLE_ORDER) {
7004
- if (!installedNames.has(name)) continue;
7005
- const composer = COMPOSERS[name];
7006
- if (!composer) {
7007
- skipped.push(name);
7008
- continue;
7009
- }
7010
- const cfg = config?.[name] ?? void 0;
7011
- const out = composer({ moduleImport, cfg, mode, bridgeInstalled });
7012
- allImports.push(...out.imports);
7013
- allCalls.push(...out.calls);
7014
- emitted.push(name);
7031
+ function toCamelCase(input) {
7032
+ const parts = input.split("_").filter(Boolean);
7033
+ if (parts.length === 0) return input;
7034
+ const [first, ...rest] = parts;
7035
+ return (first ?? "") + rest.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7036
+ }
7037
+ function toPascalCase2(input) {
7038
+ return input.split("_").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7039
+ }
7040
+ var TS_TYPE_BY_FIELD = {
7041
+ uuid: "string",
7042
+ string: "string",
7043
+ number: "number",
7044
+ boolean: "boolean",
7045
+ date: "Date",
7046
+ json: "Record<string, unknown>"
7047
+ };
7048
+ var ZOD_EXPR_BY_FIELD = {
7049
+ uuid: "z.string().uuid()",
7050
+ string: "z.string()",
7051
+ number: "z.number()",
7052
+ boolean: "z.boolean()",
7053
+ date: "z.coerce.date()",
7054
+ json: "z.record(z.unknown())"
7055
+ };
7056
+ function tsTypeForField(field) {
7057
+ let base;
7058
+ if (field.type === "array") {
7059
+ const itemType = field.items;
7060
+ base = `${TS_TYPE_BY_FIELD[itemType]}[]`;
7061
+ } else {
7062
+ base = TS_TYPE_BY_FIELD[field.type];
7015
7063
  }
7016
- for (const inst of actable) {
7017
- if (!COMPOSABLE_ORDER.includes(inst.name) && !COMPOSERS[inst.name]) {
7018
- skipped.push(inst.name);
7064
+ return field.nullable ? `${base} | null` : base;
7065
+ }
7066
+ function zodExprForField(field) {
7067
+ let base;
7068
+ if (field.type === "array") {
7069
+ const itemType = field.items;
7070
+ base = `z.array(${ZOD_EXPR_BY_FIELD[itemType]})`;
7071
+ } else {
7072
+ base = ZOD_EXPR_BY_FIELD[field.type];
7073
+ }
7074
+ return field.nullable ? `${base}.nullable()` : base;
7075
+ }
7076
+ function aggregateTypeLiteral(ev) {
7077
+ return ev.aggregate ?? ev.source ?? ev.destination ?? ev.type;
7078
+ }
7079
+ function collectEntityEvents(entitiesDir) {
7080
+ const events = [];
7081
+ const issues = [];
7082
+ if (!fs6.existsSync(entitiesDir)) {
7083
+ return { events, issues };
7084
+ }
7085
+ const files = findYamlFiles(entitiesDir);
7086
+ for (const filePath of files) {
7087
+ const result = loadEntityFromYaml(filePath);
7088
+ if (!result.success) continue;
7089
+ try {
7090
+ events.push(...desugarEntityEvents(result.definition));
7091
+ } catch (err) {
7092
+ issues.push({
7093
+ severity: "error",
7094
+ type: "entity_event_desugar_failed",
7095
+ message: err instanceof Error ? err.message : String(err),
7096
+ path: filePath
7097
+ });
7019
7098
  }
7020
7099
  }
7021
- const exportLine = allCalls.length === 0 ? `export const SUBSYSTEM_MODULES: DynamicModule[] = [];
7022
- ` : `export const SUBSYSTEM_MODULES: DynamicModule[] = [
7023
- ${allCalls.join("\n")}
7024
- ];
7025
- `;
7026
- const body = allImports.join("\n") + "\n\n" + exportLine;
7027
- return { content: HEADER4 + body, emitted, skipped };
7100
+ return { events, issues };
7028
7101
  }
7029
- async function regenerateSubsystemBarrel(opts) {
7030
- const { ctx, dryRun = false } = opts;
7031
- const generatedDir = opts.generatedDir ?? resolveGeneratedDir(ctx);
7032
- const mode = resolveRuntimeMode(ctx.config);
7033
- const installed = mode === "package" ? configuredInstalledSubsystems(
7034
- ctx.config
7035
- ) : await detectInstalledSubsystems(ctx);
7036
- const subsystemsAbs = resolveSubsystemsRoot(ctx);
7037
- const barrelAbs = path9.resolve(generatedDir, "subsystems.ts");
7038
- let subsystemsRel = path9.relative(path9.dirname(barrelAbs), subsystemsAbs).split(path9.sep).join("/");
7039
- if (!subsystemsRel.startsWith(".")) subsystemsRel = "./" + subsystemsRel;
7040
- const { content, emitted, skipped } = buildSubsystemBarrel(
7041
- installed,
7042
- ctx.config,
7043
- subsystemsRel,
7044
- mode
7102
+ function mergeEvents(topLevel, entitySugar) {
7103
+ const issues = [];
7104
+ const byType = /* @__PURE__ */ new Map();
7105
+ for (const ev of entitySugar) {
7106
+ byType.set(ev.type, ev);
7107
+ }
7108
+ for (const ev of topLevel) {
7109
+ if (byType.has(ev.type)) {
7110
+ issues.push({
7111
+ severity: "warning",
7112
+ type: "event_merge_override",
7113
+ message: `event '${ev.type}' is declared both in an entity \`events:\` block and a top-level \`events/${ev.type}.yaml\` \u2014 top-level definition wins`
7114
+ });
7115
+ }
7116
+ byType.set(ev.type, ev);
7117
+ }
7118
+ const events = Array.from(byType.values()).sort(
7119
+ (a, b) => a.type.localeCompare(b.type)
7045
7120
  );
7046
- let written = false;
7047
- if (!dryRun) {
7048
- fs6.mkdirSync(path9.dirname(barrelAbs), { recursive: true });
7049
- fs6.writeFileSync(barrelAbs, content);
7050
- written = true;
7051
- if (mode === "package" && emitted.includes("bridge")) {
7052
- const registryPath = path9.resolve(generatedDir, "bridge-registry.ts");
7053
- if (!fs6.existsSync(registryPath)) {
7054
- fs6.writeFileSync(
7055
- registryPath,
7056
- buildBridgeRegistryContent([], PACKAGE_BRIDGE_TYPE_IMPORT)
7057
- );
7058
- }
7121
+ return { events, issues };
7122
+ }
7123
+ function collectMergedEvents(opts) {
7124
+ const { entitiesDir, eventsDir } = opts;
7125
+ const entityNames = [];
7126
+ if (fs6.existsSync(entitiesDir)) {
7127
+ const entityFiles = findYamlFiles(entitiesDir);
7128
+ for (const f of entityFiles) {
7129
+ const result = loadEntityFromYaml(f);
7130
+ if (result.success) entityNames.push(result.definition.entity.name);
7059
7131
  }
7060
7132
  }
7061
- return {
7062
- subsystemBarrel: barrelAbs,
7063
- emitted,
7064
- skipped,
7065
- content,
7066
- written
7067
- };
7133
+ const topLevelResult = loadEvents(eventsDir, entityNames);
7134
+ const { events: entitySugar, issues: sugarIssues } = collectEntityEvents(entitiesDir);
7135
+ const { events: merged, issues: mergeIssues } = mergeEvents(
7136
+ topLevelResult.events,
7137
+ entitySugar
7138
+ );
7139
+ const issues = [
7140
+ ...topLevelResult.issues,
7141
+ ...sugarIssues,
7142
+ ...mergeIssues
7143
+ ];
7144
+ return { events: merged, issues };
7068
7145
  }
7146
+ function buildTypesContent(events, mode = "vendored") {
7147
+ const sorted = [...events].sort((a, b) => a.type.localeCompare(b.type));
7148
+ const protocolImport = eventsRuntimeImports(mode).protocol;
7149
+ if (sorted.length === 0) {
7150
+ return HEADER4 + `
7151
+ import type { DomainEvent } from '${protocolImport}';
7069
7152
 
7070
- // src/cli/shared/subsystem-schema-generator.ts
7071
- import fs7 from "fs";
7072
- import path10 from "path";
7073
- var PACKAGE3 = "@pattern-stack/codegen";
7074
- var SCHEMA_SYMBOLS = {
7075
- events: ["domainEvents"],
7076
- jobs: [
7077
- "jobs",
7078
- "jobRuns",
7079
- "jobSteps",
7080
- "jobRunStatusEnum",
7081
- "jobStepKindEnum",
7082
- "jobStepStatusEnum",
7083
- "collisionModeEnum",
7084
- "replayFromEnum",
7085
- "parentClosePolicyEnum",
7086
- "waitKindEnum",
7087
- "triggerSourceEnum"
7088
- ],
7089
- bridge: ["bridgeDelivery", "bridgeDeliveryStatusEnum"],
7090
- integration: [
7091
- "integrationSubscriptions",
7092
- "integrationRuns",
7093
- "integrationRunItems",
7094
- "integrationRunDirectionEnum",
7095
- "integrationRunActionEnum",
7096
- "integrationRunStatusEnum",
7097
- "integrationRunItemOperationEnum",
7098
- "integrationRunItemStatusEnum"
7099
- ]
7100
- };
7101
- var SCHEMA_ORDER = ["events", "jobs", "bridge", "integration"];
7102
- var HEADER5 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
7103
- // Subsystem Drizzle schema barrel \u2014 re-exports the tables + pgEnums of every
7104
- // installed subsystem so drizzle-kit emits their CREATE TABLE / CREATE TYPE.
7105
- //
7106
- // Fold into your drizzle-kit schema entrypoint once, alongside the entity
7107
- // schema barrel:
7108
- //
7109
- // // src/schema.ts
7110
- // export * from './generated/schema'; // entity tables
7111
- // export * from './generated/subsystems-schema'; // subsystem tables + enums
7112
- //
7113
- // Regenerated by every \`codegen entity new\` / \`codegen subsystem install\`.
7153
+ export type AppDomainEvent = never;
7114
7154
 
7155
+ export type EventTypeName = string;
7156
+ export type EventOfType<T extends EventTypeName> = DomainEvent;
7157
+ export type PayloadOfType<T extends EventTypeName> = DomainEvent['payload'];
7115
7158
  `;
7116
- function schemaImportFor(mode, subsystem, subsystemsRel) {
7117
- return mode === "vendored" ? `${subsystemsRel}/${subsystem}` : `${PACKAGE3}/runtime/subsystems/${subsystem}/index`;
7118
- }
7119
- function buildSubsystemSchemaBarrel(installed, subsystemsRel, mode) {
7120
- const actable = installed.filter((i) => i.status !== "incomplete");
7121
- const installedNames = new Set(actable.map((i) => i.name));
7122
- const emitted = [];
7123
- const lines = [];
7124
- for (const name of SCHEMA_ORDER) {
7125
- if (!installedNames.has(name)) continue;
7126
- const symbols = SCHEMA_SYMBOLS[name];
7127
- if (!symbols || symbols.length === 0) continue;
7128
- const importSpec = schemaImportFor(mode, name, subsystemsRel);
7129
- lines.push(`export { ${symbols.join(", ")} } from '${importSpec}';`);
7130
- emitted.push(name);
7131
- }
7132
- const body = lines.length === 0 ? "export {};\n" : lines.join("\n") + "\n";
7133
- return { content: HEADER5 + body, emitted };
7134
- }
7135
- async function regenerateSubsystemSchemaBarrel(opts) {
7136
- const { ctx, dryRun = false } = opts;
7137
- const generatedDir = opts.generatedDir ?? resolveGeneratedDir(ctx);
7138
- const mode = resolveRuntimeMode(ctx.config);
7139
- const installed = mode === "package" ? configuredInstalledSubsystems(
7140
- ctx.config
7141
- ) : await detectInstalledSubsystems(ctx);
7142
- const subsystemsAbs = resolveSubsystemsRoot(ctx);
7143
- const barrelAbs = path10.resolve(generatedDir, "subsystems-schema.ts");
7144
- let subsystemsRel = path10.relative(path10.dirname(barrelAbs), subsystemsAbs).split(path10.sep).join("/");
7145
- if (!subsystemsRel.startsWith(".")) subsystemsRel = "./" + subsystemsRel;
7146
- const { content, emitted } = buildSubsystemSchemaBarrel(
7147
- installed,
7148
- subsystemsRel,
7149
- mode
7150
- );
7151
- let written = false;
7152
- if (!dryRun) {
7153
- fs7.mkdirSync(path10.dirname(barrelAbs), { recursive: true });
7154
- fs7.writeFileSync(barrelAbs, content);
7155
- written = true;
7156
7159
  }
7157
- return { schemaBarrel: barrelAbs, emitted, content, written };
7160
+ const chunks = [];
7161
+ chunks.push(HEADER4);
7162
+ chunks.push("");
7163
+ chunks.push(`import type { DomainEvent } from '${protocolImport}';`);
7164
+ chunks.push("");
7165
+ for (const ev of sorted) {
7166
+ const interfaceName = `${toPascalCase2(ev.type)}Event`;
7167
+ const aggregateLit = aggregateTypeLiteral(ev);
7168
+ if (ev.description) {
7169
+ chunks.push(`/** ${ev.description} */`);
7170
+ }
7171
+ chunks.push(`export interface ${interfaceName} extends DomainEvent {`);
7172
+ chunks.push(` readonly type: '${ev.type}';`);
7173
+ chunks.push(` readonly aggregateType: '${aggregateLit}';`);
7174
+ const payloadKeys = Object.keys(ev.payload).sort();
7175
+ if (payloadKeys.length === 0) {
7176
+ chunks.push(` readonly payload: Record<string, never>;`);
7177
+ } else {
7178
+ chunks.push(` readonly payload: {`);
7179
+ for (const key of payloadKeys) {
7180
+ const field = ev.payload[key];
7181
+ if (!field) continue;
7182
+ if (field.description) {
7183
+ chunks.push(` /** ${field.description} */`);
7184
+ }
7185
+ chunks.push(` ${toCamelCase(key)}: ${tsTypeForField(field)};`);
7186
+ }
7187
+ chunks.push(` };`);
7188
+ }
7189
+ chunks.push(`}`);
7190
+ chunks.push("");
7191
+ }
7192
+ const unionMembers = sorted.map((ev) => `${toPascalCase2(ev.type)}Event`);
7193
+ chunks.push(`export type AppDomainEvent =`);
7194
+ chunks.push(` | ${unionMembers.join("\n | ")};`);
7195
+ chunks.push("");
7196
+ chunks.push(`export type EventTypeName = AppDomainEvent['type'];`);
7197
+ chunks.push(
7198
+ `export type EventOfType<T extends EventTypeName> = Extract<AppDomainEvent, { type: T }>;`
7199
+ );
7200
+ chunks.push(
7201
+ `export type PayloadOfType<T extends EventTypeName> = EventOfType<T>['payload'];`
7202
+ );
7203
+ chunks.push("");
7204
+ if (mode === "package") {
7205
+ chunks.push(buildRegistryAugmentationBlock(sorted));
7206
+ chunks.push("");
7207
+ }
7208
+ return chunks.join("\n");
7158
7209
  }
7210
+ function buildSchemasContent(events) {
7211
+ const sorted = [...events].sort((a, b) => a.type.localeCompare(b.type));
7212
+ if (sorted.length === 0) {
7213
+ return HEADER4 + `
7214
+ import { z } from 'zod';
7215
+ import type { EventTypeName } from './types';
7159
7216
 
7160
- // src/cli/shared/orchestration-generator.ts
7161
- import fs8 from "fs";
7162
- import path11 from "path";
7163
- var OrchestrationEmissionError = class extends Error {
7164
- constructor(issueType, patternName, message) {
7165
- super(`[${issueType}] ${patternName}: ${message}`);
7166
- this.issueType = issueType;
7167
- this.patternName = patternName;
7168
- }
7169
- issueType;
7170
- patternName;
7171
- name = "OrchestrationEmissionError";
7172
- };
7173
- var HEADER6 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
7174
- // See ADR-032 \u2014 Orchestration Patterns.
7217
+ export const eventPayloadSchemas = {} as Record<EventTypeName, z.ZodType>;
7175
7218
  `;
7176
- function splitWords(str) {
7177
- return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/).filter((w) => w.length > 0).map((w) => w.toLowerCase());
7178
- }
7179
- function toKebabCase2(str) {
7180
- return splitWords(str).join("-");
7181
- }
7182
- function toPascalCase2(str) {
7183
- return splitWords(str).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7184
- }
7185
- function toCamelCase(str) {
7186
- const words = splitWords(str);
7187
- if (words.length === 0) return "";
7188
- const [first, ...rest] = words;
7189
- return (first ?? "") + rest.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7190
- }
7191
- function toScreamingSnake(str) {
7192
- return splitWords(str).join("_").toUpperCase();
7193
- }
7194
- var FORBIDDEN_KEY_TYPES = /* @__PURE__ */ new Set([
7195
- "string",
7196
- "number",
7197
- "symbol",
7198
- "any",
7199
- "unknown"
7200
- ]);
7201
- function assertEmittable(pattern) {
7202
- const allRegistries2 = [
7203
- pattern.registry,
7204
- ...pattern.coKeyedRegistries ?? []
7205
- ];
7206
- for (const sibling of pattern.coKeyedRegistries ?? []) {
7207
- if (typeof sibling.name !== "string" || sibling.name.length === 0) {
7208
- throw new OrchestrationEmissionError(
7209
- "pattern_cokeyed_missing_name",
7210
- pattern.name,
7211
- `Co-keyed registry (valueType '${sibling.valueType}') is missing the required 'name' field. Each co-keyed sibling must declare an explicit name; the emitter uppercases it for the token suffix and PascalCases it for the dispatcher method (e.g. name: 'auth' \u21D2 '\${PATTERN_CONST}_AUTH_REGISTRY' + 'selectAuth(...)'). ADR-032 O-1.`
7212
- );
7213
- }
7214
7219
  }
7215
- for (const reg of allRegistries2) {
7216
- if (FORBIDDEN_KEY_TYPES.has(reg.keyType)) {
7217
- throw new OrchestrationEmissionError(
7218
- "pattern_keytype_unresolved",
7219
- pattern.name,
7220
- `registry keyType '${reg.keyType}' is a primitive \u2014 Partial<Record<K, V>> requires a literal-union K, and per-overload narrowing requires a closed enum/union. Use a string-literal union or a TypeScript enum.`
7221
- );
7222
- }
7223
- if (typeof reg.keyTypeImport !== "string" || reg.keyTypeImport.length === 0) {
7224
- throw new OrchestrationEmissionError(
7225
- "pattern_missing_import_path",
7226
- pattern.name,
7227
- `registry (keyType '${reg.keyType}') is missing 'keyTypeImport'. Phase 3-2 emission requires every keyType to carry a module specifier.`
7228
- );
7229
- }
7230
- if (typeof reg.valueTypeImport !== "string" || reg.valueTypeImport.length === 0) {
7231
- throw new OrchestrationEmissionError(
7232
- "pattern_missing_import_path",
7233
- pattern.name,
7234
- `registry (valueType '${reg.valueType}') is missing 'valueTypeImport'.`
7235
- );
7220
+ const chunks = [];
7221
+ chunks.push(HEADER4);
7222
+ chunks.push("");
7223
+ chunks.push(`import { z } from 'zod';`);
7224
+ chunks.push(`import type { EventTypeName } from './types';`);
7225
+ chunks.push("");
7226
+ for (const ev of sorted) {
7227
+ const schemaConst = `${toCamelCase(ev.type)}PayloadSchema`;
7228
+ const payloadKeys = Object.keys(ev.payload).sort();
7229
+ if (payloadKeys.length === 0) {
7230
+ chunks.push(`export const ${schemaConst} = z.object({}).strict();`);
7231
+ chunks.push("");
7232
+ continue;
7236
7233
  }
7237
- for (const e of reg.entries) {
7238
- if (typeof e.providerImport !== "string" || e.providerImport.length === 0) {
7239
- throw new OrchestrationEmissionError(
7240
- "pattern_missing_import_path",
7241
- pattern.name,
7242
- `entry '${e.key}' provider '${e.provider}' is missing 'providerImport'.`
7243
- );
7244
- }
7234
+ chunks.push(`export const ${schemaConst} = z.object({`);
7235
+ for (const key of payloadKeys) {
7236
+ const field = ev.payload[key];
7237
+ if (!field) continue;
7238
+ chunks.push(` ${toCamelCase(key)}: ${zodExprForField(field)},`);
7245
7239
  }
7240
+ chunks.push(`}).strict();`);
7241
+ chunks.push("");
7246
7242
  }
7247
- }
7248
- function patternNames(pattern) {
7249
- const slug = toKebabCase2(pattern.name);
7250
- const pascal = toPascalCase2(pattern.name);
7251
- const screaming = toScreamingSnake(pattern.name);
7252
- return { slug, pascal, screaming };
7253
- }
7254
- function registryNames(pattern, reg, isPrimary) {
7255
- const { pascal, screaming } = patternNames(pattern);
7256
- if (isPrimary) {
7257
- return {
7258
- tokenConst: `${screaming}_REGISTRY`,
7259
- mapType: `${pascal}RegistryMap`,
7260
- method: "select",
7261
- overrideField: "overrides",
7262
- builderFn: `build${pascal}RegistryProviders`
7263
- };
7243
+ chunks.push(`export const eventPayloadSchemas = {`);
7244
+ for (const ev of sorted) {
7245
+ chunks.push(` '${ev.type}': ${toCamelCase(ev.type)}PayloadSchema,`);
7264
7246
  }
7265
- const namePascal = toPascalCase2(reg.name);
7266
- const nameScreaming = toScreamingSnake(reg.name);
7267
- const nameCamel = toCamelCase(reg.name);
7268
- return {
7269
- tokenConst: `${screaming}_${nameScreaming}_REGISTRY`,
7270
- mapType: `${pascal}${namePascal}RegistryMap`,
7271
- method: `select${namePascal}`,
7272
- overrideField: `${nameCamel}Overrides`,
7273
- builderFn: `build${pascal}RegistryProviders`
7274
- };
7275
- }
7276
- function dispatcherClassName(pattern) {
7277
- return pattern.dispatcher?.className ?? `${toPascalCase2(pattern.name)}Dispatcher`;
7278
- }
7279
- function moduleClassName(pattern) {
7280
- return `${toPascalCase2(pattern.name)}OrchestrationModule`;
7281
- }
7282
- function forRootOptionsTypeName(pattern) {
7283
- return `${toPascalCase2(pattern.name)}ForRootOptions`;
7247
+ chunks.push(`} as const satisfies Record<EventTypeName, z.ZodType>;`);
7248
+ chunks.push("");
7249
+ return chunks.join("\n");
7284
7250
  }
7285
- function emitTypeImports(imports) {
7286
- const grouped = /* @__PURE__ */ new Map();
7287
- for (const i of imports) {
7288
- const set = grouped.get(i.specifier) ?? /* @__PURE__ */ new Set();
7289
- set.add(i.name);
7290
- grouped.set(i.specifier, set);
7251
+ var REGISTRY_INTERFACE = [
7252
+ "export interface EventMetadata {",
7253
+ " type: EventTypeName;",
7254
+ " tier: 'domain' | 'audit';",
7255
+ " direction: 'inbound' | 'change' | 'outbound' | null;",
7256
+ " pool: 'events_inbound' | 'events_change' | 'events_outbound' | null;",
7257
+ " aggregate?: string;",
7258
+ " source?: string;",
7259
+ " destination?: string;",
7260
+ " version: number;",
7261
+ " retry: { attempts: number; backoff: 'linear' | 'exponential' };",
7262
+ "}"
7263
+ ].join("\n");
7264
+ var REGISTRY_GETTER = [
7265
+ "export function getEventMetadata<T extends EventTypeName>(type: T): EventMetadata {",
7266
+ " const meta = eventRegistry[type];",
7267
+ " if (!meta) {",
7268
+ " throw new Error(`No registry entry for event type '${String(type)}' \u2014 declare events under events/*.yaml and re-run \\`codegen entity new --all\\`.`);",
7269
+ " }",
7270
+ " return meta;",
7271
+ "}"
7272
+ ].join("\n");
7273
+ function buildRegistryContent(events) {
7274
+ const sorted = [...events].sort((a, b) => a.type.localeCompare(b.type));
7275
+ const chunks = [];
7276
+ chunks.push(HEADER4);
7277
+ chunks.push("");
7278
+ chunks.push(`import type { EventTypeName } from './types';`);
7279
+ chunks.push("");
7280
+ chunks.push(REGISTRY_INTERFACE);
7281
+ chunks.push("");
7282
+ if (sorted.length === 0) {
7283
+ chunks.push(
7284
+ `export const eventRegistry = {} as Record<EventTypeName, EventMetadata>;`
7285
+ );
7286
+ chunks.push("");
7287
+ chunks.push(REGISTRY_GETTER);
7288
+ chunks.push("");
7289
+ return chunks.join("\n");
7291
7290
  }
7292
- const out = [];
7293
- for (const specifier of [...grouped.keys()].sort()) {
7294
- const names2 = [...grouped.get(specifier)].sort();
7295
- out.push(`import type { ${names2.join(", ")} } from '${specifier}';`);
7291
+ chunks.push(`export const eventRegistry = {`);
7292
+ for (const ev of sorted) {
7293
+ const tier = ev.tier ?? "domain";
7294
+ chunks.push(` '${ev.type}': {`);
7295
+ chunks.push(` type: '${ev.type}',`);
7296
+ chunks.push(` tier: '${tier}',`);
7297
+ if (tier === "audit") {
7298
+ chunks.push(` direction: null,`);
7299
+ chunks.push(` pool: null,`);
7300
+ } else {
7301
+ chunks.push(` direction: '${ev.direction}',`);
7302
+ chunks.push(` pool: '${ev.pool}',`);
7303
+ }
7304
+ if (ev.aggregate !== void 0) {
7305
+ chunks.push(` aggregate: '${ev.aggregate}',`);
7306
+ }
7307
+ if (ev.source !== void 0) {
7308
+ chunks.push(` source: '${ev.source}',`);
7309
+ }
7310
+ if (ev.destination !== void 0) {
7311
+ chunks.push(` destination: '${ev.destination}',`);
7312
+ }
7313
+ chunks.push(` version: ${ev.version},`);
7314
+ chunks.push(
7315
+ ` retry: { attempts: ${ev.retry.attempts}, backoff: '${ev.retry.backoff}' },`
7316
+ );
7317
+ chunks.push(` },`);
7296
7318
  }
7297
- return out;
7319
+ chunks.push(
7320
+ `} as const satisfies Record<EventTypeName, EventMetadata>;`
7321
+ );
7322
+ chunks.push("");
7323
+ chunks.push(REGISTRY_GETTER);
7324
+ chunks.push("");
7325
+ return chunks.join("\n");
7298
7326
  }
7299
- function emitValueImports(imports) {
7300
- const grouped = /* @__PURE__ */ new Map();
7301
- for (const i of imports) {
7302
- const set = grouped.get(i.specifier) ?? /* @__PURE__ */ new Set();
7303
- set.add(i.name);
7304
- grouped.set(i.specifier, set);
7305
- }
7306
- const out = [];
7307
- for (const specifier of [...grouped.keys()].sort()) {
7308
- const names2 = [...grouped.get(specifier)].sort();
7309
- out.push(`import { ${names2.join(", ")} } from '${specifier}';`);
7327
+ var BUS_BODY = `import { Injectable, Inject } from '@nestjs/common';
7328
+ import { randomUUID } from 'crypto';
7329
+ import { EVENT_BUS, EVENTS_MULTI_TENANT } from '../events.tokens';
7330
+ import { MissingTenantIdError } from '../events-errors';
7331
+ import type { IEventBus, DrizzleTransaction } from '../event-bus.protocol';
7332
+ import { eventPayloadSchemas } from './schemas';
7333
+ import { getEventMetadata } from './registry';
7334
+ import type { EventTypeName, EventOfType, PayloadOfType } from './types';
7335
+
7336
+ /**
7337
+ * Typed facade over IEventBus.
7338
+ *
7339
+ * Stamps \`pool\`, \`direction\`, \`tier\`, and \`version\` into \`event.metadata\`
7340
+ * from the generated \`eventRegistry\` before delegating to
7341
+ * \`IEventBus.publish()\`. Downstream backends (DrizzleEventBus) read those
7342
+ * values to populate the explicit \`domain_events\` columns.
7343
+ *
7344
+ * Tier stamping (AUDIT-3): every event carries \`metadata.tier\`, sourced
7345
+ * from the registry. For \`tier: 'audit'\` events, the bus FORCES
7346
+ * \`metadata.pool = null\` and \`metadata.direction = null\` regardless of
7347
+ * any caller-supplied values in \`opts.metadata\` \u2014 audit routing is
7348
+ * bus-stamped, not caller-controlled. Caller overrides are silently
7349
+ * dropped with a debug-level log (callers should not be specifying these
7350
+ * for audit events; see ai-docs/specs/issue-242/plan.md \xA7AUDIT-3).
7351
+ *
7352
+ * Validation gating (EVT-Q5): \`CODEGEN_EVENT_VALIDATE\` env flag, default on.
7353
+ * Uses \`safeParse\` + \`console.warn\` \u2014 never throws, so a bad publish does
7354
+ * not crash a hot path.
7355
+ *
7356
+ * Multi-tenancy (EVT-6): when the EventsModule is configured with
7357
+ * \`multiTenant: true\`, every publish must supply \`opts.metadata.tenantId\`
7358
+ * \u2014 otherwise \`publish()\` throws \`MissingTenantIdError\`. When \`multiTenant\`
7359
+ * is \`false\` (default), no tenantId is required. If a tenantId IS supplied,
7360
+ * it is preserved on \`event.metadata\` and the Drizzle backend writes it to
7361
+ * \`domain_events.tenant_id\` (EVT-4).
7362
+ */
7363
+ @Injectable()
7364
+ export class TypedEventBus {
7365
+ constructor(
7366
+ @Inject(EVENT_BUS) private readonly bus: IEventBus,
7367
+ @Inject(EVENTS_MULTI_TENANT) private readonly multiTenant: boolean,
7368
+ ) {}
7369
+
7370
+ async publish<T extends EventTypeName>(
7371
+ type: T,
7372
+ aggregateId: string,
7373
+ payload: PayloadOfType<T>,
7374
+ opts?: { tx?: DrizzleTransaction; metadata?: Record<string, unknown> },
7375
+ ): Promise<void> {
7376
+ const meta = getEventMetadata(type);
7377
+
7378
+ const flag = process.env['CODEGEN_EVENT_VALIDATE'];
7379
+ const shouldValidate =
7380
+ flag === undefined ? true : flag !== 'false' && flag !== '0';
7381
+ if (shouldValidate) {
7382
+ // \`eventPayloadSchemas\` is typed as \`Record<EventTypeName, z.ZodType>\`,
7383
+ // so under \`noUncheckedIndexedAccess\` the indexed lookup widens
7384
+ // to \`z.ZodType | undefined\`. When no events are registered at
7385
+ // codegen time \`EventTypeName\` degrades to \`string\` and the
7386
+ // schemas object is literally \`{}\` \u2014 the guard below is the
7387
+ // honest handling of that empty-registry case (skip validation;
7388
+ // it's a warn-only best-effort check per the class docblock).
7389
+ const schema = eventPayloadSchemas[type];
7390
+ if (schema) {
7391
+ const check = schema.safeParse(payload);
7392
+ if (!check.success) {
7393
+ console.warn(
7394
+ \`[TypedEventBus] payload validation failed for \${String(type)}:\`,
7395
+ check.error.issues,
7396
+ );
7397
+ }
7398
+ }
7399
+ }
7400
+
7401
+ const tenantId = opts?.metadata?.['tenantId'];
7402
+ if (this.multiTenant && (tenantId === undefined || tenantId === null)) {
7403
+ throw new MissingTenantIdError(type as string);
7404
+ }
7405
+
7406
+ const aggregateType =
7407
+ meta.aggregate ?? meta.source ?? meta.destination ?? (type as string);
7408
+
7409
+ // AUDIT-3: build metadata with tier-aware routing stamping. For
7410
+ // \`tier: 'audit'\` events the bus FORCES pool/direction to null,
7411
+ // even if the caller supplied them in opts.metadata. Audit routing
7412
+ // is bus-stamped, not caller-controlled (see plan \xA7AUDIT-3).
7413
+ const baseMetadata: Record<string, unknown> = { ...(opts?.metadata ?? {}) };
7414
+ if (meta.tier === 'audit') {
7415
+ if (
7416
+ baseMetadata['pool'] !== undefined ||
7417
+ baseMetadata['direction'] !== undefined
7418
+ ) {
7419
+ console.debug(
7420
+ \`[TypedEventBus] tier:audit event '\${String(type)}' had pool/direction in opts.metadata; overriding to null.\`,
7421
+ );
7422
+ }
7423
+ baseMetadata['pool'] = null;
7424
+ baseMetadata['direction'] = null;
7425
+ baseMetadata['tier'] = 'audit';
7426
+ } else {
7427
+ baseMetadata['pool'] = meta.pool;
7428
+ baseMetadata['direction'] = meta.direction;
7429
+ baseMetadata['tier'] = 'domain';
7430
+ }
7431
+ baseMetadata['version'] = meta.version;
7432
+
7433
+ await this.bus.publish(
7434
+ {
7435
+ id: randomUUID(),
7436
+ type,
7437
+ aggregateId,
7438
+ aggregateType,
7439
+ payload: payload as Record<string, unknown>,
7440
+ occurredAt: new Date(),
7441
+ metadata: baseMetadata,
7442
+ },
7443
+ opts?.tx,
7444
+ );
7445
+ }
7446
+
7447
+ subscribe<T extends EventTypeName>(
7448
+ type: T,
7449
+ handler: (event: EventOfType<T>) => Promise<void>,
7450
+ ): () => void {
7451
+ return this.bus.subscribe<EventOfType<T>>(type, handler as never);
7452
+ }
7453
+ }
7454
+ `;
7455
+ function buildBusContent(_events, mode = "vendored") {
7456
+ let body = BUS_BODY;
7457
+ if (mode === "package") {
7458
+ body = body.replace(`from '../events.tokens'`, `from '${PACKAGE_EVENTS_RUNTIME_IMPORT}'`).replace(`from '../events-errors'`, `from '${PACKAGE_EVENTS_RUNTIME_IMPORT}'`).replace(`from '../event-bus.protocol'`, `from '${PACKAGE_EVENTS_RUNTIME_IMPORT}'`);
7310
7459
  }
7311
- return out;
7460
+ return HEADER4 + "\n" + body;
7312
7461
  }
7313
- function allRegistries(pattern) {
7314
- const list = [];
7315
- list.push({
7316
- reg: pattern.registry,
7317
- names: registryNames(pattern, pattern.registry, true),
7318
- isPrimary: true
7462
+ function buildIndexContent(_events) {
7463
+ return HEADER4 + `
7464
+ export * from './types';
7465
+ export * from './schemas';
7466
+ export * from './registry';
7467
+ export * from './bus';
7468
+ `;
7469
+ }
7470
+ function buildEventCodegenContents(events, mode = "vendored") {
7471
+ return [
7472
+ { name: "types.ts", content: buildTypesContent(events, mode) },
7473
+ { name: "schemas.ts", content: buildSchemasContent(events) },
7474
+ { name: "registry.ts", content: buildRegistryContent(events) },
7475
+ { name: "bus.ts", content: buildBusContent(events, mode) },
7476
+ { name: "index.ts", content: buildIndexContent(events) }
7477
+ ];
7478
+ }
7479
+ async function generateEventCodegen(opts) {
7480
+ const { entitiesDir, eventsDir, outputDir, mode = "vendored", dryRun = false } = opts;
7481
+ const { events: merged, issues } = collectMergedEvents({
7482
+ entitiesDir,
7483
+ eventsDir
7319
7484
  });
7320
- for (const sibling of pattern.coKeyedRegistries ?? []) {
7321
- list.push({
7322
- reg: sibling,
7323
- names: registryNames(pattern, sibling, false),
7324
- isPrimary: false
7325
- });
7485
+ const files = buildEventCodegenContents(merged, mode).map(
7486
+ ({ name, content }) => ({
7487
+ name,
7488
+ outputPath: path9.join(outputDir, name),
7489
+ content
7490
+ })
7491
+ );
7492
+ const hasError = issues.some((i) => i.severity === "error");
7493
+ let written = false;
7494
+ if (!dryRun && !hasError) {
7495
+ fs6.mkdirSync(outputDir, { recursive: true });
7496
+ for (const file of files) {
7497
+ fs6.writeFileSync(file.outputPath, file.content);
7498
+ }
7499
+ written = true;
7326
7500
  }
7327
- return list;
7501
+ return {
7502
+ outputDir,
7503
+ eventCount: merged.length,
7504
+ events: merged,
7505
+ issues,
7506
+ written,
7507
+ files
7508
+ };
7328
7509
  }
7329
- function buildTokensTs(pattern) {
7330
- const registries = allRegistries(pattern);
7331
- const typeImports = [];
7332
- for (const { reg } of registries) {
7333
- typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
7334
- typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
7335
- }
7336
- const lines = [];
7337
- lines.push(HEADER6.trimEnd());
7338
- lines.push("");
7339
- for (const i of emitTypeImports(typeImports)) lines.push(i);
7340
- lines.push("");
7341
- for (const { names: names2 } of registries) {
7342
- lines.push(`export const ${names2.tokenConst} = Symbol('${names2.tokenConst}');`);
7343
- }
7344
- lines.push("");
7345
- for (const { reg, names: names2 } of registries) {
7346
- lines.push(
7347
- `export type ${names2.mapType} = Map<${reg.keyType}, ${reg.valueType}>;`
7510
+
7511
+ // src/cli/shared/subsystem-barrel-generator.ts
7512
+ function quoteOpts(opts) {
7513
+ const entries = Object.entries(opts).filter(([, v]) => v !== void 0);
7514
+ if (entries.length === 0) return "";
7515
+ return "{ " + entries.map(([k, v]) => `${k}: ${typeof v === "string" ? `'${v}'` : String(v)}`).join(", ") + " }";
7516
+ }
7517
+ function jsonToTs(value) {
7518
+ if (value === null || value === void 0) return "undefined";
7519
+ if (typeof value === "string") return `'${value.replace(/'/g, "\\'")}'`;
7520
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
7521
+ if (Array.isArray(value)) return `[${value.map(jsonToTs).join(", ")}]`;
7522
+ if (typeof value === "object") {
7523
+ const entries = Object.entries(value).filter(
7524
+ ([, v]) => v !== void 0
7348
7525
  );
7526
+ return `{ ${entries.map(([k, v]) => `${k}: ${jsonToTs(v)}`).join(", ")} }`;
7349
7527
  }
7350
- lines.push("");
7351
- return lines.join("\n");
7528
+ return "undefined";
7352
7529
  }
7353
- function buildProvidersTs(pattern) {
7354
- const registries = allRegistries(pattern);
7355
- const providerImports = [];
7356
- for (const { reg } of registries) {
7357
- for (const e of reg.entries) {
7358
- providerImports.push({ specifier: e.providerImport, name: e.provider });
7359
- }
7530
+ function quoteBullmqDomainOpts(input) {
7531
+ const { backend, multiTenant, bullExt } = input;
7532
+ if (backend !== "bullmq" || !bullExt) {
7533
+ return quoteOpts({ backend, multiTenant });
7360
7534
  }
7361
- const tokenValueImports = registries.map(({ names: names2 }) => names2.tokenConst);
7362
- const mapTypeImports = registries.map(({ names: names2 }) => names2.mapType);
7363
- const typeImports = [];
7364
- for (const { reg } of registries) {
7365
- typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
7366
- typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
7535
+ const parts = [`backend: 'bullmq'`];
7536
+ if (multiTenant) parts.push(`multiTenant: true`);
7537
+ parts.push(`extensions: { bullmq: ${jsonToTs(bullExt)} }`);
7538
+ return `{ ${parts.join(", ")} }`;
7539
+ }
7540
+ function workerPoolsClause(cfg, bridgeInstalled) {
7541
+ const explicit = cfg?.worker_pools;
7542
+ if (Array.isArray(explicit) && explicit.length > 0) {
7543
+ const list = explicit.filter((p) => typeof p === "string").map((p) => `'${p}'`).join(", ");
7544
+ if (list.length > 0) return `pools: [${list}]`;
7367
7545
  }
7368
- const optsType = forRootOptionsTypeName(pattern);
7369
- const lines = [];
7370
- lines.push(HEADER6.trimEnd());
7371
- lines.push("");
7372
- lines.push(`import type { Provider } from '@nestjs/common';`);
7373
- for (const i of emitTypeImports(typeImports)) lines.push(i);
7374
- for (const i of emitValueImports(providerImports)) lines.push(i);
7375
- const tokenLine = [
7376
- ...tokenValueImports.sort(),
7377
- ...mapTypeImports.sort().map((m) => `type ${m}`)
7378
- ].join(", ");
7379
- lines.push(`import { ${tokenLine} } from './tokens.js';`);
7380
- lines.push(`import type { ${optsType} } from './module.js';`);
7381
- lines.push("");
7382
- const { pascal } = patternNames(pattern);
7383
- lines.push(
7384
- `export function build${pascal}RegistryProviders(opts?: ${optsType}): Provider[] {`
7385
- );
7386
- lines.push(" return [");
7387
- for (const { reg, names: names2 } of registries) {
7388
- const factoryArgs = reg.entries.map((e, i) => `${camelArg(e.provider, i)}: ${e.provider}`).join(", ");
7389
- lines.push(" {");
7390
- lines.push(` provide: ${names2.tokenConst},`);
7391
- lines.push(` useFactory: (${factoryArgs}) => {`);
7392
- lines.push(` const base: ${names2.mapType} = new Map<${reg.keyType}, ${reg.valueType}>([`);
7393
- for (const [i, e] of reg.entries.entries()) {
7394
- lines.push(
7395
- ` ['${e.key}' as ${reg.keyType}, ${camelArg(e.provider, i)}],`
7546
+ if (cfg?.all_pools === true) return "allPools: true";
7547
+ if (bridgeInstalled) return "allPools: true";
7548
+ return "";
7549
+ }
7550
+ var COMPOSERS = {
7551
+ events: ({ moduleImport, cfg, mode }) => {
7552
+ const backend = cfg?.backend ?? "drizzle";
7553
+ const multiTenant = Boolean(cfg?.multi_tenant);
7554
+ const imports = [
7555
+ `import { EventsModule } from '${moduleImport("events", "events.module")}';`
7556
+ ];
7557
+ if (mode === "package") {
7558
+ imports.push(`import { TypedEventBus } from './events/bus';`);
7559
+ return {
7560
+ imports,
7561
+ calls: [
7562
+ ` EventsModule.forRoot({ backend: '${backend}', multiTenant: ${multiTenant}, typedBus: TypedEventBus }),`
7563
+ ]
7564
+ };
7565
+ }
7566
+ return {
7567
+ imports,
7568
+ calls: [
7569
+ ` EventsModule.forRoot(${quoteOpts({ backend, multiTenant })}),`
7570
+ ]
7571
+ };
7572
+ },
7573
+ jobs: ({ moduleImport, cfg, bridgeInstalled }) => {
7574
+ const backend = cfg?.backend ?? "drizzle";
7575
+ const multiTenant = Boolean(cfg?.multi_tenant);
7576
+ const workerMode = (cfg?.worker_mode ?? "standalone").trim();
7577
+ const imports = [
7578
+ `import { JobsDomainModule } from '${moduleImport("jobs", "jobs-domain.module")}';`
7579
+ ];
7580
+ const bullExt = backend === "bullmq" ? cfg?.extensions?.bullmq : void 0;
7581
+ const domainOpts = quoteBullmqDomainOpts({ backend, multiTenant, bullExt });
7582
+ const calls = [` JobsDomainModule.forRoot(${domainOpts}),`];
7583
+ if (workerMode === "embedded") {
7584
+ imports.push(
7585
+ `import { JobWorkerModule } from '${moduleImport("jobs", "job-worker.module")}';`
7396
7586
  );
7587
+ const parts = [`mode: 'embedded'`];
7588
+ if (backend === "bullmq") {
7589
+ parts.push(`backend: 'bullmq'`);
7590
+ if (bullExt) {
7591
+ parts.push(`domainModuleExtensions: { bullmq: ${jsonToTs(bullExt)} }`);
7592
+ }
7593
+ }
7594
+ const poolsClause = workerPoolsClause(cfg, bridgeInstalled);
7595
+ if (poolsClause) parts.push(poolsClause);
7596
+ calls.push(` JobWorkerModule.forRoot({ ${parts.join(", ")} }),`);
7397
7597
  }
7398
- lines.push(" ]);");
7399
- lines.push(` if (opts?.${names2.overrideField}) {`);
7400
- lines.push(` for (const [k, v] of Object.entries(opts.${names2.overrideField})) {`);
7401
- lines.push(` if (v !== undefined) base.set(k as ${reg.keyType}, v as ${reg.valueType});`);
7402
- lines.push(" }");
7403
- lines.push(" }");
7404
- lines.push(` return base as ${names2.mapType};`);
7405
- lines.push(" },");
7406
- const injectList = reg.entries.map((e) => e.provider).join(", ");
7407
- lines.push(` inject: [${injectList}],`);
7408
- lines.push(" },");
7409
- }
7410
- lines.push(" ];");
7411
- lines.push("}");
7412
- lines.push("");
7413
- return lines.join("\n");
7414
- }
7415
- function camelArg(provider, index2) {
7416
- const camel = provider.charAt(0).toLowerCase() + provider.slice(1);
7417
- return `${camel}_${index2}`;
7418
- }
7419
- function buildDispatcherTs(pattern) {
7420
- const registries = allRegistries(pattern);
7421
- const className = dispatcherClassName(pattern);
7422
- const errorClass = `${className}Error`;
7423
- const typeImports = [];
7424
- for (const { reg } of registries) {
7425
- typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
7426
- typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
7427
- }
7428
- const providerImports = [];
7429
- for (const { reg } of registries) {
7430
- for (const e of reg.entries) {
7431
- providerImports.push({ specifier: e.providerImport, name: e.provider });
7598
+ return { imports, calls };
7599
+ },
7600
+ bridge: ({ moduleImport, cfg, mode }) => {
7601
+ const backend = cfg?.backend ?? "drizzle";
7602
+ const multiTenant = Boolean(cfg?.multi_tenant);
7603
+ const imports = [
7604
+ `import { BridgeModule } from '${moduleImport("bridge", "bridge.module")}';`
7605
+ ];
7606
+ if (mode === "package") {
7607
+ imports.push(`import { bridgeRegistry } from './bridge-registry';`);
7608
+ return {
7609
+ imports,
7610
+ calls: [
7611
+ ` BridgeModule.forRoot({ backend: '${backend}', multiTenant: ${multiTenant}, registry: bridgeRegistry }),`
7612
+ ]
7613
+ };
7432
7614
  }
7615
+ return {
7616
+ imports,
7617
+ calls: [
7618
+ ` BridgeModule.forRoot(${quoteOpts({ backend, multiTenant })}),`
7619
+ ]
7620
+ };
7621
+ },
7622
+ integration: ({ moduleImport, cfg }) => {
7623
+ const backend = cfg?.backend ?? "drizzle";
7624
+ const multiTenant = Boolean(cfg?.multi_tenant);
7625
+ return {
7626
+ imports: [
7627
+ `import { IntegrationModule } from '${moduleImport("integration", "integration.module")}';`
7628
+ ],
7629
+ calls: [
7630
+ ` IntegrationModule.forRoot(${quoteOpts({ backend, multiTenant })}),`
7631
+ ]
7632
+ };
7433
7633
  }
7434
- const tokenValues = registries.map(({ names: names2 }) => names2.tokenConst);
7435
- const mapTypes = registries.map(({ names: names2 }) => names2.mapType);
7436
- const lines = [];
7437
- lines.push(HEADER6.trimEnd());
7438
- lines.push("");
7439
- lines.push(`import { Inject, Injectable } from '@nestjs/common';`);
7440
- for (const i of emitTypeImports(typeImports)) lines.push(i);
7441
- for (const i of emitValueImports(providerImports)) lines.push(i);
7442
- const tokenLine = [
7443
- ...tokenValues.sort(),
7444
- ...mapTypes.sort().map((m) => `type ${m}`)
7445
- ].join(", ");
7446
- lines.push(`import { ${tokenLine} } from './tokens.js';`);
7447
- lines.push("");
7448
- lines.push(`export class ${errorClass} extends Error {}`);
7449
- lines.push("");
7450
- lines.push("@Injectable()");
7451
- lines.push(`export class ${className} {`);
7452
- lines.push(" constructor(");
7453
- for (const { names: names2 } of registries) {
7454
- const fieldName = names2.method === "select" ? "registry" : `${camelFromMethod(names2.method)}Registry`;
7455
- lines.push(
7456
- ` @Inject(${names2.tokenConst}) protected readonly ${fieldName}: ${names2.mapType},`
7457
- );
7634
+ };
7635
+ var PACKAGE2 = "@pattern-stack/codegen";
7636
+ function makeModuleImport(mode, subsystemsRel) {
7637
+ if (mode === "vendored") {
7638
+ return (subsystem, moduleBasename) => `${subsystemsRel}/${subsystem}/${moduleBasename}`;
7458
7639
  }
7459
- lines.push(" ) {}");
7460
- lines.push("");
7461
- for (const { reg, names: names2 } of registries) {
7462
- const fieldName = names2.method === "select" ? "registry" : `${camelFromMethod(names2.method)}Registry`;
7463
- for (const e of reg.entries) {
7464
- lines.push(` ${names2.method}(key: '${e.key}'): ${e.provider};`);
7640
+ return (subsystem) => subsystem === "events" ? `${PACKAGE2}/subsystems` : `${PACKAGE2}/runtime/subsystems/${subsystem}/index`;
7641
+ }
7642
+ var COMPOSABLE_ORDER = ["events", "jobs", "bridge", "integration"];
7643
+ var HEADER5 = `// AUTO-GENERATED by @pattern-stack/codegen. DO NOT EDIT.
7644
+ // Subsystem composition barrel \u2014 reflects \`subsystems.install\` in
7645
+ // codegen.config.yaml and the per-subsystem option blocks
7646
+ // (\`events:\`, \`jobs:\`, \`bridge:\`, \`integration:\`).
7647
+ //
7648
+ // Wire into AppModule once:
7649
+ //
7650
+ // import { SUBSYSTEM_MODULES } from './generated/subsystems';
7651
+ // @Module({ imports: [DatabaseModule, ...SUBSYSTEM_MODULES, ...GENERATED_MODULES] })
7652
+ //
7653
+ // Regenerated by every \`codegen entity new\` / \`codegen subsystem install\`.
7654
+
7655
+ `;
7656
+ function buildSubsystemBarrel(installed, config, subsystemsRel, mode = "vendored") {
7657
+ const moduleImport = makeModuleImport(mode, subsystemsRel);
7658
+ const actable = installed.filter((i) => i.status !== "incomplete");
7659
+ const installedNames = new Set(actable.map((i) => i.name));
7660
+ const bridgeInstalled = installedNames.has("bridge");
7661
+ const emitted = [];
7662
+ const skipped = [];
7663
+ const allImports = [`import type { DynamicModule } from '@nestjs/common';`];
7664
+ const allCalls = [];
7665
+ for (const name of COMPOSABLE_ORDER) {
7666
+ if (!installedNames.has(name)) continue;
7667
+ const composer = COMPOSERS[name];
7668
+ if (!composer) {
7669
+ skipped.push(name);
7670
+ continue;
7465
7671
  }
7466
- lines.push(` ${names2.method}(key: ${reg.keyType}): ${reg.valueType};`);
7467
- lines.push(` ${names2.method}(key: ${reg.keyType}): ${reg.valueType} {`);
7468
- lines.push(` const entry = this.${fieldName}.get(key);`);
7469
- lines.push(
7470
- ` if (!entry) throw new ${errorClass}(\`Unknown ${reg.keyType}: \${String(key)}\`);`
7471
- );
7472
- lines.push(` return entry;`);
7473
- lines.push(" }");
7474
- lines.push("");
7475
- }
7476
- if (pattern.dispatcher?.assemblySlot) {
7477
- lines.push(
7478
- ` // ADR-032 Decision 5 \u2014 \`assemblySlot: '${pattern.dispatcher.assemblySlot}'\` is a`
7479
- );
7480
- lines.push(
7481
- ` // subclass contract, not emitted here. Consumers subclass and add the`
7482
- );
7483
- lines.push(
7484
- ` // \`${pattern.dispatcher.assemblySlot}(...)\` method themselves.`
7485
- );
7672
+ const cfg = config?.[name] ?? void 0;
7673
+ const out = composer({ moduleImport, cfg, mode, bridgeInstalled });
7674
+ allImports.push(...out.imports);
7675
+ allCalls.push(...out.calls);
7676
+ emitted.push(name);
7486
7677
  }
7487
- lines.push("}");
7488
- lines.push("");
7489
- return lines.join("\n");
7490
- }
7491
- function camelFromMethod(method) {
7492
- if (method.startsWith("select") && method.length > "select".length) {
7493
- const tail = method.slice("select".length);
7494
- return tail.charAt(0).toLowerCase() + tail.slice(1);
7678
+ for (const inst of actable) {
7679
+ if (!COMPOSABLE_ORDER.includes(inst.name) && !COMPOSERS[inst.name]) {
7680
+ skipped.push(inst.name);
7681
+ }
7495
7682
  }
7496
- return method;
7683
+ const exportLine = allCalls.length === 0 ? `export const SUBSYSTEM_MODULES: DynamicModule[] = [];
7684
+ ` : `export const SUBSYSTEM_MODULES: DynamicModule[] = [
7685
+ ${allCalls.join("\n")}
7686
+ ];
7687
+ `;
7688
+ const body = allImports.join("\n") + "\n\n" + exportLine;
7689
+ return { content: HEADER5 + body, emitted, skipped };
7497
7690
  }
7498
- function buildModuleTs(pattern) {
7499
- const registries = allRegistries(pattern);
7500
- const className = dispatcherClassName(pattern);
7501
- const moduleName = moduleClassName(pattern);
7502
- const optsType = forRootOptionsTypeName(pattern);
7503
- const { pascal } = patternNames(pattern);
7504
- const typeImports = [];
7505
- for (const { reg } of registries) {
7506
- typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
7507
- typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
7508
- }
7509
- const tokenValues = registries.map(({ names: names2 }) => names2.tokenConst);
7510
- const lines = [];
7511
- lines.push(HEADER6.trimEnd());
7512
- lines.push("");
7513
- lines.push(`import { type DynamicModule, Module } from '@nestjs/common';`);
7514
- for (const i of emitTypeImports(typeImports)) lines.push(i);
7515
- lines.push(`import { ${className} } from './dispatcher.js';`);
7516
- lines.push(`import { build${pascal}RegistryProviders } from './registry.providers.js';`);
7517
- lines.push(`import { ${tokenValues.sort().join(", ")} } from './tokens.js';`);
7518
- lines.push("");
7519
- lines.push(`export interface ${optsType} {`);
7520
- for (const { reg, names: names2 } of registries) {
7521
- lines.push(
7522
- ` ${names2.overrideField}?: Partial<Record<${reg.keyType}, ${reg.valueType}>>;`
7523
- );
7524
- }
7525
- lines.push("}");
7526
- lines.push("");
7527
- lines.push("@Module({})");
7528
- lines.push(`export class ${moduleName} {`);
7529
- lines.push(` static forRoot(opts?: ${optsType}): DynamicModule {`);
7530
- lines.push(" return {");
7531
- lines.push(` module: ${moduleName},`);
7532
- lines.push(
7533
- ` providers: [...build${pascal}RegistryProviders(opts), ${className}],`
7691
+ async function regenerateSubsystemBarrel(opts) {
7692
+ const { ctx, dryRun = false } = opts;
7693
+ const generatedDir = opts.generatedDir ?? resolveGeneratedDir(ctx);
7694
+ const mode = resolveRuntimeMode(ctx.config);
7695
+ const installed = mode === "package" ? configuredInstalledSubsystems(
7696
+ ctx.config
7697
+ ) : await detectInstalledSubsystems(ctx);
7698
+ const subsystemsAbs = resolveSubsystemsRoot(ctx);
7699
+ const barrelAbs = path10.resolve(generatedDir, "subsystems.ts");
7700
+ let subsystemsRel = path10.relative(path10.dirname(barrelAbs), subsystemsAbs).split(path10.sep).join("/");
7701
+ if (!subsystemsRel.startsWith(".")) subsystemsRel = "./" + subsystemsRel;
7702
+ const { content, emitted, skipped } = buildSubsystemBarrel(
7703
+ installed,
7704
+ ctx.config,
7705
+ subsystemsRel,
7706
+ mode
7534
7707
  );
7535
- const exportItems = [...tokenValues.sort(), className];
7536
- lines.push(` exports: [${exportItems.join(", ")}],`);
7537
- lines.push(" };");
7538
- lines.push(" }");
7539
- lines.push("}");
7540
- lines.push("");
7541
- return lines.join("\n");
7708
+ let written = false;
7709
+ if (!dryRun) {
7710
+ fs7.mkdirSync(path10.dirname(barrelAbs), { recursive: true });
7711
+ fs7.writeFileSync(barrelAbs, content);
7712
+ written = true;
7713
+ if (mode === "package" && emitted.includes("bridge")) {
7714
+ const registryPath = path10.resolve(generatedDir, "bridge-registry.ts");
7715
+ if (!fs7.existsSync(registryPath)) {
7716
+ fs7.writeFileSync(
7717
+ registryPath,
7718
+ buildBridgeRegistryContent([], PACKAGE_BRIDGE_TYPE_IMPORT)
7719
+ );
7720
+ }
7721
+ }
7722
+ if (mode === "package" && emitted.includes("events")) {
7723
+ const eventsDir = path10.resolve(generatedDir, "events");
7724
+ if (!fs7.existsSync(path10.resolve(eventsDir, "bus.ts"))) {
7725
+ fs7.mkdirSync(eventsDir, { recursive: true });
7726
+ for (const { name, content: content2 } of buildEventCodegenContents([], "package")) {
7727
+ fs7.writeFileSync(path10.resolve(eventsDir, name), content2);
7728
+ }
7729
+ }
7730
+ }
7731
+ }
7732
+ return {
7733
+ subsystemBarrel: barrelAbs,
7734
+ emitted,
7735
+ skipped,
7736
+ content,
7737
+ written
7738
+ };
7542
7739
  }
7543
- function buildIndexTs(pattern) {
7544
- const lines = [];
7545
- lines.push(HEADER6.trimEnd());
7546
- lines.push("");
7547
- lines.push(`export * from './tokens.js';`);
7548
- lines.push(`export * from './dispatcher.js';`);
7549
- lines.push(`export * from './module.js';`);
7550
- lines.push(`export * from './registry.providers.js';`);
7551
- lines.push("");
7552
- return lines.join("\n");
7740
+
7741
+ // src/cli/shared/subsystem-schema-generator.ts
7742
+ import fs8 from "fs";
7743
+ import path11 from "path";
7744
+ var PACKAGE3 = "@pattern-stack/codegen";
7745
+ var SCHEMA_SYMBOLS = {
7746
+ events: ["domainEvents"],
7747
+ jobs: [
7748
+ "jobs",
7749
+ "jobRuns",
7750
+ "jobSteps",
7751
+ "jobRunStatusEnum",
7752
+ "jobStepKindEnum",
7753
+ "jobStepStatusEnum",
7754
+ "collisionModeEnum",
7755
+ "replayFromEnum",
7756
+ "parentClosePolicyEnum",
7757
+ "waitKindEnum",
7758
+ "triggerSourceEnum"
7759
+ ],
7760
+ bridge: ["bridgeDelivery", "bridgeDeliveryStatusEnum"],
7761
+ integration: [
7762
+ "integrationSubscriptions",
7763
+ "integrationRuns",
7764
+ "integrationRunItems",
7765
+ "integrationRunDirectionEnum",
7766
+ "integrationRunActionEnum",
7767
+ "integrationRunStatusEnum",
7768
+ "integrationRunItemOperationEnum",
7769
+ "integrationRunItemStatusEnum"
7770
+ ]
7771
+ };
7772
+ var SCHEMA_ORDER = ["events", "jobs", "bridge", "integration"];
7773
+ var HEADER6 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
7774
+ // Subsystem Drizzle schema barrel \u2014 re-exports the tables + pgEnums of every
7775
+ // installed subsystem so drizzle-kit emits their CREATE TABLE / CREATE TYPE.
7776
+ //
7777
+ // Fold into your drizzle-kit schema entrypoint once, alongside the entity
7778
+ // schema barrel:
7779
+ //
7780
+ // // src/schema.ts
7781
+ // export * from './generated/schema'; // entity tables
7782
+ // export * from './generated/subsystems-schema'; // subsystem tables + enums
7783
+ //
7784
+ // Regenerated by every \`codegen entity new\` / \`codegen subsystem install\`.
7785
+
7786
+ `;
7787
+ function schemaImportFor(mode, subsystem, subsystemsRel) {
7788
+ return mode === "vendored" ? `${subsystemsRel}/${subsystem}` : `${PACKAGE3}/runtime/subsystems/${subsystem}/index`;
7553
7789
  }
7554
- function buildRootBarrelTs(patterns) {
7790
+ function buildSubsystemSchemaBarrel(installed, subsystemsRel, mode) {
7791
+ const actable = installed.filter((i) => i.status !== "incomplete");
7792
+ const installedNames = new Set(actable.map((i) => i.name));
7793
+ const emitted = [];
7555
7794
  const lines = [];
7556
- lines.push(HEADER6.trimEnd());
7557
- lines.push("");
7558
- if (patterns.length === 0) {
7559
- lines.push("// No orchestration patterns registered.");
7560
- lines.push("export {};");
7561
- lines.push("");
7562
- return lines.join("\n");
7563
- }
7564
- const slugs = patterns.map((p) => toKebabCase2(p.name)).sort();
7565
- for (const slug of slugs) {
7566
- lines.push(`export * from './${slug}/index.js';`);
7795
+ for (const name of SCHEMA_ORDER) {
7796
+ if (!installedNames.has(name)) continue;
7797
+ const symbols = SCHEMA_SYMBOLS[name];
7798
+ if (!symbols || symbols.length === 0) continue;
7799
+ const importSpec = schemaImportFor(mode, name, subsystemsRel);
7800
+ lines.push(`export { ${symbols.join(", ")} } from '${importSpec}';`);
7801
+ emitted.push(name);
7567
7802
  }
7568
- lines.push("");
7569
- return lines.join("\n");
7570
- }
7571
- function buildPatternFiles(pattern, outputRoot) {
7572
- assertEmittable(pattern);
7573
- const slug = toKebabCase2(pattern.name);
7574
- const outputDir = path11.join(outputRoot, slug);
7575
- const make = (name, content) => ({
7576
- name,
7577
- outputPath: path11.join(outputDir, name),
7578
- relativePath: path11.join(slug, name),
7579
- content
7580
- });
7581
- const files = [
7582
- make("tokens.ts", buildTokensTs(pattern)),
7583
- make("registry.providers.ts", buildProvidersTs(pattern)),
7584
- make("dispatcher.ts", buildDispatcherTs(pattern)),
7585
- make("module.ts", buildModuleTs(pattern)),
7586
- make("index.ts", buildIndexTs(pattern))
7587
- ];
7588
- return {
7589
- patternName: pattern.name,
7590
- slug,
7591
- outputDir,
7592
- files
7593
- };
7803
+ const body = lines.length === 0 ? "export {};\n" : lines.join("\n") + "\n";
7804
+ return { content: HEADER6 + body, emitted };
7594
7805
  }
7595
- function generateOrchestrationModules(opts) {
7596
- const { patterns, outputRoot, dryRun = false } = opts;
7597
- const perPattern = [];
7598
- const allFiles = [];
7599
- for (const pattern of patterns) {
7600
- const result = buildPatternFiles(pattern, outputRoot);
7601
- perPattern.push(result);
7602
- allFiles.push(...result.files);
7603
- }
7604
- const rootBarrel = {
7605
- name: "index.ts",
7606
- outputPath: path11.join(outputRoot, "index.ts"),
7607
- relativePath: "index.ts",
7608
- content: buildRootBarrelTs(patterns)
7609
- };
7610
- allFiles.push(rootBarrel);
7806
+ async function regenerateSubsystemSchemaBarrel(opts) {
7807
+ const { ctx, dryRun = false } = opts;
7808
+ const generatedDir = opts.generatedDir ?? resolveGeneratedDir(ctx);
7809
+ const mode = resolveRuntimeMode(ctx.config);
7810
+ const installed = mode === "package" ? configuredInstalledSubsystems(
7811
+ ctx.config
7812
+ ) : await detectInstalledSubsystems(ctx);
7813
+ const subsystemsAbs = resolveSubsystemsRoot(ctx);
7814
+ const barrelAbs = path11.resolve(generatedDir, "subsystems-schema.ts");
7815
+ let subsystemsRel = path11.relative(path11.dirname(barrelAbs), subsystemsAbs).split(path11.sep).join("/");
7816
+ if (!subsystemsRel.startsWith(".")) subsystemsRel = "./" + subsystemsRel;
7817
+ const { content, emitted } = buildSubsystemSchemaBarrel(
7818
+ installed,
7819
+ subsystemsRel,
7820
+ mode
7821
+ );
7611
7822
  let written = false;
7612
- if (!dryRun && patterns.length > 0) {
7613
- fs8.mkdirSync(outputRoot, { recursive: true });
7614
- for (const r of perPattern) {
7615
- fs8.mkdirSync(r.outputDir, { recursive: true });
7616
- for (const f of r.files) {
7617
- fs8.writeFileSync(f.outputPath, f.content);
7618
- }
7619
- }
7620
- fs8.writeFileSync(rootBarrel.outputPath, rootBarrel.content);
7823
+ if (!dryRun) {
7824
+ fs8.mkdirSync(path11.dirname(barrelAbs), { recursive: true });
7825
+ fs8.writeFileSync(barrelAbs, content);
7621
7826
  written = true;
7622
7827
  }
7623
- return {
7624
- outputRoot,
7625
- patterns: perPattern,
7626
- files: allFiles,
7627
- written
7628
- };
7828
+ return { schemaBarrel: barrelAbs, emitted, content, written };
7629
7829
  }
7630
7830
 
7631
- // src/cli/shared/event-codegen-generator.ts
7831
+ // src/cli/shared/orchestration-generator.ts
7632
7832
  import fs9 from "fs";
7633
7833
  import path12 from "path";
7634
-
7635
- // src/parser/load-events.ts
7636
- import { basename as basename2, resolve as resolve4 } from "path";
7637
- function loadErrorToIssue2(error) {
7638
- const issues = [];
7639
- issues.push({
7640
- severity: "error",
7641
- type: "parse_error",
7642
- message: error.error,
7643
- path: error.filePath
7644
- });
7645
- if (error.details) {
7646
- for (const detail of error.details) {
7647
- issues.push({
7648
- severity: "error",
7649
- type: "schema_error",
7650
- message: detail,
7651
- path: error.filePath
7652
- });
7653
- }
7834
+ var OrchestrationEmissionError = class extends Error {
7835
+ constructor(issueType, patternName, message) {
7836
+ super(`[${issueType}] ${patternName}: ${message}`);
7837
+ this.issueType = issueType;
7838
+ this.patternName = patternName;
7654
7839
  }
7655
- return issues;
7840
+ issueType;
7841
+ patternName;
7842
+ name = "OrchestrationEmissionError";
7843
+ };
7844
+ var HEADER7 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
7845
+ // See ADR-032 \u2014 Orchestration Patterns.
7846
+ `;
7847
+ function splitWords(str) {
7848
+ return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/).filter((w) => w.length > 0).map((w) => w.toLowerCase());
7656
7849
  }
7657
- function stripYamlExt(file) {
7658
- const base = basename2(file);
7659
- if (base.endsWith(".yaml")) return base.slice(0, -".yaml".length);
7660
- if (base.endsWith(".yml")) return base.slice(0, -".yml".length);
7661
- return base;
7850
+ function toKebabCase2(str) {
7851
+ return splitWords(str).join("-");
7662
7852
  }
7663
- function loadEvents(eventsDir, entityNames) {
7664
- const events = [];
7665
- const issues = [];
7666
- const resolvedDir = resolve4(eventsDir);
7667
- let files;
7668
- try {
7669
- files = findYamlFiles(resolvedDir);
7670
- } catch {
7671
- issues.push({
7672
- severity: "warning",
7673
- type: "no_events_dir",
7674
- message: `No events directory found at: ${resolvedDir}`,
7675
- path: resolvedDir
7676
- });
7677
- return { events, issues };
7678
- }
7679
- if (files.length === 0) {
7680
- issues.push({
7681
- severity: "warning",
7682
- type: "no_files",
7683
- message: `No event YAML files found in directory: ${resolvedDir}`,
7684
- path: resolvedDir
7685
- });
7686
- return { events, issues };
7687
- }
7688
- const entityNameSet = new Set(entityNames);
7689
- const seenTypes = /* @__PURE__ */ new Map();
7690
- for (const filePath of files) {
7691
- const result = loadEventFromYaml(filePath);
7692
- if (!result.success) {
7693
- issues.push(...loadErrorToIssue2(result));
7694
- continue;
7695
- }
7696
- const { definition } = result;
7697
- const baseName = stripYamlExt(filePath);
7698
- if (baseName !== definition.type) {
7699
- issues.push({
7700
- severity: "error",
7701
- type: "event_filename_mismatch",
7702
- message: `Event file '${baseName}' must contain 'type: ${baseName}' (found 'type: ${definition.type}')`,
7703
- path: filePath,
7704
- suggestion: `Rename the file to '${definition.type}.yaml' or fix the 'type' field to '${baseName}'`
7705
- });
7706
- continue;
7707
- }
7708
- if (definition.direction === "change" && definition.aggregate !== void 0 && !entityNameSet.has(definition.aggregate)) {
7709
- issues.push({
7710
- severity: "error",
7711
- type: "unknown_aggregate",
7712
- message: `change event '${definition.type}' references unknown aggregate entity '${definition.aggregate}'`,
7713
- path: filePath,
7714
- suggestion: `Define entities/${definition.aggregate}.yaml or fix the aggregate value`
7715
- });
7716
- continue;
7717
- }
7718
- if (seenTypes.has(definition.type)) {
7719
- issues.push({
7720
- severity: "error",
7721
- type: "duplicate_event_type",
7722
- message: `Duplicate event type '${definition.type}' (already declared in ${seenTypes.get(definition.type)})`,
7723
- path: filePath
7724
- });
7725
- continue;
7853
+ function toPascalCase3(str) {
7854
+ return splitWords(str).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7855
+ }
7856
+ function toCamelCase2(str) {
7857
+ const words = splitWords(str);
7858
+ if (words.length === 0) return "";
7859
+ const [first, ...rest] = words;
7860
+ return (first ?? "") + rest.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7861
+ }
7862
+ function toScreamingSnake(str) {
7863
+ return splitWords(str).join("_").toUpperCase();
7864
+ }
7865
+ var FORBIDDEN_KEY_TYPES = /* @__PURE__ */ new Set([
7866
+ "string",
7867
+ "number",
7868
+ "symbol",
7869
+ "any",
7870
+ "unknown"
7871
+ ]);
7872
+ function assertEmittable(pattern) {
7873
+ const allRegistries2 = [
7874
+ pattern.registry,
7875
+ ...pattern.coKeyedRegistries ?? []
7876
+ ];
7877
+ for (const sibling of pattern.coKeyedRegistries ?? []) {
7878
+ if (typeof sibling.name !== "string" || sibling.name.length === 0) {
7879
+ throw new OrchestrationEmissionError(
7880
+ "pattern_cokeyed_missing_name",
7881
+ pattern.name,
7882
+ `Co-keyed registry (valueType '${sibling.valueType}') is missing the required 'name' field. Each co-keyed sibling must declare an explicit name; the emitter uppercases it for the token suffix and PascalCases it for the dispatcher method (e.g. name: 'auth' \u21D2 '\${PATTERN_CONST}_AUTH_REGISTRY' + 'selectAuth(...)'). ADR-032 O-1.`
7883
+ );
7726
7884
  }
7727
- seenTypes.set(definition.type, filePath);
7728
- events.push(definition);
7729
7885
  }
7730
- return { events, issues };
7731
- }
7732
- function desugarEntityEvents(entity) {
7733
- const entityName = entity.entity.name;
7734
- const entityEvents = entity.events ?? [];
7735
- return entityEvents.map((ev) => {
7736
- const payload = {};
7737
- for (const [key, typeString] of Object.entries(ev.body)) {
7738
- if (!isEventFieldType(typeString)) {
7739
- throw new Error(
7740
- `Entity '${entityName}' event '${ev.name}' field '${key}' has unknown type '${typeString}' \u2014 expected one of ${EVENT_FIELD_TYPES.join("|")}`
7886
+ for (const reg of allRegistries2) {
7887
+ if (FORBIDDEN_KEY_TYPES.has(reg.keyType)) {
7888
+ throw new OrchestrationEmissionError(
7889
+ "pattern_keytype_unresolved",
7890
+ pattern.name,
7891
+ `registry keyType '${reg.keyType}' is a primitive \u2014 Partial<Record<K, V>> requires a literal-union K, and per-overload narrowing requires a closed enum/union. Use a string-literal union or a TypeScript enum.`
7892
+ );
7893
+ }
7894
+ if (typeof reg.keyTypeImport !== "string" || reg.keyTypeImport.length === 0) {
7895
+ throw new OrchestrationEmissionError(
7896
+ "pattern_missing_import_path",
7897
+ pattern.name,
7898
+ `registry (keyType '${reg.keyType}') is missing 'keyTypeImport'. Phase 3-2 emission requires every keyType to carry a module specifier.`
7899
+ );
7900
+ }
7901
+ if (typeof reg.valueTypeImport !== "string" || reg.valueTypeImport.length === 0) {
7902
+ throw new OrchestrationEmissionError(
7903
+ "pattern_missing_import_path",
7904
+ pattern.name,
7905
+ `registry (valueType '${reg.valueType}') is missing 'valueTypeImport'.`
7906
+ );
7907
+ }
7908
+ for (const e of reg.entries) {
7909
+ if (typeof e.providerImport !== "string" || e.providerImport.length === 0) {
7910
+ throw new OrchestrationEmissionError(
7911
+ "pattern_missing_import_path",
7912
+ pattern.name,
7913
+ `entry '${e.key}' provider '${e.provider}' is missing 'providerImport'.`
7741
7914
  );
7742
7915
  }
7743
- payload[key] = { type: typeString, nullable: false };
7744
7916
  }
7745
- const def = {
7746
- type: ev.name,
7747
- tier: "domain",
7748
- direction: "change",
7749
- aggregate: entityName,
7750
- payload,
7751
- retry: { attempts: 3, backoff: "exponential" },
7752
- version: 1,
7753
- pool: DIRECTION_TO_POOL.change
7754
- };
7755
- return def;
7756
- });
7757
- }
7758
- function isEventFieldType(s) {
7759
- return EVENT_FIELD_TYPES.includes(s);
7760
- }
7761
-
7762
- // src/cli/shared/event-codegen-generator.ts
7763
- var HEADER7 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
7764
- // Run \`codegen entity new --all\` to refresh.
7765
- `;
7766
- function toCamelCase2(input) {
7767
- const parts = input.split("_").filter(Boolean);
7768
- if (parts.length === 0) return input;
7769
- const [first, ...rest] = parts;
7770
- return (first ?? "") + rest.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7917
+ }
7771
7918
  }
7772
- function toPascalCase3(input) {
7773
- return input.split("_").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7919
+ function patternNames(pattern) {
7920
+ const slug = toKebabCase2(pattern.name);
7921
+ const pascal = toPascalCase3(pattern.name);
7922
+ const screaming = toScreamingSnake(pattern.name);
7923
+ return { slug, pascal, screaming };
7774
7924
  }
7775
- var TS_TYPE_BY_FIELD = {
7776
- uuid: "string",
7777
- string: "string",
7778
- number: "number",
7779
- boolean: "boolean",
7780
- date: "Date",
7781
- json: "Record<string, unknown>"
7782
- };
7783
- var ZOD_EXPR_BY_FIELD = {
7784
- uuid: "z.string().uuid()",
7785
- string: "z.string()",
7786
- number: "z.number()",
7787
- boolean: "z.boolean()",
7788
- date: "z.coerce.date()",
7789
- json: "z.record(z.unknown())"
7790
- };
7791
- function tsTypeForField(field) {
7792
- let base;
7793
- if (field.type === "array") {
7794
- const itemType = field.items;
7795
- base = `${TS_TYPE_BY_FIELD[itemType]}[]`;
7796
- } else {
7797
- base = TS_TYPE_BY_FIELD[field.type];
7925
+ function registryNames(pattern, reg, isPrimary) {
7926
+ const { pascal, screaming } = patternNames(pattern);
7927
+ if (isPrimary) {
7928
+ return {
7929
+ tokenConst: `${screaming}_REGISTRY`,
7930
+ mapType: `${pascal}RegistryMap`,
7931
+ method: "select",
7932
+ overrideField: "overrides",
7933
+ builderFn: `build${pascal}RegistryProviders`
7934
+ };
7798
7935
  }
7799
- return field.nullable ? `${base} | null` : base;
7936
+ const namePascal = toPascalCase3(reg.name);
7937
+ const nameScreaming = toScreamingSnake(reg.name);
7938
+ const nameCamel = toCamelCase2(reg.name);
7939
+ return {
7940
+ tokenConst: `${screaming}_${nameScreaming}_REGISTRY`,
7941
+ mapType: `${pascal}${namePascal}RegistryMap`,
7942
+ method: `select${namePascal}`,
7943
+ overrideField: `${nameCamel}Overrides`,
7944
+ builderFn: `build${pascal}RegistryProviders`
7945
+ };
7800
7946
  }
7801
- function zodExprForField(field) {
7802
- let base;
7803
- if (field.type === "array") {
7804
- const itemType = field.items;
7805
- base = `z.array(${ZOD_EXPR_BY_FIELD[itemType]})`;
7806
- } else {
7807
- base = ZOD_EXPR_BY_FIELD[field.type];
7808
- }
7809
- return field.nullable ? `${base}.nullable()` : base;
7947
+ function dispatcherClassName(pattern) {
7948
+ return pattern.dispatcher?.className ?? `${toPascalCase3(pattern.name)}Dispatcher`;
7810
7949
  }
7811
- function aggregateTypeLiteral(ev) {
7812
- return ev.aggregate ?? ev.source ?? ev.destination ?? ev.type;
7950
+ function moduleClassName(pattern) {
7951
+ return `${toPascalCase3(pattern.name)}OrchestrationModule`;
7813
7952
  }
7814
- function collectEntityEvents(entitiesDir) {
7815
- const events = [];
7816
- const issues = [];
7817
- if (!fs9.existsSync(entitiesDir)) {
7818
- return { events, issues };
7953
+ function forRootOptionsTypeName(pattern) {
7954
+ return `${toPascalCase3(pattern.name)}ForRootOptions`;
7955
+ }
7956
+ function emitTypeImports(imports) {
7957
+ const grouped = /* @__PURE__ */ new Map();
7958
+ for (const i of imports) {
7959
+ const set = grouped.get(i.specifier) ?? /* @__PURE__ */ new Set();
7960
+ set.add(i.name);
7961
+ grouped.set(i.specifier, set);
7819
7962
  }
7820
- const files = findYamlFiles(entitiesDir);
7821
- for (const filePath of files) {
7822
- const result = loadEntityFromYaml(filePath);
7823
- if (!result.success) continue;
7824
- try {
7825
- events.push(...desugarEntityEvents(result.definition));
7826
- } catch (err) {
7827
- issues.push({
7828
- severity: "error",
7829
- type: "entity_event_desugar_failed",
7830
- message: err instanceof Error ? err.message : String(err),
7831
- path: filePath
7832
- });
7833
- }
7963
+ const out = [];
7964
+ for (const specifier of [...grouped.keys()].sort()) {
7965
+ const names2 = [...grouped.get(specifier)].sort();
7966
+ out.push(`import type { ${names2.join(", ")} } from '${specifier}';`);
7834
7967
  }
7835
- return { events, issues };
7968
+ return out;
7836
7969
  }
7837
- function mergeEvents(topLevel, entitySugar) {
7838
- const issues = [];
7839
- const byType = /* @__PURE__ */ new Map();
7840
- for (const ev of entitySugar) {
7841
- byType.set(ev.type, ev);
7970
+ function emitValueImports(imports) {
7971
+ const grouped = /* @__PURE__ */ new Map();
7972
+ for (const i of imports) {
7973
+ const set = grouped.get(i.specifier) ?? /* @__PURE__ */ new Set();
7974
+ set.add(i.name);
7975
+ grouped.set(i.specifier, set);
7842
7976
  }
7843
- for (const ev of topLevel) {
7844
- if (byType.has(ev.type)) {
7845
- issues.push({
7846
- severity: "warning",
7847
- type: "event_merge_override",
7848
- message: `event '${ev.type}' is declared both in an entity \`events:\` block and a top-level \`events/${ev.type}.yaml\` \u2014 top-level definition wins`
7849
- });
7850
- }
7851
- byType.set(ev.type, ev);
7977
+ const out = [];
7978
+ for (const specifier of [...grouped.keys()].sort()) {
7979
+ const names2 = [...grouped.get(specifier)].sort();
7980
+ out.push(`import { ${names2.join(", ")} } from '${specifier}';`);
7852
7981
  }
7853
- const events = Array.from(byType.values()).sort(
7854
- (a, b) => a.type.localeCompare(b.type)
7855
- );
7856
- return { events, issues };
7982
+ return out;
7857
7983
  }
7858
- function collectMergedEvents(opts) {
7859
- const { entitiesDir, eventsDir } = opts;
7860
- const entityNames = [];
7861
- if (fs9.existsSync(entitiesDir)) {
7862
- const entityFiles = findYamlFiles(entitiesDir);
7863
- for (const f of entityFiles) {
7864
- const result = loadEntityFromYaml(f);
7865
- if (result.success) entityNames.push(result.definition.entity.name);
7866
- }
7984
+ function allRegistries(pattern) {
7985
+ const list = [];
7986
+ list.push({
7987
+ reg: pattern.registry,
7988
+ names: registryNames(pattern, pattern.registry, true),
7989
+ isPrimary: true
7990
+ });
7991
+ for (const sibling of pattern.coKeyedRegistries ?? []) {
7992
+ list.push({
7993
+ reg: sibling,
7994
+ names: registryNames(pattern, sibling, false),
7995
+ isPrimary: false
7996
+ });
7867
7997
  }
7868
- const topLevelResult = loadEvents(eventsDir, entityNames);
7869
- const { events: entitySugar, issues: sugarIssues } = collectEntityEvents(entitiesDir);
7870
- const { events: merged, issues: mergeIssues } = mergeEvents(
7871
- topLevelResult.events,
7872
- entitySugar
7873
- );
7874
- const issues = [
7875
- ...topLevelResult.issues,
7876
- ...sugarIssues,
7877
- ...mergeIssues
7878
- ];
7879
- return { events: merged, issues };
7998
+ return list;
7880
7999
  }
7881
- function buildTypesContent(events) {
7882
- const sorted = [...events].sort((a, b) => a.type.localeCompare(b.type));
7883
- if (sorted.length === 0) {
7884
- return HEADER7 + `
7885
- import type { DomainEvent } from '../event-bus.protocol';
7886
-
7887
- export type AppDomainEvent = never;
7888
-
7889
- export type EventTypeName = string;
7890
- export type EventOfType<T extends EventTypeName> = DomainEvent;
7891
- export type PayloadOfType<T extends EventTypeName> = DomainEvent['payload'];
7892
- `;
8000
+ function buildTokensTs(pattern) {
8001
+ const registries = allRegistries(pattern);
8002
+ const typeImports = [];
8003
+ for (const { reg } of registries) {
8004
+ typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
8005
+ typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
7893
8006
  }
7894
- const chunks = [];
7895
- chunks.push(HEADER7);
7896
- chunks.push("");
7897
- chunks.push(`import type { DomainEvent } from '../event-bus.protocol';`);
7898
- chunks.push("");
7899
- for (const ev of sorted) {
7900
- const interfaceName = `${toPascalCase3(ev.type)}Event`;
7901
- const aggregateLit = aggregateTypeLiteral(ev);
7902
- if (ev.description) {
7903
- chunks.push(`/** ${ev.description} */`);
7904
- }
7905
- chunks.push(`export interface ${interfaceName} extends DomainEvent {`);
7906
- chunks.push(` readonly type: '${ev.type}';`);
7907
- chunks.push(` readonly aggregateType: '${aggregateLit}';`);
7908
- const payloadKeys = Object.keys(ev.payload).sort();
7909
- if (payloadKeys.length === 0) {
7910
- chunks.push(` readonly payload: Record<string, never>;`);
7911
- } else {
7912
- chunks.push(` readonly payload: {`);
7913
- for (const key of payloadKeys) {
7914
- const field = ev.payload[key];
7915
- if (!field) continue;
7916
- if (field.description) {
7917
- chunks.push(` /** ${field.description} */`);
7918
- }
7919
- chunks.push(` ${toCamelCase2(key)}: ${tsTypeForField(field)};`);
7920
- }
7921
- chunks.push(` };`);
8007
+ const lines = [];
8008
+ lines.push(HEADER7.trimEnd());
8009
+ lines.push("");
8010
+ for (const i of emitTypeImports(typeImports)) lines.push(i);
8011
+ lines.push("");
8012
+ for (const { names: names2 } of registries) {
8013
+ lines.push(`export const ${names2.tokenConst} = Symbol('${names2.tokenConst}');`);
8014
+ }
8015
+ lines.push("");
8016
+ for (const { reg, names: names2 } of registries) {
8017
+ lines.push(
8018
+ `export type ${names2.mapType} = Map<${reg.keyType}, ${reg.valueType}>;`
8019
+ );
8020
+ }
8021
+ lines.push("");
8022
+ return lines.join("\n");
8023
+ }
8024
+ function buildProvidersTs(pattern) {
8025
+ const registries = allRegistries(pattern);
8026
+ const providerImports = [];
8027
+ for (const { reg } of registries) {
8028
+ for (const e of reg.entries) {
8029
+ providerImports.push({ specifier: e.providerImport, name: e.provider });
7922
8030
  }
7923
- chunks.push(`}`);
7924
- chunks.push("");
7925
8031
  }
7926
- const unionMembers = sorted.map((ev) => `${toPascalCase3(ev.type)}Event`);
7927
- chunks.push(`export type AppDomainEvent =`);
7928
- chunks.push(` | ${unionMembers.join("\n | ")};`);
7929
- chunks.push("");
7930
- chunks.push(`export type EventTypeName = AppDomainEvent['type'];`);
7931
- chunks.push(
7932
- `export type EventOfType<T extends EventTypeName> = Extract<AppDomainEvent, { type: T }>;`
7933
- );
7934
- chunks.push(
7935
- `export type PayloadOfType<T extends EventTypeName> = EventOfType<T>['payload'];`
8032
+ const tokenValueImports = registries.map(({ names: names2 }) => names2.tokenConst);
8033
+ const mapTypeImports = registries.map(({ names: names2 }) => names2.mapType);
8034
+ const typeImports = [];
8035
+ for (const { reg } of registries) {
8036
+ typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
8037
+ typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
8038
+ }
8039
+ const optsType = forRootOptionsTypeName(pattern);
8040
+ const lines = [];
8041
+ lines.push(HEADER7.trimEnd());
8042
+ lines.push("");
8043
+ lines.push(`import type { Provider } from '@nestjs/common';`);
8044
+ for (const i of emitTypeImports(typeImports)) lines.push(i);
8045
+ for (const i of emitValueImports(providerImports)) lines.push(i);
8046
+ const tokenLine = [
8047
+ ...tokenValueImports.sort(),
8048
+ ...mapTypeImports.sort().map((m) => `type ${m}`)
8049
+ ].join(", ");
8050
+ lines.push(`import { ${tokenLine} } from './tokens.js';`);
8051
+ lines.push(`import type { ${optsType} } from './module.js';`);
8052
+ lines.push("");
8053
+ const { pascal } = patternNames(pattern);
8054
+ lines.push(
8055
+ `export function build${pascal}RegistryProviders(opts?: ${optsType}): Provider[] {`
7936
8056
  );
7937
- chunks.push("");
7938
- return chunks.join("\n");
8057
+ lines.push(" return [");
8058
+ for (const { reg, names: names2 } of registries) {
8059
+ const factoryArgs = reg.entries.map((e, i) => `${camelArg(e.provider, i)}: ${e.provider}`).join(", ");
8060
+ lines.push(" {");
8061
+ lines.push(` provide: ${names2.tokenConst},`);
8062
+ lines.push(` useFactory: (${factoryArgs}) => {`);
8063
+ lines.push(` const base: ${names2.mapType} = new Map<${reg.keyType}, ${reg.valueType}>([`);
8064
+ for (const [i, e] of reg.entries.entries()) {
8065
+ lines.push(
8066
+ ` ['${e.key}' as ${reg.keyType}, ${camelArg(e.provider, i)}],`
8067
+ );
8068
+ }
8069
+ lines.push(" ]);");
8070
+ lines.push(` if (opts?.${names2.overrideField}) {`);
8071
+ lines.push(` for (const [k, v] of Object.entries(opts.${names2.overrideField})) {`);
8072
+ lines.push(` if (v !== undefined) base.set(k as ${reg.keyType}, v as ${reg.valueType});`);
8073
+ lines.push(" }");
8074
+ lines.push(" }");
8075
+ lines.push(` return base as ${names2.mapType};`);
8076
+ lines.push(" },");
8077
+ const injectList = reg.entries.map((e) => e.provider).join(", ");
8078
+ lines.push(` inject: [${injectList}],`);
8079
+ lines.push(" },");
8080
+ }
8081
+ lines.push(" ];");
8082
+ lines.push("}");
8083
+ lines.push("");
8084
+ return lines.join("\n");
7939
8085
  }
7940
- function buildSchemasContent(events) {
7941
- const sorted = [...events].sort((a, b) => a.type.localeCompare(b.type));
7942
- if (sorted.length === 0) {
7943
- return HEADER7 + `
7944
- import { z } from 'zod';
7945
- import type { EventTypeName } from './types';
7946
-
7947
- export const eventPayloadSchemas = {} as Record<EventTypeName, z.ZodType>;
7948
- `;
8086
+ function camelArg(provider, index2) {
8087
+ const camel = provider.charAt(0).toLowerCase() + provider.slice(1);
8088
+ return `${camel}_${index2}`;
8089
+ }
8090
+ function buildDispatcherTs(pattern) {
8091
+ const registries = allRegistries(pattern);
8092
+ const className = dispatcherClassName(pattern);
8093
+ const errorClass = `${className}Error`;
8094
+ const typeImports = [];
8095
+ for (const { reg } of registries) {
8096
+ typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
8097
+ typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
7949
8098
  }
7950
- const chunks = [];
7951
- chunks.push(HEADER7);
7952
- chunks.push("");
7953
- chunks.push(`import { z } from 'zod';`);
7954
- chunks.push(`import type { EventTypeName } from './types';`);
7955
- chunks.push("");
7956
- for (const ev of sorted) {
7957
- const schemaConst = `${toCamelCase2(ev.type)}PayloadSchema`;
7958
- const payloadKeys = Object.keys(ev.payload).sort();
7959
- if (payloadKeys.length === 0) {
7960
- chunks.push(`export const ${schemaConst} = z.object({}).strict();`);
7961
- chunks.push("");
7962
- continue;
8099
+ const providerImports = [];
8100
+ for (const { reg } of registries) {
8101
+ for (const e of reg.entries) {
8102
+ providerImports.push({ specifier: e.providerImport, name: e.provider });
7963
8103
  }
7964
- chunks.push(`export const ${schemaConst} = z.object({`);
7965
- for (const key of payloadKeys) {
7966
- const field = ev.payload[key];
7967
- if (!field) continue;
7968
- chunks.push(` ${toCamelCase2(key)}: ${zodExprForField(field)},`);
8104
+ }
8105
+ const tokenValues = registries.map(({ names: names2 }) => names2.tokenConst);
8106
+ const mapTypes = registries.map(({ names: names2 }) => names2.mapType);
8107
+ const lines = [];
8108
+ lines.push(HEADER7.trimEnd());
8109
+ lines.push("");
8110
+ lines.push(`import { Inject, Injectable } from '@nestjs/common';`);
8111
+ for (const i of emitTypeImports(typeImports)) lines.push(i);
8112
+ for (const i of emitValueImports(providerImports)) lines.push(i);
8113
+ const tokenLine = [
8114
+ ...tokenValues.sort(),
8115
+ ...mapTypes.sort().map((m) => `type ${m}`)
8116
+ ].join(", ");
8117
+ lines.push(`import { ${tokenLine} } from './tokens.js';`);
8118
+ lines.push("");
8119
+ lines.push(`export class ${errorClass} extends Error {}`);
8120
+ lines.push("");
8121
+ lines.push("@Injectable()");
8122
+ lines.push(`export class ${className} {`);
8123
+ lines.push(" constructor(");
8124
+ for (const { names: names2 } of registries) {
8125
+ const fieldName = names2.method === "select" ? "registry" : `${camelFromMethod(names2.method)}Registry`;
8126
+ lines.push(
8127
+ ` @Inject(${names2.tokenConst}) protected readonly ${fieldName}: ${names2.mapType},`
8128
+ );
8129
+ }
8130
+ lines.push(" ) {}");
8131
+ lines.push("");
8132
+ for (const { reg, names: names2 } of registries) {
8133
+ const fieldName = names2.method === "select" ? "registry" : `${camelFromMethod(names2.method)}Registry`;
8134
+ for (const e of reg.entries) {
8135
+ lines.push(` ${names2.method}(key: '${e.key}'): ${e.provider};`);
7969
8136
  }
7970
- chunks.push(`}).strict();`);
7971
- chunks.push("");
8137
+ lines.push(` ${names2.method}(key: ${reg.keyType}): ${reg.valueType};`);
8138
+ lines.push(` ${names2.method}(key: ${reg.keyType}): ${reg.valueType} {`);
8139
+ lines.push(` const entry = this.${fieldName}.get(key);`);
8140
+ lines.push(
8141
+ ` if (!entry) throw new ${errorClass}(\`Unknown ${reg.keyType}: \${String(key)}\`);`
8142
+ );
8143
+ lines.push(` return entry;`);
8144
+ lines.push(" }");
8145
+ lines.push("");
7972
8146
  }
7973
- chunks.push(`export const eventPayloadSchemas = {`);
7974
- for (const ev of sorted) {
7975
- chunks.push(` '${ev.type}': ${toCamelCase2(ev.type)}PayloadSchema,`);
8147
+ if (pattern.dispatcher?.assemblySlot) {
8148
+ lines.push(
8149
+ ` // ADR-032 Decision 5 \u2014 \`assemblySlot: '${pattern.dispatcher.assemblySlot}'\` is a`
8150
+ );
8151
+ lines.push(
8152
+ ` // subclass contract, not emitted here. Consumers subclass and add the`
8153
+ );
8154
+ lines.push(
8155
+ ` // \`${pattern.dispatcher.assemblySlot}(...)\` method themselves.`
8156
+ );
7976
8157
  }
7977
- chunks.push(`} as const satisfies Record<EventTypeName, z.ZodType>;`);
7978
- chunks.push("");
7979
- return chunks.join("\n");
8158
+ lines.push("}");
8159
+ lines.push("");
8160
+ return lines.join("\n");
7980
8161
  }
7981
- var REGISTRY_INTERFACE = [
7982
- "export interface EventMetadata {",
7983
- " type: EventTypeName;",
7984
- " tier: 'domain' | 'audit';",
7985
- " direction: 'inbound' | 'change' | 'outbound' | null;",
7986
- " pool: 'events_inbound' | 'events_change' | 'events_outbound' | null;",
7987
- " aggregate?: string;",
7988
- " source?: string;",
7989
- " destination?: string;",
7990
- " version: number;",
7991
- " retry: { attempts: number; backoff: 'linear' | 'exponential' };",
7992
- "}"
7993
- ].join("\n");
7994
- var REGISTRY_GETTER = [
7995
- "export function getEventMetadata<T extends EventTypeName>(type: T): EventMetadata {",
7996
- " const meta = eventRegistry[type];",
7997
- " if (!meta) {",
7998
- " throw new Error(`No registry entry for event type '${String(type)}' \u2014 declare events under events/*.yaml and re-run \\`codegen entity new --all\\`.`);",
7999
- " }",
8000
- " return meta;",
8001
- "}"
8002
- ].join("\n");
8003
- function buildRegistryContent(events) {
8004
- const sorted = [...events].sort((a, b) => a.type.localeCompare(b.type));
8005
- const chunks = [];
8006
- chunks.push(HEADER7);
8007
- chunks.push("");
8008
- chunks.push(`import type { EventTypeName } from './types';`);
8009
- chunks.push("");
8010
- chunks.push(REGISTRY_INTERFACE);
8011
- chunks.push("");
8012
- if (sorted.length === 0) {
8013
- chunks.push(
8014
- `export const eventRegistry = {} as Record<EventTypeName, EventMetadata>;`
8015
- );
8016
- chunks.push("");
8017
- chunks.push(REGISTRY_GETTER);
8018
- chunks.push("");
8019
- return chunks.join("\n");
8162
+ function camelFromMethod(method) {
8163
+ if (method.startsWith("select") && method.length > "select".length) {
8164
+ const tail = method.slice("select".length);
8165
+ return tail.charAt(0).toLowerCase() + tail.slice(1);
8020
8166
  }
8021
- chunks.push(`export const eventRegistry = {`);
8022
- for (const ev of sorted) {
8023
- const tier = ev.tier ?? "domain";
8024
- chunks.push(` '${ev.type}': {`);
8025
- chunks.push(` type: '${ev.type}',`);
8026
- chunks.push(` tier: '${tier}',`);
8027
- if (tier === "audit") {
8028
- chunks.push(` direction: null,`);
8029
- chunks.push(` pool: null,`);
8030
- } else {
8031
- chunks.push(` direction: '${ev.direction}',`);
8032
- chunks.push(` pool: '${ev.pool}',`);
8033
- }
8034
- if (ev.aggregate !== void 0) {
8035
- chunks.push(` aggregate: '${ev.aggregate}',`);
8036
- }
8037
- if (ev.source !== void 0) {
8038
- chunks.push(` source: '${ev.source}',`);
8039
- }
8040
- if (ev.destination !== void 0) {
8041
- chunks.push(` destination: '${ev.destination}',`);
8042
- }
8043
- chunks.push(` version: ${ev.version},`);
8044
- chunks.push(
8045
- ` retry: { attempts: ${ev.retry.attempts}, backoff: '${ev.retry.backoff}' },`
8167
+ return method;
8168
+ }
8169
+ function buildModuleTs(pattern) {
8170
+ const registries = allRegistries(pattern);
8171
+ const className = dispatcherClassName(pattern);
8172
+ const moduleName = moduleClassName(pattern);
8173
+ const optsType = forRootOptionsTypeName(pattern);
8174
+ const { pascal } = patternNames(pattern);
8175
+ const typeImports = [];
8176
+ for (const { reg } of registries) {
8177
+ typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
8178
+ typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
8179
+ }
8180
+ const tokenValues = registries.map(({ names: names2 }) => names2.tokenConst);
8181
+ const lines = [];
8182
+ lines.push(HEADER7.trimEnd());
8183
+ lines.push("");
8184
+ lines.push(`import { type DynamicModule, Module } from '@nestjs/common';`);
8185
+ for (const i of emitTypeImports(typeImports)) lines.push(i);
8186
+ lines.push(`import { ${className} } from './dispatcher.js';`);
8187
+ lines.push(`import { build${pascal}RegistryProviders } from './registry.providers.js';`);
8188
+ lines.push(`import { ${tokenValues.sort().join(", ")} } from './tokens.js';`);
8189
+ lines.push("");
8190
+ lines.push(`export interface ${optsType} {`);
8191
+ for (const { reg, names: names2 } of registries) {
8192
+ lines.push(
8193
+ ` ${names2.overrideField}?: Partial<Record<${reg.keyType}, ${reg.valueType}>>;`
8046
8194
  );
8047
- chunks.push(` },`);
8048
8195
  }
8049
- chunks.push(
8050
- `} as const satisfies Record<EventTypeName, EventMetadata>;`
8196
+ lines.push("}");
8197
+ lines.push("");
8198
+ lines.push("@Module({})");
8199
+ lines.push(`export class ${moduleName} {`);
8200
+ lines.push(` static forRoot(opts?: ${optsType}): DynamicModule {`);
8201
+ lines.push(" return {");
8202
+ lines.push(` module: ${moduleName},`);
8203
+ lines.push(
8204
+ ` providers: [...build${pascal}RegistryProviders(opts), ${className}],`
8051
8205
  );
8052
- chunks.push("");
8053
- chunks.push(REGISTRY_GETTER);
8054
- chunks.push("");
8055
- return chunks.join("\n");
8056
- }
8057
- var BUS_BODY = `import { Injectable, Inject } from '@nestjs/common';
8058
- import { randomUUID } from 'crypto';
8059
- import { EVENT_BUS, EVENTS_MULTI_TENANT } from '../events.tokens';
8060
- import { MissingTenantIdError } from '../events-errors';
8061
- import type { IEventBus, DrizzleTransaction } from '../event-bus.protocol';
8062
- import { eventPayloadSchemas } from './schemas';
8063
- import { getEventMetadata } from './registry';
8064
- import type { EventTypeName, EventOfType, PayloadOfType } from './types';
8065
-
8066
- /**
8067
- * Typed facade over IEventBus.
8068
- *
8069
- * Stamps \`pool\`, \`direction\`, \`tier\`, and \`version\` into \`event.metadata\`
8070
- * from the generated \`eventRegistry\` before delegating to
8071
- * \`IEventBus.publish()\`. Downstream backends (DrizzleEventBus) read those
8072
- * values to populate the explicit \`domain_events\` columns.
8073
- *
8074
- * Tier stamping (AUDIT-3): every event carries \`metadata.tier\`, sourced
8075
- * from the registry. For \`tier: 'audit'\` events, the bus FORCES
8076
- * \`metadata.pool = null\` and \`metadata.direction = null\` regardless of
8077
- * any caller-supplied values in \`opts.metadata\` \u2014 audit routing is
8078
- * bus-stamped, not caller-controlled. Caller overrides are silently
8079
- * dropped with a debug-level log (callers should not be specifying these
8080
- * for audit events; see ai-docs/specs/issue-242/plan.md \xA7AUDIT-3).
8081
- *
8082
- * Validation gating (EVT-Q5): \`CODEGEN_EVENT_VALIDATE\` env flag, default on.
8083
- * Uses \`safeParse\` + \`console.warn\` \u2014 never throws, so a bad publish does
8084
- * not crash a hot path.
8085
- *
8086
- * Multi-tenancy (EVT-6): when the EventsModule is configured with
8087
- * \`multiTenant: true\`, every publish must supply \`opts.metadata.tenantId\`
8088
- * \u2014 otherwise \`publish()\` throws \`MissingTenantIdError\`. When \`multiTenant\`
8089
- * is \`false\` (default), no tenantId is required. If a tenantId IS supplied,
8090
- * it is preserved on \`event.metadata\` and the Drizzle backend writes it to
8091
- * \`domain_events.tenant_id\` (EVT-4).
8092
- */
8093
- @Injectable()
8094
- export class TypedEventBus {
8095
- constructor(
8096
- @Inject(EVENT_BUS) private readonly bus: IEventBus,
8097
- @Inject(EVENTS_MULTI_TENANT) private readonly multiTenant: boolean,
8098
- ) {}
8099
-
8100
- async publish<T extends EventTypeName>(
8101
- type: T,
8102
- aggregateId: string,
8103
- payload: PayloadOfType<T>,
8104
- opts?: { tx?: DrizzleTransaction; metadata?: Record<string, unknown> },
8105
- ): Promise<void> {
8106
- const meta = getEventMetadata(type);
8107
-
8108
- const flag = process.env['CODEGEN_EVENT_VALIDATE'];
8109
- const shouldValidate =
8110
- flag === undefined ? true : flag !== 'false' && flag !== '0';
8111
- if (shouldValidate) {
8112
- // \`eventPayloadSchemas\` is typed as \`Record<EventTypeName, z.ZodType>\`,
8113
- // so under \`noUncheckedIndexedAccess\` the indexed lookup widens
8114
- // to \`z.ZodType | undefined\`. When no events are registered at
8115
- // codegen time \`EventTypeName\` degrades to \`string\` and the
8116
- // schemas object is literally \`{}\` \u2014 the guard below is the
8117
- // honest handling of that empty-registry case (skip validation;
8118
- // it's a warn-only best-effort check per the class docblock).
8119
- const schema = eventPayloadSchemas[type];
8120
- if (schema) {
8121
- const check = schema.safeParse(payload);
8122
- if (!check.success) {
8123
- console.warn(
8124
- \`[TypedEventBus] payload validation failed for \${String(type)}:\`,
8125
- check.error.issues,
8126
- );
8127
- }
8128
- }
8129
- }
8130
-
8131
- const tenantId = opts?.metadata?.['tenantId'];
8132
- if (this.multiTenant && (tenantId === undefined || tenantId === null)) {
8133
- throw new MissingTenantIdError(type as string);
8134
- }
8135
-
8136
- const aggregateType =
8137
- meta.aggregate ?? meta.source ?? meta.destination ?? (type as string);
8138
-
8139
- // AUDIT-3: build metadata with tier-aware routing stamping. For
8140
- // \`tier: 'audit'\` events the bus FORCES pool/direction to null,
8141
- // even if the caller supplied them in opts.metadata. Audit routing
8142
- // is bus-stamped, not caller-controlled (see plan \xA7AUDIT-3).
8143
- const baseMetadata: Record<string, unknown> = { ...(opts?.metadata ?? {}) };
8144
- if (meta.tier === 'audit') {
8145
- if (
8146
- baseMetadata['pool'] !== undefined ||
8147
- baseMetadata['direction'] !== undefined
8148
- ) {
8149
- console.debug(
8150
- \`[TypedEventBus] tier:audit event '\${String(type)}' had pool/direction in opts.metadata; overriding to null.\`,
8151
- );
8152
- }
8153
- baseMetadata['pool'] = null;
8154
- baseMetadata['direction'] = null;
8155
- baseMetadata['tier'] = 'audit';
8156
- } else {
8157
- baseMetadata['pool'] = meta.pool;
8158
- baseMetadata['direction'] = meta.direction;
8159
- baseMetadata['tier'] = 'domain';
8160
- }
8161
- baseMetadata['version'] = meta.version;
8162
-
8163
- await this.bus.publish(
8164
- {
8165
- id: randomUUID(),
8166
- type,
8167
- aggregateId,
8168
- aggregateType,
8169
- payload: payload as Record<string, unknown>,
8170
- occurredAt: new Date(),
8171
- metadata: baseMetadata,
8172
- },
8173
- opts?.tx,
8174
- );
8175
- }
8176
-
8177
- subscribe<T extends EventTypeName>(
8178
- type: T,
8179
- handler: (event: EventOfType<T>) => Promise<void>,
8180
- ): () => void {
8181
- return this.bus.subscribe<EventOfType<T>>(type, handler as never);
8182
- }
8206
+ const exportItems = [...tokenValues.sort(), className];
8207
+ lines.push(` exports: [${exportItems.join(", ")}],`);
8208
+ lines.push(" };");
8209
+ lines.push(" }");
8210
+ lines.push("}");
8211
+ lines.push("");
8212
+ return lines.join("\n");
8183
8213
  }
8184
- `;
8185
- function buildBusContent(_events) {
8186
- return HEADER7 + "\n" + BUS_BODY;
8214
+ function buildIndexTs(pattern) {
8215
+ const lines = [];
8216
+ lines.push(HEADER7.trimEnd());
8217
+ lines.push("");
8218
+ lines.push(`export * from './tokens.js';`);
8219
+ lines.push(`export * from './dispatcher.js';`);
8220
+ lines.push(`export * from './module.js';`);
8221
+ lines.push(`export * from './registry.providers.js';`);
8222
+ lines.push("");
8223
+ return lines.join("\n");
8187
8224
  }
8188
- function buildIndexContent(_events) {
8189
- return HEADER7 + `
8190
- export * from './types';
8191
- export * from './schemas';
8192
- export * from './registry';
8193
- export * from './bus';
8194
- `;
8225
+ function buildRootBarrelTs(patterns) {
8226
+ const lines = [];
8227
+ lines.push(HEADER7.trimEnd());
8228
+ lines.push("");
8229
+ if (patterns.length === 0) {
8230
+ lines.push("// No orchestration patterns registered.");
8231
+ lines.push("export {};");
8232
+ lines.push("");
8233
+ return lines.join("\n");
8234
+ }
8235
+ const slugs = patterns.map((p) => toKebabCase2(p.name)).sort();
8236
+ for (const slug of slugs) {
8237
+ lines.push(`export * from './${slug}/index.js';`);
8238
+ }
8239
+ lines.push("");
8240
+ return lines.join("\n");
8195
8241
  }
8196
- var OUTPUT_FILE_NAMES = [
8197
- "types.ts",
8198
- "schemas.ts",
8199
- "registry.ts",
8200
- "bus.ts",
8201
- "index.ts"
8202
- ];
8203
- async function generateEventCodegen(opts) {
8204
- const { entitiesDir, eventsDir, outputDir, dryRun = false } = opts;
8205
- const { events: merged, issues } = collectMergedEvents({
8206
- entitiesDir,
8207
- eventsDir
8208
- });
8209
- const builders = {
8210
- "types.ts": buildTypesContent,
8211
- "schemas.ts": buildSchemasContent,
8212
- "registry.ts": buildRegistryContent,
8213
- "bus.ts": buildBusContent,
8214
- "index.ts": buildIndexContent
8215
- };
8216
- const files = OUTPUT_FILE_NAMES.map((name) => ({
8242
+ function buildPatternFiles(pattern, outputRoot) {
8243
+ assertEmittable(pattern);
8244
+ const slug = toKebabCase2(pattern.name);
8245
+ const outputDir = path12.join(outputRoot, slug);
8246
+ const make = (name, content) => ({
8217
8247
  name,
8218
8248
  outputPath: path12.join(outputDir, name),
8219
- content: builders[name](merged)
8220
- }));
8221
- const hasError = issues.some((i) => i.severity === "error");
8249
+ relativePath: path12.join(slug, name),
8250
+ content
8251
+ });
8252
+ const files = [
8253
+ make("tokens.ts", buildTokensTs(pattern)),
8254
+ make("registry.providers.ts", buildProvidersTs(pattern)),
8255
+ make("dispatcher.ts", buildDispatcherTs(pattern)),
8256
+ make("module.ts", buildModuleTs(pattern)),
8257
+ make("index.ts", buildIndexTs(pattern))
8258
+ ];
8259
+ return {
8260
+ patternName: pattern.name,
8261
+ slug,
8262
+ outputDir,
8263
+ files
8264
+ };
8265
+ }
8266
+ function generateOrchestrationModules(opts) {
8267
+ const { patterns, outputRoot, dryRun = false } = opts;
8268
+ const perPattern = [];
8269
+ const allFiles = [];
8270
+ for (const pattern of patterns) {
8271
+ const result = buildPatternFiles(pattern, outputRoot);
8272
+ perPattern.push(result);
8273
+ allFiles.push(...result.files);
8274
+ }
8275
+ const rootBarrel = {
8276
+ name: "index.ts",
8277
+ outputPath: path12.join(outputRoot, "index.ts"),
8278
+ relativePath: "index.ts",
8279
+ content: buildRootBarrelTs(patterns)
8280
+ };
8281
+ allFiles.push(rootBarrel);
8222
8282
  let written = false;
8223
- if (!dryRun && !hasError) {
8224
- fs9.mkdirSync(outputDir, { recursive: true });
8225
- for (const file of files) {
8226
- fs9.writeFileSync(file.outputPath, file.content);
8283
+ if (!dryRun && patterns.length > 0) {
8284
+ fs9.mkdirSync(outputRoot, { recursive: true });
8285
+ for (const r of perPattern) {
8286
+ fs9.mkdirSync(r.outputDir, { recursive: true });
8287
+ for (const f of r.files) {
8288
+ fs9.writeFileSync(f.outputPath, f.content);
8289
+ }
8227
8290
  }
8291
+ fs9.writeFileSync(rootBarrel.outputPath, rootBarrel.content);
8228
8292
  written = true;
8229
8293
  }
8230
8294
  return {
8231
- outputDir,
8232
- eventCount: merged.length,
8233
- events: merged,
8234
- issues,
8235
- written,
8236
- files
8295
+ outputRoot,
8296
+ patterns: perPattern,
8297
+ files: allFiles,
8298
+ written
8237
8299
  };
8238
8300
  }
8239
8301
 
@@ -9779,16 +9841,10 @@ var EntityNewCommand = class extends Command2 {
9779
9841
  const generatedDir = resolveGeneratedDir(ctx);
9780
9842
  const architecture = resolveArchitecture(ctx);
9781
9843
  const subsystemsRoot = resolveSubsystemsRoot(ctx);
9782
- const scopeEntityTypePath = path14.resolve(
9783
- subsystemsRoot,
9784
- "jobs/generated/scope-entity-type.ts"
9785
- );
9786
- const eventsDir = resolveEventsDir(ctx);
9787
- const eventCodegenOutputDir = path14.resolve(
9788
- subsystemsRoot,
9789
- "events/generated"
9790
- );
9791
9844
  const runtimeMode = resolveRuntimeMode(ctx.config);
9845
+ const scopeEntityTypePath = runtimeMode === "package" ? path14.resolve(generatedDir, "scope-entity-type.ts") : path14.resolve(subsystemsRoot, "jobs/generated/scope-entity-type.ts");
9846
+ const eventsDir = resolveEventsDir(ctx);
9847
+ const eventCodegenOutputDir = runtimeMode === "package" ? path14.resolve(generatedDir, "events") : path14.resolve(subsystemsRoot, "events/generated");
9792
9848
  const bridgeInstalledForRegistry = configuredSubsystemNames(
9793
9849
  ctx.config
9794
9850
  ).includes("bridge");
@@ -9838,6 +9894,7 @@ var EntityNewCommand = class extends Command2 {
9838
9894
  entitiesDir,
9839
9895
  eventsDir,
9840
9896
  outputDir: eventCodegenOutputDir,
9897
+ mode: runtimeMode,
9841
9898
  dryRun: true
9842
9899
  });
9843
9900
  const bridgeRegistryPlan = await generateBridgeRegistry({
@@ -10018,7 +10075,8 @@ var EntityNewCommand = class extends Command2 {
10018
10075
  eventCodegenResult = await generateEventCodegen({
10019
10076
  entitiesDir,
10020
10077
  eventsDir,
10021
- outputDir: eventCodegenOutputDir
10078
+ outputDir: eventCodegenOutputDir,
10079
+ mode: runtimeMode
10022
10080
  });
10023
10081
  if (!isJsonMode()) {
10024
10082
  for (const issue of eventCodegenResult.issues) {