@pattern-stack/codegen 0.14.2 → 0.15.0

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