@pattern-stack/codegen 0.14.1 → 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 fs5 from "fs";
6277
- import path8 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";
@@ -6541,274 +6541,16 @@ function resolveSubsystemsRoot(ctx, overrideTarget) {
6541
6541
  return resolveSubsystemsRootFromConfig(ctx.cwd, ctx.config);
6542
6542
  }
6543
6543
 
6544
- // src/cli/shared/subsystem-barrel-generator.ts
6545
- function quoteOpts(opts) {
6546
- const entries = Object.entries(opts).filter(([, v]) => v !== void 0);
6547
- if (entries.length === 0) return "";
6548
- return "{ " + entries.map(([k, v]) => `${k}: ${typeof v === "string" ? `'${v}'` : String(v)}`).join(", ") + " }";
6549
- }
6550
- function jsonToTs(value) {
6551
- if (value === null || value === void 0) return "undefined";
6552
- if (typeof value === "string") return `'${value.replace(/'/g, "\\'")}'`;
6553
- if (typeof value === "number" || typeof value === "boolean") return String(value);
6554
- if (Array.isArray(value)) return `[${value.map(jsonToTs).join(", ")}]`;
6555
- if (typeof value === "object") {
6556
- const entries = Object.entries(value).filter(
6557
- ([, v]) => v !== void 0
6558
- );
6559
- return `{ ${entries.map(([k, v]) => `${k}: ${jsonToTs(v)}`).join(", ")} }`;
6560
- }
6561
- return "undefined";
6562
- }
6563
- function quoteBullmqDomainOpts(input) {
6564
- const { backend, multiTenant, bullExt } = input;
6565
- if (backend !== "bullmq" || !bullExt) {
6566
- return quoteOpts({ backend, multiTenant });
6567
- }
6568
- const parts = [`backend: 'bullmq'`];
6569
- if (multiTenant) parts.push(`multiTenant: true`);
6570
- parts.push(`extensions: { bullmq: ${jsonToTs(bullExt)} }`);
6571
- return `{ ${parts.join(", ")} }`;
6572
- }
6573
- var COMPOSERS = {
6574
- events: ({ moduleImport, cfg }) => {
6575
- const backend = cfg?.backend ?? "drizzle";
6576
- const multiTenant = Boolean(cfg?.multi_tenant);
6577
- return {
6578
- imports: [
6579
- `import { EventsModule } from '${moduleImport("events", "events.module")}';`
6580
- ],
6581
- calls: [
6582
- ` EventsModule.forRoot(${quoteOpts({ backend, multiTenant })}),`
6583
- ]
6584
- };
6585
- },
6586
- jobs: ({ moduleImport, cfg }) => {
6587
- const backend = cfg?.backend ?? "drizzle";
6588
- const multiTenant = Boolean(cfg?.multi_tenant);
6589
- const workerMode = (cfg?.worker_mode ?? "standalone").trim();
6590
- const imports = [
6591
- `import { JobsDomainModule } from '${moduleImport("jobs", "jobs-domain.module")}';`
6592
- ];
6593
- const bullExt = backend === "bullmq" ? cfg?.extensions?.bullmq : void 0;
6594
- const domainOpts = quoteBullmqDomainOpts({ backend, multiTenant, bullExt });
6595
- const calls = [` JobsDomainModule.forRoot(${domainOpts}),`];
6596
- if (workerMode === "embedded") {
6597
- imports.push(
6598
- `import { JobWorkerModule } from '${moduleImport("jobs", "job-worker.module")}';`
6599
- );
6600
- const workerOpts = backend === "bullmq" ? `{ mode: 'embedded', backend: 'bullmq'${bullExt ? `, domainModuleExtensions: { bullmq: ${jsonToTs(bullExt)} }` : ""} }` : `{ mode: 'embedded' }`;
6601
- calls.push(` JobWorkerModule.forRoot(${workerOpts}),`);
6602
- }
6603
- return { imports, calls };
6604
- },
6605
- bridge: ({ moduleImport, cfg }) => {
6606
- const backend = cfg?.backend ?? "drizzle";
6607
- const multiTenant = Boolean(cfg?.multi_tenant);
6608
- return {
6609
- imports: [
6610
- `import { BridgeModule } from '${moduleImport("bridge", "bridge.module")}';`
6611
- ],
6612
- calls: [
6613
- ` BridgeModule.forRoot(${quoteOpts({ backend, multiTenant })}),`
6614
- ]
6615
- };
6616
- },
6617
- integration: ({ moduleImport, cfg }) => {
6618
- const backend = cfg?.backend ?? "drizzle";
6619
- const multiTenant = Boolean(cfg?.multi_tenant);
6620
- return {
6621
- imports: [
6622
- `import { IntegrationModule } from '${moduleImport("integration", "integration.module")}';`
6623
- ],
6624
- calls: [
6625
- ` IntegrationModule.forRoot(${quoteOpts({ backend, multiTenant })}),`
6626
- ]
6627
- };
6628
- }
6629
- };
6630
- var PACKAGE2 = "@pattern-stack/codegen";
6631
- function makeModuleImport(mode, subsystemsRel) {
6632
- if (mode === "vendored") {
6633
- return (subsystem, moduleBasename) => `${subsystemsRel}/${subsystem}/${moduleBasename}`;
6634
- }
6635
- return (subsystem) => subsystem === "events" ? `${PACKAGE2}/subsystems` : `${PACKAGE2}/runtime/subsystems/${subsystem}/index`;
6636
- }
6637
- var COMPOSABLE_ORDER = ["events", "jobs", "bridge", "integration"];
6638
- var HEADER3 = `// AUTO-GENERATED by @pattern-stack/codegen. DO NOT EDIT.
6639
- // Subsystem composition barrel \u2014 reflects \`subsystems.install\` in
6640
- // codegen.config.yaml and the per-subsystem option blocks
6641
- // (\`events:\`, \`jobs:\`, \`bridge:\`, \`integration:\`).
6642
- //
6643
- // Wire into AppModule once:
6644
- //
6645
- // import { SUBSYSTEM_MODULES } from './generated/subsystems';
6646
- // @Module({ imports: [DatabaseModule, ...SUBSYSTEM_MODULES, ...GENERATED_MODULES] })
6647
- //
6648
- // Regenerated by every \`codegen entity new\` / \`codegen subsystem install\`.
6649
-
6650
- `;
6651
- function buildSubsystemBarrel(installed, config, subsystemsRel, mode = "vendored") {
6652
- const moduleImport = makeModuleImport(mode, subsystemsRel);
6653
- const actable = installed.filter((i) => i.status !== "incomplete");
6654
- const installedNames = new Set(actable.map((i) => i.name));
6655
- const emitted = [];
6656
- const skipped = [];
6657
- const allImports = [`import type { DynamicModule } from '@nestjs/common';`];
6658
- const allCalls = [];
6659
- for (const name of COMPOSABLE_ORDER) {
6660
- if (!installedNames.has(name)) continue;
6661
- const composer = COMPOSERS[name];
6662
- if (!composer) {
6663
- skipped.push(name);
6664
- continue;
6665
- }
6666
- const cfg = config?.[name] ?? void 0;
6667
- const out = composer({ moduleImport, cfg });
6668
- allImports.push(...out.imports);
6669
- allCalls.push(...out.calls);
6670
- emitted.push(name);
6671
- }
6672
- for (const inst of actable) {
6673
- if (!COMPOSABLE_ORDER.includes(inst.name) && !COMPOSERS[inst.name]) {
6674
- skipped.push(inst.name);
6675
- }
6676
- }
6677
- const exportLine = allCalls.length === 0 ? `export const SUBSYSTEM_MODULES: DynamicModule[] = [];
6678
- ` : `export const SUBSYSTEM_MODULES: DynamicModule[] = [
6679
- ${allCalls.join("\n")}
6680
- ];
6681
- `;
6682
- const body = allImports.join("\n") + "\n\n" + exportLine;
6683
- return { content: HEADER3 + body, emitted, skipped };
6684
- }
6685
- async function regenerateSubsystemBarrel(opts) {
6686
- const { ctx, dryRun = false } = opts;
6687
- const generatedDir = opts.generatedDir ?? resolveGeneratedDir(ctx);
6688
- const mode = resolveRuntimeMode(ctx.config);
6689
- const installed = mode === "package" ? configuredInstalledSubsystems(
6690
- ctx.config
6691
- ) : await detectInstalledSubsystems(ctx);
6692
- const subsystemsAbs = resolveSubsystemsRoot(ctx);
6693
- const barrelAbs = path8.resolve(generatedDir, "subsystems.ts");
6694
- let subsystemsRel = path8.relative(path8.dirname(barrelAbs), subsystemsAbs).split(path8.sep).join("/");
6695
- if (!subsystemsRel.startsWith(".")) subsystemsRel = "./" + subsystemsRel;
6696
- const { content, emitted, skipped } = buildSubsystemBarrel(
6697
- installed,
6698
- ctx.config,
6699
- subsystemsRel,
6700
- mode
6701
- );
6702
- let written = false;
6703
- if (!dryRun) {
6704
- fs5.mkdirSync(path8.dirname(barrelAbs), { recursive: true });
6705
- fs5.writeFileSync(barrelAbs, content);
6706
- written = true;
6707
- }
6708
- return {
6709
- subsystemBarrel: barrelAbs,
6710
- emitted,
6711
- skipped,
6712
- content,
6713
- written
6714
- };
6715
- }
6716
-
6717
- // src/cli/shared/subsystem-schema-generator.ts
6718
- import fs6 from "fs";
6719
- import path9 from "path";
6720
- var PACKAGE3 = "@pattern-stack/codegen";
6721
- var SCHEMA_SYMBOLS = {
6722
- events: ["domainEvents"],
6723
- jobs: [
6724
- "jobs",
6725
- "jobRuns",
6726
- "jobSteps",
6727
- "jobRunStatusEnum",
6728
- "jobStepKindEnum",
6729
- "jobStepStatusEnum",
6730
- "collisionModeEnum",
6731
- "replayFromEnum",
6732
- "parentClosePolicyEnum",
6733
- "waitKindEnum",
6734
- "triggerSourceEnum"
6735
- ],
6736
- bridge: ["bridgeDelivery", "bridgeDeliveryStatusEnum"],
6737
- integration: [
6738
- "integrationSubscriptions",
6739
- "integrationRuns",
6740
- "integrationRunItems",
6741
- "integrationRunDirectionEnum",
6742
- "integrationRunActionEnum",
6743
- "integrationRunStatusEnum",
6744
- "integrationRunItemOperationEnum",
6745
- "integrationRunItemStatusEnum"
6746
- ]
6747
- };
6748
- var SCHEMA_ORDER = ["events", "jobs", "bridge", "integration"];
6749
- var HEADER4 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
6750
- // Subsystem Drizzle schema barrel \u2014 re-exports the tables + pgEnums of every
6751
- // installed subsystem so drizzle-kit emits their CREATE TABLE / CREATE TYPE.
6752
- //
6753
- // Fold into your drizzle-kit schema entrypoint once, alongside the entity
6754
- // schema barrel:
6755
- //
6756
- // // src/schema.ts
6757
- // export * from './generated/schema'; // entity tables
6758
- // export * from './generated/subsystems-schema'; // subsystem tables + enums
6759
- //
6760
- // Regenerated by every \`codegen entity new\` / \`codegen subsystem install\`.
6761
-
6762
- `;
6763
- function schemaImportFor(mode, subsystem, subsystemsRel) {
6764
- return mode === "vendored" ? `${subsystemsRel}/${subsystem}` : `${PACKAGE3}/runtime/subsystems/${subsystem}/index`;
6765
- }
6766
- function buildSubsystemSchemaBarrel(installed, subsystemsRel, mode) {
6767
- const actable = installed.filter((i) => i.status !== "incomplete");
6768
- const installedNames = new Set(actable.map((i) => i.name));
6769
- const emitted = [];
6770
- const lines = [];
6771
- for (const name of SCHEMA_ORDER) {
6772
- if (!installedNames.has(name)) continue;
6773
- const symbols = SCHEMA_SYMBOLS[name];
6774
- if (!symbols || symbols.length === 0) continue;
6775
- const importSpec = schemaImportFor(mode, name, subsystemsRel);
6776
- lines.push(`export { ${symbols.join(", ")} } from '${importSpec}';`);
6777
- emitted.push(name);
6778
- }
6779
- const body = lines.length === 0 ? "export {};\n" : lines.join("\n") + "\n";
6780
- return { content: HEADER4 + body, emitted };
6781
- }
6782
- async function regenerateSubsystemSchemaBarrel(opts) {
6783
- const { ctx, dryRun = false } = opts;
6784
- const generatedDir = opts.generatedDir ?? resolveGeneratedDir(ctx);
6785
- const mode = resolveRuntimeMode(ctx.config);
6786
- const installed = mode === "package" ? configuredInstalledSubsystems(
6787
- ctx.config
6788
- ) : await detectInstalledSubsystems(ctx);
6789
- const subsystemsAbs = resolveSubsystemsRoot(ctx);
6790
- const barrelAbs = path9.resolve(generatedDir, "subsystems-schema.ts");
6791
- let subsystemsRel = path9.relative(path9.dirname(barrelAbs), subsystemsAbs).split(path9.sep).join("/");
6792
- if (!subsystemsRel.startsWith(".")) subsystemsRel = "./" + subsystemsRel;
6793
- const { content, emitted } = buildSubsystemSchemaBarrel(
6794
- installed,
6795
- subsystemsRel,
6796
- mode
6797
- );
6798
- let written = false;
6799
- if (!dryRun) {
6800
- fs6.mkdirSync(path9.dirname(barrelAbs), { recursive: true });
6801
- fs6.writeFileSync(barrelAbs, content);
6802
- written = true;
6803
- }
6804
- return { schemaBarrel: barrelAbs, emitted, content, written };
6805
- }
6806
-
6807
6544
  // src/cli/shared/bridge-registry-generator.ts
6808
- import fs7 from "fs";
6809
- import path10 from "path";
6545
+ import fs5 from "fs";
6546
+ import path8 from "path";
6810
6547
  import ts2 from "typescript";
6811
- var HEADER5 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
6548
+ var PACKAGE_BRIDGE_TYPE_IMPORT = "@pattern-stack/codegen/runtime/subsystems/bridge/index";
6549
+ var OUTPUT_FILE_BY_MODE = {
6550
+ vendored: "registry.ts",
6551
+ package: "bridge-registry.ts"
6552
+ };
6553
+ var HEADER3 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
6812
6554
  // Run \`codegen entity new --all\` to refresh.
6813
6555
  `;
6814
6556
  var DuplicateTriggerError = class extends Error {
@@ -6866,12 +6608,12 @@ var UnknownTriggerEventError = class extends Error {
6866
6608
  name = "UnknownTriggerEventError";
6867
6609
  };
6868
6610
  function findHandlerFiles(dir) {
6869
- if (!fs7.existsSync(dir)) return [];
6611
+ if (!fs5.existsSync(dir)) return [];
6870
6612
  const out = [];
6871
- for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
6613
+ for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
6872
6614
  if (entry.name.startsWith(".")) continue;
6873
6615
  if (entry.name === "node_modules" || entry.name === "generated") continue;
6874
- const full = path10.join(dir, entry.name);
6616
+ const full = path8.join(dir, entry.name);
6875
6617
  if (entry.isDirectory()) {
6876
6618
  out.push(...findHandlerFiles(full));
6877
6619
  } else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
@@ -6934,7 +6676,7 @@ function scanHandlerFiles(handlersDir) {
6934
6676
  const files = findHandlerFiles(handlersDir);
6935
6677
  const out = [];
6936
6678
  for (const filePath of files) {
6937
- const text2 = fs7.readFileSync(filePath, "utf8");
6679
+ const text2 = fs5.readFileSync(filePath, "utf8");
6938
6680
  const sourceFile = ts2.createSourceFile(
6939
6681
  filePath,
6940
6682
  text2,
@@ -6949,9 +6691,9 @@ function scanHandlerFiles(handlersDir) {
6949
6691
  }
6950
6692
  function readKnownEventTypes(eventsGeneratedDir) {
6951
6693
  if (!eventsGeneratedDir) return [];
6952
- const registryPath = path10.join(eventsGeneratedDir, "registry.ts");
6953
- if (!fs7.existsSync(registryPath)) return [];
6954
- const text2 = fs7.readFileSync(registryPath, "utf8");
6694
+ const registryPath = path8.join(eventsGeneratedDir, "registry.ts");
6695
+ if (!fs5.existsSync(registryPath)) return [];
6696
+ const text2 = fs5.readFileSync(registryPath, "utf8");
6955
6697
  const out = /* @__PURE__ */ new Set();
6956
6698
  const re = /^\s*'([a-zA-Z0-9_.-]+)':\s*\{/gm;
6957
6699
  let m;
@@ -6963,9 +6705,9 @@ function readKnownEventTypes(eventsGeneratedDir) {
6963
6705
  function readEventTiers(eventsGeneratedDir) {
6964
6706
  const out = /* @__PURE__ */ new Map();
6965
6707
  if (!eventsGeneratedDir) return out;
6966
- const registryPath = path10.join(eventsGeneratedDir, "registry.ts");
6967
- if (!fs7.existsSync(registryPath)) return out;
6968
- const text2 = fs7.readFileSync(registryPath, "utf8");
6708
+ const registryPath = path8.join(eventsGeneratedDir, "registry.ts");
6709
+ if (!fs5.existsSync(registryPath)) return out;
6710
+ const text2 = fs5.readFileSync(registryPath, "utf8");
6969
6711
  const re = /'([a-zA-Z0-9_.-]+)':\s*\{[^}]*?tier:\s*'(domain|audit)'/g;
6970
6712
  let m;
6971
6713
  while ((m = re.exec(text2)) !== null) {
@@ -7020,11 +6762,11 @@ function validateAgainstEventRegistry(triggers, knownEventTypes) {
7020
6762
  }
7021
6763
  }
7022
6764
  }
7023
- function buildBridgeRegistryContent(triggers) {
6765
+ function buildBridgeRegistryContent(triggers, typeImport = "../bridge.protocol") {
7024
6766
  const chunks = [];
7025
- chunks.push(HEADER5);
6767
+ chunks.push(HEADER3);
7026
6768
  chunks.push("");
7027
- chunks.push(`import type { BridgeRegistry } from '../bridge.protocol';`);
6769
+ chunks.push(`import type { BridgeRegistry } from '${typeImport}';`);
7028
6770
  chunks.push("");
7029
6771
  if (triggers.length === 0) {
7030
6772
  chunks.push(`export const bridgeRegistry: BridgeRegistry = {};`);
@@ -7057,17 +6799,25 @@ function buildBridgeRegistryContent(triggers) {
7057
6799
  chunks.push("");
7058
6800
  return chunks.join("\n");
7059
6801
  }
7060
- var OUTPUT_FILE_NAME = "registry.ts";
7061
6802
  async function generateBridgeRegistry(opts) {
7062
- const { handlersDir, eventsGeneratedDir, outputDir, dryRun = false } = opts;
7063
- const bridgeProtocolPath = path10.resolve(outputDir, "..", "bridge.protocol.ts");
7064
- if (!fs7.existsSync(bridgeProtocolPath)) {
7065
- const strayPath = path10.join(outputDir, OUTPUT_FILE_NAME);
7066
- if (!dryRun && fs7.existsSync(strayPath)) {
7067
- fs7.rmSync(strayPath);
7068
- }
7069
- return {
7070
- outputDir,
6803
+ const {
6804
+ handlersDir,
6805
+ eventsGeneratedDir,
6806
+ outputDir,
6807
+ mode = "vendored",
6808
+ bridgeInstalled = false,
6809
+ dryRun = false
6810
+ } = opts;
6811
+ const outputFileName = OUTPUT_FILE_BY_MODE[mode];
6812
+ const typeImport = mode === "package" ? PACKAGE_BRIDGE_TYPE_IMPORT : "../bridge.protocol";
6813
+ const installed = mode === "package" ? bridgeInstalled : fs5.existsSync(path8.resolve(outputDir, "..", "bridge.protocol.ts"));
6814
+ if (!installed) {
6815
+ const strayPath = path8.join(outputDir, outputFileName);
6816
+ if (!dryRun && fs5.existsSync(strayPath)) {
6817
+ fs5.rmSync(strayPath);
6818
+ }
6819
+ return {
6820
+ outputDir,
7071
6821
  triggerCount: 0,
7072
6822
  triggers: [],
7073
6823
  eventTypeCount: 0,
@@ -7082,16 +6832,16 @@ async function generateBridgeRegistry(opts) {
7082
6832
  validateAgainstEventRegistry(triggers, knownEventTypes);
7083
6833
  const eventTiers = readEventTiers(eventsGeneratedDir);
7084
6834
  validateNoAuditTriggers(triggers, eventTiers);
7085
- const content = buildBridgeRegistryContent(triggers);
6835
+ const content = buildBridgeRegistryContent(triggers, typeImport);
7086
6836
  const file = {
7087
- name: OUTPUT_FILE_NAME,
7088
- outputPath: path10.join(outputDir, OUTPUT_FILE_NAME),
6837
+ name: outputFileName,
6838
+ outputPath: path8.join(outputDir, outputFileName),
7089
6839
  content
7090
6840
  };
7091
6841
  let written = false;
7092
6842
  if (!dryRun) {
7093
- fs7.mkdirSync(outputDir, { recursive: true });
7094
- fs7.writeFileSync(file.outputPath, file.content);
6843
+ fs5.mkdirSync(outputDir, { recursive: true });
6844
+ fs5.writeFileSync(file.outputPath, file.content);
7095
6845
  written = true;
7096
6846
  }
7097
6847
  const eventTypeCount = new Set(triggers.map((t) => t.event)).size;
@@ -7106,1083 +6856,1420 @@ async function generateBridgeRegistry(opts) {
7106
6856
  };
7107
6857
  }
7108
6858
 
7109
- // src/cli/shared/orchestration-generator.ts
7110
- import fs8 from "fs";
7111
- import path11 from "path";
7112
- var OrchestrationEmissionError = class extends Error {
7113
- constructor(issueType, patternName, message) {
7114
- super(`[${issueType}] ${patternName}: ${message}`);
7115
- this.issueType = issueType;
7116
- this.patternName = patternName;
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
+ }
7117
6882
  }
7118
- issueType;
7119
- patternName;
7120
- name = "OrchestrationEmissionError";
7121
- };
7122
- var HEADER6 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
7123
- // See ADR-032 \u2014 Orchestration Patterns.
7124
- `;
7125
- function splitWords(str) {
7126
- return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").split(/\s+/).filter((w) => w.length > 0).map((w) => w.toLowerCase());
7127
- }
7128
- function toKebabCase2(str) {
7129
- return splitWords(str).join("-");
7130
- }
7131
- function toPascalCase2(str) {
7132
- return splitWords(str).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7133
- }
7134
- function toCamelCase(str) {
7135
- const words = splitWords(str);
7136
- if (words.length === 0) return "";
7137
- const [first, ...rest] = words;
7138
- return (first ?? "") + rest.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
6883
+ return issues;
7139
6884
  }
7140
- function toScreamingSnake(str) {
7141
- return splitWords(str).join("_").toUpperCase();
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;
7142
6890
  }
7143
- var FORBIDDEN_KEY_TYPES = /* @__PURE__ */ new Set([
7144
- "string",
7145
- "number",
7146
- "symbol",
7147
- "any",
7148
- "unknown"
7149
- ]);
7150
- function assertEmittable(pattern) {
7151
- const allRegistries2 = [
7152
- pattern.registry,
7153
- ...pattern.coKeyedRegistries ?? []
7154
- ];
7155
- for (const sibling of pattern.coKeyedRegistries ?? []) {
7156
- if (typeof sibling.name !== "string" || sibling.name.length === 0) {
7157
- throw new OrchestrationEmissionError(
7158
- "pattern_cokeyed_missing_name",
7159
- pattern.name,
7160
- `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.`
7161
- );
7162
- }
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 };
7163
6906
  }
7164
- for (const reg of allRegistries2) {
7165
- if (FORBIDDEN_KEY_TYPES.has(reg.keyType)) {
7166
- throw new OrchestrationEmissionError(
7167
- "pattern_keytype_unresolved",
7168
- pattern.name,
7169
- `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.`
7170
- );
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;
7171
6923
  }
7172
- if (typeof reg.keyTypeImport !== "string" || reg.keyTypeImport.length === 0) {
7173
- throw new OrchestrationEmissionError(
7174
- "pattern_missing_import_path",
7175
- pattern.name,
7176
- `registry (keyType '${reg.keyType}') is missing 'keyTypeImport'. Phase 3-2 emission requires every keyType to carry a module specifier.`
7177
- );
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;
7178
6935
  }
7179
- if (typeof reg.valueTypeImport !== "string" || reg.valueTypeImport.length === 0) {
7180
- throw new OrchestrationEmissionError(
7181
- "pattern_missing_import_path",
7182
- pattern.name,
7183
- `registry (valueType '${reg.valueType}') is missing 'valueTypeImport'.`
7184
- );
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;
7185
6945
  }
7186
- for (const e of reg.entries) {
7187
- if (typeof e.providerImport !== "string" || e.providerImport.length === 0) {
7188
- throw new OrchestrationEmissionError(
7189
- "pattern_missing_import_path",
7190
- pattern.name,
7191
- `entry '${e.key}' provider '${e.provider}' is missing 'providerImport'.`
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 };
6959
+ }
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("|")}`
7192
6969
  );
7193
6970
  }
6971
+ payload[key] = { type: typeString, nullable: false };
7194
6972
  }
7195
- }
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
6982
+ };
6983
+ return def;
6984
+ });
7196
6985
  }
7197
- function patternNames(pattern) {
7198
- const slug = toKebabCase2(pattern.name);
7199
- const pascal = toPascalCase2(pattern.name);
7200
- const screaming = toScreamingSnake(pattern.name);
7201
- return { slug, pascal, screaming };
6986
+ function isEventFieldType(s) {
6987
+ return EVENT_FIELD_TYPES.includes(s);
7202
6988
  }
7203
- function registryNames(pattern, reg, isPrimary) {
7204
- const { pascal, screaming } = patternNames(pattern);
7205
- if (isPrimary) {
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") {
7206
6994
  return {
7207
- tokenConst: `${screaming}_REGISTRY`,
7208
- mapType: `${pascal}RegistryMap`,
7209
- method: "select",
7210
- overrideField: "overrides",
7211
- builderFn: `build${pascal}RegistryProviders`
6995
+ protocol: PACKAGE_EVENTS_RUNTIME_IMPORT,
6996
+ tokens: PACKAGE_EVENTS_RUNTIME_IMPORT,
6997
+ errors: PACKAGE_EVENTS_RUNTIME_IMPORT
7212
6998
  };
7213
6999
  }
7214
- const namePascal = toPascalCase2(reg.name);
7215
- const nameScreaming = toScreamingSnake(reg.name);
7216
- const nameCamel = toCamelCase(reg.name);
7217
7000
  return {
7218
- tokenConst: `${screaming}_${nameScreaming}_REGISTRY`,
7219
- mapType: `${pascal}${namePascal}RegistryMap`,
7220
- method: `select${namePascal}`,
7221
- overrideField: `${nameCamel}Overrides`,
7222
- builderFn: `build${pascal}RegistryProviders`
7001
+ protocol: "../event-bus.protocol",
7002
+ tokens: "../events.tokens",
7003
+ errors: "../events-errors"
7223
7004
  };
7224
7005
  }
7225
- function dispatcherClassName(pattern) {
7226
- return pattern.dispatcher?.className ?? `${toPascalCase2(pattern.name)}Dispatcher`;
7006
+ var HEADER4 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
7007
+ // Run \`codegen entity new --all\` to refresh.
7008
+ `;
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("");
7227
7014
  }
7228
- function moduleClassName(pattern) {
7229
- return `${toPascalCase2(pattern.name)}OrchestrationModule`;
7015
+ function toPascalCase2(input) {
7016
+ return input.split("_").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7230
7017
  }
7231
- function forRootOptionsTypeName(pattern) {
7232
- return `${toPascalCase2(pattern.name)}ForRootOptions`;
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];
7041
+ }
7042
+ return field.nullable ? `${base} | null` : base;
7233
7043
  }
7234
- function emitTypeImports(imports) {
7235
- const grouped = /* @__PURE__ */ new Map();
7236
- for (const i of imports) {
7237
- const set = grouped.get(i.specifier) ?? /* @__PURE__ */ new Set();
7238
- set.add(i.name);
7239
- grouped.set(i.specifier, set);
7240
- }
7241
- const out = [];
7242
- for (const specifier of [...grouped.keys()].sort()) {
7243
- const names2 = [...grouped.get(specifier)].sort();
7244
- out.push(`import type { ${names2.join(", ")} } from '${specifier}';`);
7245
- }
7246
- return out;
7247
- }
7248
- function emitValueImports(imports) {
7249
- const grouped = /* @__PURE__ */ new Map();
7250
- for (const i of imports) {
7251
- const set = grouped.get(i.specifier) ?? /* @__PURE__ */ new Set();
7252
- set.add(i.name);
7253
- grouped.set(i.specifier, set);
7254
- }
7255
- const out = [];
7256
- for (const specifier of [...grouped.keys()].sort()) {
7257
- const names2 = [...grouped.get(specifier)].sort();
7258
- out.push(`import { ${names2.join(", ")} } from '${specifier}';`);
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];
7259
7051
  }
7260
- return out;
7052
+ return field.nullable ? `${base}.nullable()` : base;
7261
7053
  }
7262
- function allRegistries(pattern) {
7263
- const list = [];
7264
- list.push({
7265
- reg: pattern.registry,
7266
- names: registryNames(pattern, pattern.registry, true),
7267
- isPrimary: true
7268
- });
7269
- for (const sibling of pattern.coKeyedRegistries ?? []) {
7270
- list.push({
7271
- reg: sibling,
7272
- names: registryNames(pattern, sibling, false),
7273
- isPrimary: false
7274
- });
7275
- }
7276
- return list;
7054
+ function aggregateTypeLiteral(ev) {
7055
+ return ev.aggregate ?? ev.source ?? ev.destination ?? ev.type;
7277
7056
  }
7278
- function buildTokensTs(pattern) {
7279
- const registries = allRegistries(pattern);
7280
- const typeImports = [];
7281
- for (const { reg } of registries) {
7282
- typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
7283
- typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
7284
- }
7285
- const lines = [];
7286
- lines.push(HEADER6.trimEnd());
7287
- lines.push("");
7288
- for (const i of emitTypeImports(typeImports)) lines.push(i);
7289
- lines.push("");
7290
- for (const { names: names2 } of registries) {
7291
- lines.push(`export const ${names2.tokenConst} = Symbol('${names2.tokenConst}');`);
7057
+ function collectEntityEvents(entitiesDir) {
7058
+ const events = [];
7059
+ const issues = [];
7060
+ if (!fs6.existsSync(entitiesDir)) {
7061
+ return { events, issues };
7292
7062
  }
7293
- lines.push("");
7294
- for (const { reg, names: names2 } of registries) {
7295
- lines.push(
7296
- `export type ${names2.mapType} = Map<${reg.keyType}, ${reg.valueType}>;`
7297
- );
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
+ }
7298
7077
  }
7299
- lines.push("");
7300
- return lines.join("\n");
7078
+ return { events, issues };
7301
7079
  }
7302
- function buildProvidersTs(pattern) {
7303
- const registries = allRegistries(pattern);
7304
- const providerImports = [];
7305
- for (const { reg } of registries) {
7306
- for (const e of reg.entries) {
7307
- providerImports.push({ specifier: e.providerImport, name: e.provider });
7308
- }
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);
7309
7085
  }
7310
- const tokenValueImports = registries.map(({ names: names2 }) => names2.tokenConst);
7311
- const mapTypeImports = registries.map(({ names: names2 }) => names2.mapType);
7312
- const typeImports = [];
7313
- for (const { reg } of registries) {
7314
- typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
7315
- typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
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);
7316
7095
  }
7317
- const optsType = forRootOptionsTypeName(pattern);
7318
- const lines = [];
7319
- lines.push(HEADER6.trimEnd());
7320
- lines.push("");
7321
- lines.push(`import type { Provider } from '@nestjs/common';`);
7322
- for (const i of emitTypeImports(typeImports)) lines.push(i);
7323
- for (const i of emitValueImports(providerImports)) lines.push(i);
7324
- const tokenLine = [
7325
- ...tokenValueImports.sort(),
7326
- ...mapTypeImports.sort().map((m) => `type ${m}`)
7327
- ].join(", ");
7328
- lines.push(`import { ${tokenLine} } from './tokens.js';`);
7329
- lines.push(`import type { ${optsType} } from './module.js';`);
7330
- lines.push("");
7331
- const { pascal } = patternNames(pattern);
7332
- lines.push(
7333
- `export function build${pascal}RegistryProviders(opts?: ${optsType}): Provider[] {`
7096
+ const events = Array.from(byType.values()).sort(
7097
+ (a, b) => a.type.localeCompare(b.type)
7334
7098
  );
7335
- lines.push(" return [");
7336
- for (const { reg, names: names2 } of registries) {
7337
- const factoryArgs = reg.entries.map((e, i) => `${camelArg(e.provider, i)}: ${e.provider}`).join(", ");
7338
- lines.push(" {");
7339
- lines.push(` provide: ${names2.tokenConst},`);
7340
- lines.push(` useFactory: (${factoryArgs}) => {`);
7341
- lines.push(` const base: ${names2.mapType} = new Map<${reg.keyType}, ${reg.valueType}>([`);
7342
- for (const [i, e] of reg.entries.entries()) {
7343
- lines.push(
7344
- ` ['${e.key}' as ${reg.keyType}, ${camelArg(e.provider, i)}],`
7345
- );
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);
7346
7109
  }
7347
- lines.push(" ]);");
7348
- lines.push(` if (opts?.${names2.overrideField}) {`);
7349
- lines.push(` for (const [k, v] of Object.entries(opts.${names2.overrideField})) {`);
7350
- lines.push(` if (v !== undefined) base.set(k as ${reg.keyType}, v as ${reg.valueType});`);
7351
- lines.push(" }");
7352
- lines.push(" }");
7353
- lines.push(` return base as ${names2.mapType};`);
7354
- lines.push(" },");
7355
- const injectList = reg.entries.map((e) => e.provider).join(", ");
7356
- lines.push(` inject: [${injectList}],`);
7357
- lines.push(" },");
7358
7110
  }
7359
- lines.push(" ];");
7360
- lines.push("}");
7361
- lines.push("");
7362
- return lines.join("\n");
7363
- }
7364
- function camelArg(provider, index2) {
7365
- const camel = provider.charAt(0).toLowerCase() + provider.slice(1);
7366
- return `${camel}_${index2}`;
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 };
7367
7123
  }
7368
- function buildDispatcherTs(pattern) {
7369
- const registries = allRegistries(pattern);
7370
- const className = dispatcherClassName(pattern);
7371
- const errorClass = `${className}Error`;
7372
- const typeImports = [];
7373
- for (const { reg } of registries) {
7374
- typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
7375
- typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
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}';
7130
+
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
+ `;
7376
7137
  }
7377
- const providerImports = [];
7378
- for (const { reg } of registries) {
7379
- for (const e of reg.entries) {
7380
- providerImports.push({ specifier: e.providerImport, name: e.provider });
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} */`);
7148
+ }
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)};`);
7164
+ }
7165
+ chunks.push(` };`);
7381
7166
  }
7167
+ chunks.push(`}`);
7168
+ chunks.push("");
7382
7169
  }
7383
- const tokenValues = registries.map(({ names: names2 }) => names2.tokenConst);
7384
- const mapTypes = registries.map(({ names: names2 }) => names2.mapType);
7385
- const lines = [];
7386
- lines.push(HEADER6.trimEnd());
7387
- lines.push("");
7388
- lines.push(`import { Inject, Injectable } from '@nestjs/common';`);
7389
- for (const i of emitTypeImports(typeImports)) lines.push(i);
7390
- for (const i of emitValueImports(providerImports)) lines.push(i);
7391
- const tokenLine = [
7392
- ...tokenValues.sort(),
7393
- ...mapTypes.sort().map((m) => `type ${m}`)
7394
- ].join(", ");
7395
- lines.push(`import { ${tokenLine} } from './tokens.js';`);
7396
- lines.push("");
7397
- lines.push(`export class ${errorClass} extends Error {}`);
7398
- lines.push("");
7399
- lines.push("@Injectable()");
7400
- lines.push(`export class ${className} {`);
7401
- lines.push(" constructor(");
7402
- for (const { names: names2 } of registries) {
7403
- const fieldName = names2.method === "select" ? "registry" : `${camelFromMethod(names2.method)}Registry`;
7404
- lines.push(
7405
- ` @Inject(${names2.tokenConst}) protected readonly ${fieldName}: ${names2.mapType},`
7406
- );
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");
7183
+ }
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
+ `;
7407
7193
  }
7408
- lines.push(" ) {}");
7409
- lines.push("");
7410
- for (const { reg, names: names2 } of registries) {
7411
- const fieldName = names2.method === "select" ? "registry" : `${camelFromMethod(names2.method)}Registry`;
7412
- for (const e of reg.entries) {
7413
- lines.push(` ${names2.method}(key: '${e.key}'): ${e.provider};`);
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;
7414
7207
  }
7415
- lines.push(` ${names2.method}(key: ${reg.keyType}): ${reg.valueType};`);
7416
- lines.push(` ${names2.method}(key: ${reg.keyType}): ${reg.valueType} {`);
7417
- lines.push(` const entry = this.${fieldName}.get(key);`);
7418
- lines.push(
7419
- ` if (!entry) throw new ${errorClass}(\`Unknown ${reg.keyType}: \${String(key)}\`);`
7420
- );
7421
- lines.push(` return entry;`);
7422
- lines.push(" }");
7423
- lines.push("");
7424
- }
7425
- if (pattern.dispatcher?.assemblySlot) {
7426
- lines.push(
7427
- ` // ADR-032 Decision 5 \u2014 \`assemblySlot: '${pattern.dispatcher.assemblySlot}'\` is a`
7428
- );
7429
- lines.push(
7430
- ` // subclass contract, not emitted here. Consumers subclass and add the`
7431
- );
7432
- lines.push(
7433
- ` // \`${pattern.dispatcher.assemblySlot}(...)\` method themselves.`
7434
- );
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("");
7435
7216
  }
7436
- lines.push("}");
7437
- lines.push("");
7438
- return lines.join("\n");
7439
- }
7440
- function camelFromMethod(method) {
7441
- if (method.startsWith("select") && method.length > "select".length) {
7442
- const tail = method.slice("select".length);
7443
- return tail.charAt(0).toLowerCase() + tail.slice(1);
7217
+ chunks.push(`export const eventPayloadSchemas = {`);
7218
+ for (const ev of sorted) {
7219
+ chunks.push(` '${ev.type}': ${toCamelCase(ev.type)}PayloadSchema,`);
7444
7220
  }
7445
- return method;
7221
+ chunks.push(`} as const satisfies Record<EventTypeName, z.ZodType>;`);
7222
+ chunks.push("");
7223
+ return chunks.join("\n");
7446
7224
  }
7447
- function buildModuleTs(pattern) {
7448
- const registries = allRegistries(pattern);
7449
- const className = dispatcherClassName(pattern);
7450
- const moduleName = moduleClassName(pattern);
7451
- const optsType = forRootOptionsTypeName(pattern);
7452
- const { pascal } = patternNames(pattern);
7453
- const typeImports = [];
7454
- for (const { reg } of registries) {
7455
- typeImports.push({ specifier: reg.keyTypeImport, name: reg.keyType });
7456
- typeImports.push({ specifier: reg.valueTypeImport, name: reg.valueType });
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");
7457
7264
  }
7458
- const tokenValues = registries.map(({ names: names2 }) => names2.tokenConst);
7459
- const lines = [];
7460
- lines.push(HEADER6.trimEnd());
7461
- lines.push("");
7462
- lines.push(`import { type DynamicModule, Module } from '@nestjs/common';`);
7463
- for (const i of emitTypeImports(typeImports)) lines.push(i);
7464
- lines.push(`import { ${className} } from './dispatcher.js';`);
7465
- lines.push(`import { build${pascal}RegistryProviders } from './registry.providers.js';`);
7466
- lines.push(`import { ${tokenValues.sort().join(", ")} } from './tokens.js';`);
7467
- lines.push("");
7468
- lines.push(`export interface ${optsType} {`);
7469
- for (const { reg, names: names2 } of registries) {
7470
- lines.push(
7471
- ` ${names2.overrideField}?: Partial<Record<${reg.keyType}, ${reg.valueType}>>;`
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}' },`
7472
7290
  );
7291
+ chunks.push(` },`);
7473
7292
  }
7474
- lines.push("}");
7475
- lines.push("");
7476
- lines.push("@Module({})");
7477
- lines.push(`export class ${moduleName} {`);
7478
- lines.push(` static forRoot(opts?: ${optsType}): DynamicModule {`);
7479
- lines.push(" return {");
7480
- lines.push(` module: ${moduleName},`);
7481
- lines.push(
7482
- ` providers: [...build${pascal}RegistryProviders(opts), ${className}],`
7293
+ chunks.push(
7294
+ `} as const satisfies Record<EventTypeName, EventMetadata>;`
7483
7295
  );
7484
- const exportItems = [...tokenValues.sort(), className];
7485
- lines.push(` exports: [${exportItems.join(", ")}],`);
7486
- lines.push(" };");
7487
- lines.push(" }");
7488
- lines.push("}");
7489
- lines.push("");
7490
- return lines.join("\n");
7296
+ chunks.push("");
7297
+ chunks.push(REGISTRY_GETTER);
7298
+ chunks.push("");
7299
+ return chunks.join("\n");
7491
7300
  }
7492
- function buildIndexTs(pattern) {
7493
- const lines = [];
7494
- lines.push(HEADER6.trimEnd());
7495
- lines.push("");
7496
- lines.push(`export * from './tokens.js';`);
7497
- lines.push(`export * from './dispatcher.js';`);
7498
- lines.push(`export * from './module.js';`);
7499
- lines.push(`export * from './registry.providers.js';`);
7500
- lines.push("");
7501
- return lines.join("\n");
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
+ }
7427
+ }
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}'`);
7433
+ }
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;
7474
+ }
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
7499
+ );
7500
+ return `{ ${entries.map(([k, v]) => `${k}: ${jsonToTs(v)}`).join(", ")} }`;
7501
+ }
7502
+ return "undefined";
7503
+ }
7504
+ function quoteBullmqDomainOpts(input) {
7505
+ const { backend, multiTenant, bullExt } = input;
7506
+ if (backend !== "bullmq" || !bullExt) {
7507
+ return quoteOpts({ backend, multiTenant });
7508
+ }
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}]`;
7519
+ }
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")}';`
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(", ")} }),`);
7571
+ }
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
+ };
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
+ };
7607
+ }
7608
+ };
7609
+ var PACKAGE2 = "@pattern-stack/codegen";
7610
+ function makeModuleImport(mode, subsystemsRel) {
7611
+ if (mode === "vendored") {
7612
+ return (subsystem, moduleBasename) => `${subsystemsRel}/${subsystem}/${moduleBasename}`;
7613
+ }
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;
7645
+ }
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);
7651
+ }
7652
+ for (const inst of actable) {
7653
+ if (!COMPOSABLE_ORDER.includes(inst.name) && !COMPOSERS[inst.name]) {
7654
+ skipped.push(inst.name);
7655
+ }
7656
+ }
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 };
7664
+ }
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
7681
+ );
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
+ };
7713
+ }
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`;
7502
7763
  }
7503
- 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 = [];
7504
7768
  const lines = [];
7505
- lines.push(HEADER6.trimEnd());
7506
- lines.push("");
7507
- if (patterns.length === 0) {
7508
- lines.push("// No orchestration patterns registered.");
7509
- lines.push("export {};");
7510
- lines.push("");
7511
- return lines.join("\n");
7512
- }
7513
- const slugs = patterns.map((p) => toKebabCase2(p.name)).sort();
7514
- for (const slug of slugs) {
7515
- 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);
7516
7776
  }
7517
- lines.push("");
7518
- return lines.join("\n");
7519
- }
7520
- function buildPatternFiles(pattern, outputRoot) {
7521
- assertEmittable(pattern);
7522
- const slug = toKebabCase2(pattern.name);
7523
- const outputDir = path11.join(outputRoot, slug);
7524
- const make = (name, content) => ({
7525
- name,
7526
- outputPath: path11.join(outputDir, name),
7527
- relativePath: path11.join(slug, name),
7528
- content
7529
- });
7530
- const files = [
7531
- make("tokens.ts", buildTokensTs(pattern)),
7532
- make("registry.providers.ts", buildProvidersTs(pattern)),
7533
- make("dispatcher.ts", buildDispatcherTs(pattern)),
7534
- make("module.ts", buildModuleTs(pattern)),
7535
- make("index.ts", buildIndexTs(pattern))
7536
- ];
7537
- return {
7538
- patternName: pattern.name,
7539
- slug,
7540
- outputDir,
7541
- files
7542
- };
7777
+ const body = lines.length === 0 ? "export {};\n" : lines.join("\n") + "\n";
7778
+ return { content: HEADER6 + body, emitted };
7543
7779
  }
7544
- function generateOrchestrationModules(opts) {
7545
- const { patterns, outputRoot, dryRun = false } = opts;
7546
- const perPattern = [];
7547
- const allFiles = [];
7548
- for (const pattern of patterns) {
7549
- const result = buildPatternFiles(pattern, outputRoot);
7550
- perPattern.push(result);
7551
- allFiles.push(...result.files);
7552
- }
7553
- const rootBarrel = {
7554
- name: "index.ts",
7555
- outputPath: path11.join(outputRoot, "index.ts"),
7556
- relativePath: "index.ts",
7557
- content: buildRootBarrelTs(patterns)
7558
- };
7559
- 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
+ );
7560
7796
  let written = false;
7561
- if (!dryRun && patterns.length > 0) {
7562
- fs8.mkdirSync(outputRoot, { recursive: true });
7563
- for (const r of perPattern) {
7564
- fs8.mkdirSync(r.outputDir, { recursive: true });
7565
- for (const f of r.files) {
7566
- fs8.writeFileSync(f.outputPath, f.content);
7567
- }
7568
- }
7569
- fs8.writeFileSync(rootBarrel.outputPath, rootBarrel.content);
7797
+ if (!dryRun) {
7798
+ fs8.mkdirSync(path11.dirname(barrelAbs), { recursive: true });
7799
+ fs8.writeFileSync(barrelAbs, content);
7570
7800
  written = true;
7571
7801
  }
7572
- return {
7573
- outputRoot,
7574
- patterns: perPattern,
7575
- files: allFiles,
7576
- written
7577
- };
7802
+ return { schemaBarrel: barrelAbs, emitted, content, written };
7578
7803
  }
7579
7804
 
7580
- // src/cli/shared/event-codegen-generator.ts
7805
+ // src/cli/shared/orchestration-generator.ts
7581
7806
  import fs9 from "fs";
7582
7807
  import path12 from "path";
7583
-
7584
- // src/parser/load-events.ts
7585
- import { basename as basename2, resolve as resolve4 } from "path";
7586
- function loadErrorToIssue2(error) {
7587
- const issues = [];
7588
- issues.push({
7589
- severity: "error",
7590
- type: "parse_error",
7591
- message: error.error,
7592
- path: error.filePath
7593
- });
7594
- if (error.details) {
7595
- for (const detail of error.details) {
7596
- issues.push({
7597
- severity: "error",
7598
- type: "schema_error",
7599
- message: detail,
7600
- path: error.filePath
7601
- });
7602
- }
7808
+ var OrchestrationEmissionError = class extends Error {
7809
+ constructor(issueType, patternName, message) {
7810
+ super(`[${issueType}] ${patternName}: ${message}`);
7811
+ this.issueType = issueType;
7812
+ this.patternName = patternName;
7603
7813
  }
7604
- 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());
7605
7823
  }
7606
- function stripYamlExt(file) {
7607
- const base = basename2(file);
7608
- if (base.endsWith(".yaml")) return base.slice(0, -".yaml".length);
7609
- if (base.endsWith(".yml")) return base.slice(0, -".yml".length);
7610
- return base;
7824
+ function toKebabCase2(str) {
7825
+ return splitWords(str).join("-");
7611
7826
  }
7612
- function loadEvents(eventsDir, entityNames) {
7613
- const events = [];
7614
- const issues = [];
7615
- const resolvedDir = resolve4(eventsDir);
7616
- let files;
7617
- try {
7618
- files = findYamlFiles(resolvedDir);
7619
- } catch {
7620
- issues.push({
7621
- severity: "warning",
7622
- type: "no_events_dir",
7623
- message: `No events directory found at: ${resolvedDir}`,
7624
- path: resolvedDir
7625
- });
7626
- return { events, issues };
7627
- }
7628
- if (files.length === 0) {
7629
- issues.push({
7630
- severity: "warning",
7631
- type: "no_files",
7632
- message: `No event YAML files found in directory: ${resolvedDir}`,
7633
- path: resolvedDir
7634
- });
7635
- return { events, issues };
7636
- }
7637
- const entityNameSet = new Set(entityNames);
7638
- const seenTypes = /* @__PURE__ */ new Map();
7639
- for (const filePath of files) {
7640
- const result = loadEventFromYaml(filePath);
7641
- if (!result.success) {
7642
- issues.push(...loadErrorToIssue2(result));
7643
- continue;
7644
- }
7645
- const { definition } = result;
7646
- const baseName = stripYamlExt(filePath);
7647
- if (baseName !== definition.type) {
7648
- issues.push({
7649
- severity: "error",
7650
- type: "event_filename_mismatch",
7651
- message: `Event file '${baseName}' must contain 'type: ${baseName}' (found 'type: ${definition.type}')`,
7652
- path: filePath,
7653
- suggestion: `Rename the file to '${definition.type}.yaml' or fix the 'type' field to '${baseName}'`
7654
- });
7655
- continue;
7656
- }
7657
- if (definition.direction === "change" && definition.aggregate !== void 0 && !entityNameSet.has(definition.aggregate)) {
7658
- issues.push({
7659
- severity: "error",
7660
- type: "unknown_aggregate",
7661
- message: `change event '${definition.type}' references unknown aggregate entity '${definition.aggregate}'`,
7662
- path: filePath,
7663
- suggestion: `Define entities/${definition.aggregate}.yaml or fix the aggregate value`
7664
- });
7665
- continue;
7666
- }
7667
- if (seenTypes.has(definition.type)) {
7668
- issues.push({
7669
- severity: "error",
7670
- type: "duplicate_event_type",
7671
- message: `Duplicate event type '${definition.type}' (already declared in ${seenTypes.get(definition.type)})`,
7672
- path: filePath
7673
- });
7674
- 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
+ );
7675
7858
  }
7676
- seenTypes.set(definition.type, filePath);
7677
- events.push(definition);
7678
7859
  }
7679
- return { events, issues };
7680
- }
7681
- function desugarEntityEvents(entity) {
7682
- const entityName = entity.entity.name;
7683
- const entityEvents = entity.events ?? [];
7684
- return entityEvents.map((ev) => {
7685
- const payload = {};
7686
- for (const [key, typeString] of Object.entries(ev.body)) {
7687
- if (!isEventFieldType(typeString)) {
7688
- throw new Error(
7689
- `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'.`
7690
7888
  );
7691
7889
  }
7692
- payload[key] = { type: typeString, nullable: false };
7693
7890
  }
7694
- const def = {
7695
- type: ev.name,
7696
- tier: "domain",
7697
- direction: "change",
7698
- aggregate: entityName,
7699
- payload,
7700
- retry: { attempts: 3, backoff: "exponential" },
7701
- version: 1,
7702
- pool: DIRECTION_TO_POOL.change
7703
- };
7704
- return def;
7705
- });
7706
- }
7707
- function isEventFieldType(s) {
7708
- return EVENT_FIELD_TYPES.includes(s);
7709
- }
7710
-
7711
- // src/cli/shared/event-codegen-generator.ts
7712
- var HEADER7 = `// AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
7713
- // Run \`codegen entity new --all\` to refresh.
7714
- `;
7715
- function toCamelCase2(input) {
7716
- const parts = input.split("_").filter(Boolean);
7717
- if (parts.length === 0) return input;
7718
- const [first, ...rest] = parts;
7719
- return (first ?? "") + rest.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
7891
+ }
7720
7892
  }
7721
- function toPascalCase3(input) {
7722
- 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 };
7723
7898
  }
7724
- var TS_TYPE_BY_FIELD = {
7725
- uuid: "string",
7726
- string: "string",
7727
- number: "number",
7728
- boolean: "boolean",
7729
- date: "Date",
7730
- json: "Record<string, unknown>"
7731
- };
7732
- var ZOD_EXPR_BY_FIELD = {
7733
- uuid: "z.string().uuid()",
7734
- string: "z.string()",
7735
- number: "z.number()",
7736
- boolean: "z.boolean()",
7737
- date: "z.coerce.date()",
7738
- json: "z.record(z.unknown())"
7739
- };
7740
- function tsTypeForField(field) {
7741
- let base;
7742
- if (field.type === "array") {
7743
- const itemType = field.items;
7744
- base = `${TS_TYPE_BY_FIELD[itemType]}[]`;
7745
- } else {
7746
- 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
+ };
7747
7909
  }
7748
- 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
+ };
7749
7920
  }
7750
- function zodExprForField(field) {
7751
- let base;
7752
- if (field.type === "array") {
7753
- const itemType = field.items;
7754
- base = `z.array(${ZOD_EXPR_BY_FIELD[itemType]})`;
7755
- } else {
7756
- base = ZOD_EXPR_BY_FIELD[field.type];
7757
- }
7758
- return field.nullable ? `${base}.nullable()` : base;
7921
+ function dispatcherClassName(pattern) {
7922
+ return pattern.dispatcher?.className ?? `${toPascalCase3(pattern.name)}Dispatcher`;
7759
7923
  }
7760
- function aggregateTypeLiteral(ev) {
7761
- return ev.aggregate ?? ev.source ?? ev.destination ?? ev.type;
7924
+ function moduleClassName(pattern) {
7925
+ return `${toPascalCase3(pattern.name)}OrchestrationModule`;
7762
7926
  }
7763
- function collectEntityEvents(entitiesDir) {
7764
- const events = [];
7765
- const issues = [];
7766
- if (!fs9.existsSync(entitiesDir)) {
7767
- 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);
7768
7936
  }
7769
- const files = findYamlFiles(entitiesDir);
7770
- for (const filePath of files) {
7771
- const result = loadEntityFromYaml(filePath);
7772
- if (!result.success) continue;
7773
- try {
7774
- events.push(...desugarEntityEvents(result.definition));
7775
- } catch (err) {
7776
- issues.push({
7777
- severity: "error",
7778
- type: "entity_event_desugar_failed",
7779
- message: err instanceof Error ? err.message : String(err),
7780
- path: filePath
7781
- });
7782
- }
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}';`);
7783
7941
  }
7784
- return { events, issues };
7942
+ return out;
7785
7943
  }
7786
- function mergeEvents(topLevel, entitySugar) {
7787
- const issues = [];
7788
- const byType = /* @__PURE__ */ new Map();
7789
- for (const ev of entitySugar) {
7790
- 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);
7791
7950
  }
7792
- for (const ev of topLevel) {
7793
- if (byType.has(ev.type)) {
7794
- issues.push({
7795
- severity: "warning",
7796
- type: "event_merge_override",
7797
- 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`
7798
- });
7799
- }
7800
- 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}';`);
7801
7955
  }
7802
- const events = Array.from(byType.values()).sort(
7803
- (a, b) => a.type.localeCompare(b.type)
7804
- );
7805
- return { events, issues };
7956
+ return out;
7806
7957
  }
7807
- function collectMergedEvents(opts) {
7808
- const { entitiesDir, eventsDir } = opts;
7809
- const entityNames = [];
7810
- if (fs9.existsSync(entitiesDir)) {
7811
- const entityFiles = findYamlFiles(entitiesDir);
7812
- for (const f of entityFiles) {
7813
- const result = loadEntityFromYaml(f);
7814
- if (result.success) entityNames.push(result.definition.entity.name);
7815
- }
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
+ });
7816
7971
  }
7817
- const topLevelResult = loadEvents(eventsDir, entityNames);
7818
- const { events: entitySugar, issues: sugarIssues } = collectEntityEvents(entitiesDir);
7819
- const { events: merged, issues: mergeIssues } = mergeEvents(
7820
- topLevelResult.events,
7821
- entitySugar
7822
- );
7823
- const issues = [
7824
- ...topLevelResult.issues,
7825
- ...sugarIssues,
7826
- ...mergeIssues
7827
- ];
7828
- return { events: merged, issues };
7972
+ return list;
7829
7973
  }
7830
- function buildTypesContent(events) {
7831
- const sorted = [...events].sort((a, b) => a.type.localeCompare(b.type));
7832
- if (sorted.length === 0) {
7833
- return HEADER7 + `
7834
- import type { DomainEvent } from '../event-bus.protocol';
7835
-
7836
- export type AppDomainEvent = never;
7837
-
7838
- export type EventTypeName = string;
7839
- export type EventOfType<T extends EventTypeName> = DomainEvent;
7840
- export type PayloadOfType<T extends EventTypeName> = DomainEvent['payload'];
7841
- `;
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 });
7842
7980
  }
7843
- const chunks = [];
7844
- chunks.push(HEADER7);
7845
- chunks.push("");
7846
- chunks.push(`import type { DomainEvent } from '../event-bus.protocol';`);
7847
- chunks.push("");
7848
- for (const ev of sorted) {
7849
- const interfaceName = `${toPascalCase3(ev.type)}Event`;
7850
- const aggregateLit = aggregateTypeLiteral(ev);
7851
- if (ev.description) {
7852
- chunks.push(`/** ${ev.description} */`);
7853
- }
7854
- chunks.push(`export interface ${interfaceName} extends DomainEvent {`);
7855
- chunks.push(` readonly type: '${ev.type}';`);
7856
- chunks.push(` readonly aggregateType: '${aggregateLit}';`);
7857
- const payloadKeys = Object.keys(ev.payload).sort();
7858
- if (payloadKeys.length === 0) {
7859
- chunks.push(` readonly payload: Record<string, never>;`);
7860
- } else {
7861
- chunks.push(` readonly payload: {`);
7862
- for (const key of payloadKeys) {
7863
- const field = ev.payload[key];
7864
- if (!field) continue;
7865
- if (field.description) {
7866
- chunks.push(` /** ${field.description} */`);
7867
- }
7868
- chunks.push(` ${toCamelCase2(key)}: ${tsTypeForField(field)};`);
7869
- }
7870
- 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 });
7871
8004
  }
7872
- chunks.push(`}`);
7873
- chunks.push("");
7874
8005
  }
7875
- const unionMembers = sorted.map((ev) => `${toPascalCase3(ev.type)}Event`);
7876
- chunks.push(`export type AppDomainEvent =`);
7877
- chunks.push(` | ${unionMembers.join("\n | ")};`);
7878
- chunks.push("");
7879
- chunks.push(`export type EventTypeName = AppDomainEvent['type'];`);
7880
- chunks.push(
7881
- `export type EventOfType<T extends EventTypeName> = Extract<AppDomainEvent, { type: T }>;`
7882
- );
7883
- chunks.push(
7884
- `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[] {`
7885
8030
  );
7886
- chunks.push("");
7887
- 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");
7888
8059
  }
7889
- function buildSchemasContent(events) {
7890
- const sorted = [...events].sort((a, b) => a.type.localeCompare(b.type));
7891
- if (sorted.length === 0) {
7892
- return HEADER7 + `
7893
- import { z } from 'zod';
7894
- import type { EventTypeName } from './types';
7895
-
7896
- export const eventPayloadSchemas = {} as Record<EventTypeName, z.ZodType>;
7897
- `;
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 });
7898
8072
  }
7899
- const chunks = [];
7900
- chunks.push(HEADER7);
7901
- chunks.push("");
7902
- chunks.push(`import { z } from 'zod';`);
7903
- chunks.push(`import type { EventTypeName } from './types';`);
7904
- chunks.push("");
7905
- for (const ev of sorted) {
7906
- const schemaConst = `${toCamelCase2(ev.type)}PayloadSchema`;
7907
- const payloadKeys = Object.keys(ev.payload).sort();
7908
- if (payloadKeys.length === 0) {
7909
- chunks.push(`export const ${schemaConst} = z.object({}).strict();`);
7910
- chunks.push("");
7911
- 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 });
7912
8077
  }
7913
- chunks.push(`export const ${schemaConst} = z.object({`);
7914
- for (const key of payloadKeys) {
7915
- const field = ev.payload[key];
7916
- if (!field) continue;
7917
- 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};`);
7918
8110
  }
7919
- chunks.push(`}).strict();`);
7920
- 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("");
7921
8120
  }
7922
- chunks.push(`export const eventPayloadSchemas = {`);
7923
- for (const ev of sorted) {
7924
- 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
+ );
7925
8131
  }
7926
- chunks.push(`} as const satisfies Record<EventTypeName, z.ZodType>;`);
7927
- chunks.push("");
7928
- return chunks.join("\n");
8132
+ lines.push("}");
8133
+ lines.push("");
8134
+ return lines.join("\n");
7929
8135
  }
7930
- var REGISTRY_INTERFACE = [
7931
- "export interface EventMetadata {",
7932
- " type: EventTypeName;",
7933
- " tier: 'domain' | 'audit';",
7934
- " direction: 'inbound' | 'change' | 'outbound' | null;",
7935
- " pool: 'events_inbound' | 'events_change' | 'events_outbound' | null;",
7936
- " aggregate?: string;",
7937
- " source?: string;",
7938
- " destination?: string;",
7939
- " version: number;",
7940
- " retry: { attempts: number; backoff: 'linear' | 'exponential' };",
7941
- "}"
7942
- ].join("\n");
7943
- var REGISTRY_GETTER = [
7944
- "export function getEventMetadata<T extends EventTypeName>(type: T): EventMetadata {",
7945
- " const meta = eventRegistry[type];",
7946
- " if (!meta) {",
7947
- " throw new Error(`No registry entry for event type '${String(type)}' \u2014 declare events under events/*.yaml and re-run \\`codegen entity new --all\\`.`);",
7948
- " }",
7949
- " return meta;",
7950
- "}"
7951
- ].join("\n");
7952
- function buildRegistryContent(events) {
7953
- const sorted = [...events].sort((a, b) => a.type.localeCompare(b.type));
7954
- const chunks = [];
7955
- chunks.push(HEADER7);
7956
- chunks.push("");
7957
- chunks.push(`import type { EventTypeName } from './types';`);
7958
- chunks.push("");
7959
- chunks.push(REGISTRY_INTERFACE);
7960
- chunks.push("");
7961
- if (sorted.length === 0) {
7962
- chunks.push(
7963
- `export const eventRegistry = {} as Record<EventTypeName, EventMetadata>;`
7964
- );
7965
- chunks.push("");
7966
- chunks.push(REGISTRY_GETTER);
7967
- chunks.push("");
7968
- 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);
7969
8140
  }
7970
- chunks.push(`export const eventRegistry = {`);
7971
- for (const ev of sorted) {
7972
- const tier = ev.tier ?? "domain";
7973
- chunks.push(` '${ev.type}': {`);
7974
- chunks.push(` type: '${ev.type}',`);
7975
- chunks.push(` tier: '${tier}',`);
7976
- if (tier === "audit") {
7977
- chunks.push(` direction: null,`);
7978
- chunks.push(` pool: null,`);
7979
- } else {
7980
- chunks.push(` direction: '${ev.direction}',`);
7981
- chunks.push(` pool: '${ev.pool}',`);
7982
- }
7983
- if (ev.aggregate !== void 0) {
7984
- chunks.push(` aggregate: '${ev.aggregate}',`);
7985
- }
7986
- if (ev.source !== void 0) {
7987
- chunks.push(` source: '${ev.source}',`);
7988
- }
7989
- if (ev.destination !== void 0) {
7990
- chunks.push(` destination: '${ev.destination}',`);
7991
- }
7992
- chunks.push(` version: ${ev.version},`);
7993
- chunks.push(
7994
- ` 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}>>;`
7995
8168
  );
7996
- chunks.push(` },`);
7997
8169
  }
7998
- chunks.push(
7999
- `} 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}],`
8000
8179
  );
8001
- chunks.push("");
8002
- chunks.push(REGISTRY_GETTER);
8003
- chunks.push("");
8004
- return chunks.join("\n");
8005
- }
8006
- var BUS_BODY = `import { Injectable, Inject } from '@nestjs/common';
8007
- import { randomUUID } from 'crypto';
8008
- import { EVENT_BUS, EVENTS_MULTI_TENANT } from '../events.tokens';
8009
- import { MissingTenantIdError } from '../events-errors';
8010
- import type { IEventBus, DrizzleTransaction } from '../event-bus.protocol';
8011
- import { eventPayloadSchemas } from './schemas';
8012
- import { getEventMetadata } from './registry';
8013
- import type { EventTypeName, EventOfType, PayloadOfType } from './types';
8014
-
8015
- /**
8016
- * Typed facade over IEventBus.
8017
- *
8018
- * Stamps \`pool\`, \`direction\`, \`tier\`, and \`version\` into \`event.metadata\`
8019
- * from the generated \`eventRegistry\` before delegating to
8020
- * \`IEventBus.publish()\`. Downstream backends (DrizzleEventBus) read those
8021
- * values to populate the explicit \`domain_events\` columns.
8022
- *
8023
- * Tier stamping (AUDIT-3): every event carries \`metadata.tier\`, sourced
8024
- * from the registry. For \`tier: 'audit'\` events, the bus FORCES
8025
- * \`metadata.pool = null\` and \`metadata.direction = null\` regardless of
8026
- * any caller-supplied values in \`opts.metadata\` \u2014 audit routing is
8027
- * bus-stamped, not caller-controlled. Caller overrides are silently
8028
- * dropped with a debug-level log (callers should not be specifying these
8029
- * for audit events; see ai-docs/specs/issue-242/plan.md \xA7AUDIT-3).
8030
- *
8031
- * Validation gating (EVT-Q5): \`CODEGEN_EVENT_VALIDATE\` env flag, default on.
8032
- * Uses \`safeParse\` + \`console.warn\` \u2014 never throws, so a bad publish does
8033
- * not crash a hot path.
8034
- *
8035
- * Multi-tenancy (EVT-6): when the EventsModule is configured with
8036
- * \`multiTenant: true\`, every publish must supply \`opts.metadata.tenantId\`
8037
- * \u2014 otherwise \`publish()\` throws \`MissingTenantIdError\`. When \`multiTenant\`
8038
- * is \`false\` (default), no tenantId is required. If a tenantId IS supplied,
8039
- * it is preserved on \`event.metadata\` and the Drizzle backend writes it to
8040
- * \`domain_events.tenant_id\` (EVT-4).
8041
- */
8042
- @Injectable()
8043
- export class TypedEventBus {
8044
- constructor(
8045
- @Inject(EVENT_BUS) private readonly bus: IEventBus,
8046
- @Inject(EVENTS_MULTI_TENANT) private readonly multiTenant: boolean,
8047
- ) {}
8048
-
8049
- async publish<T extends EventTypeName>(
8050
- type: T,
8051
- aggregateId: string,
8052
- payload: PayloadOfType<T>,
8053
- opts?: { tx?: DrizzleTransaction; metadata?: Record<string, unknown> },
8054
- ): Promise<void> {
8055
- const meta = getEventMetadata(type);
8056
-
8057
- const flag = process.env['CODEGEN_EVENT_VALIDATE'];
8058
- const shouldValidate =
8059
- flag === undefined ? true : flag !== 'false' && flag !== '0';
8060
- if (shouldValidate) {
8061
- // \`eventPayloadSchemas\` is typed as \`Record<EventTypeName, z.ZodType>\`,
8062
- // so under \`noUncheckedIndexedAccess\` the indexed lookup widens
8063
- // to \`z.ZodType | undefined\`. When no events are registered at
8064
- // codegen time \`EventTypeName\` degrades to \`string\` and the
8065
- // schemas object is literally \`{}\` \u2014 the guard below is the
8066
- // honest handling of that empty-registry case (skip validation;
8067
- // it's a warn-only best-effort check per the class docblock).
8068
- const schema = eventPayloadSchemas[type];
8069
- if (schema) {
8070
- const check = schema.safeParse(payload);
8071
- if (!check.success) {
8072
- console.warn(
8073
- \`[TypedEventBus] payload validation failed for \${String(type)}:\`,
8074
- check.error.issues,
8075
- );
8076
- }
8077
- }
8078
- }
8079
-
8080
- const tenantId = opts?.metadata?.['tenantId'];
8081
- if (this.multiTenant && (tenantId === undefined || tenantId === null)) {
8082
- throw new MissingTenantIdError(type as string);
8083
- }
8084
-
8085
- const aggregateType =
8086
- meta.aggregate ?? meta.source ?? meta.destination ?? (type as string);
8087
-
8088
- // AUDIT-3: build metadata with tier-aware routing stamping. For
8089
- // \`tier: 'audit'\` events the bus FORCES pool/direction to null,
8090
- // even if the caller supplied them in opts.metadata. Audit routing
8091
- // is bus-stamped, not caller-controlled (see plan \xA7AUDIT-3).
8092
- const baseMetadata: Record<string, unknown> = { ...(opts?.metadata ?? {}) };
8093
- if (meta.tier === 'audit') {
8094
- if (
8095
- baseMetadata['pool'] !== undefined ||
8096
- baseMetadata['direction'] !== undefined
8097
- ) {
8098
- console.debug(
8099
- \`[TypedEventBus] tier:audit event '\${String(type)}' had pool/direction in opts.metadata; overriding to null.\`,
8100
- );
8101
- }
8102
- baseMetadata['pool'] = null;
8103
- baseMetadata['direction'] = null;
8104
- baseMetadata['tier'] = 'audit';
8105
- } else {
8106
- baseMetadata['pool'] = meta.pool;
8107
- baseMetadata['direction'] = meta.direction;
8108
- baseMetadata['tier'] = 'domain';
8109
- }
8110
- baseMetadata['version'] = meta.version;
8111
-
8112
- await this.bus.publish(
8113
- {
8114
- id: randomUUID(),
8115
- type,
8116
- aggregateId,
8117
- aggregateType,
8118
- payload: payload as Record<string, unknown>,
8119
- occurredAt: new Date(),
8120
- metadata: baseMetadata,
8121
- },
8122
- opts?.tx,
8123
- );
8124
- }
8125
-
8126
- subscribe<T extends EventTypeName>(
8127
- type: T,
8128
- handler: (event: EventOfType<T>) => Promise<void>,
8129
- ): () => void {
8130
- return this.bus.subscribe<EventOfType<T>>(type, handler as never);
8131
- }
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");
8132
8187
  }
8133
- `;
8134
- function buildBusContent(_events) {
8135
- 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");
8136
8198
  }
8137
- function buildIndexContent(_events) {
8138
- return HEADER7 + `
8139
- export * from './types';
8140
- export * from './schemas';
8141
- export * from './registry';
8142
- export * from './bus';
8143
- `;
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");
8144
8215
  }
8145
- var OUTPUT_FILE_NAMES = [
8146
- "types.ts",
8147
- "schemas.ts",
8148
- "registry.ts",
8149
- "bus.ts",
8150
- "index.ts"
8151
- ];
8152
- async function generateEventCodegen(opts) {
8153
- const { entitiesDir, eventsDir, outputDir, dryRun = false } = opts;
8154
- const { events: merged, issues } = collectMergedEvents({
8155
- entitiesDir,
8156
- eventsDir
8157
- });
8158
- const builders = {
8159
- "types.ts": buildTypesContent,
8160
- "schemas.ts": buildSchemasContent,
8161
- "registry.ts": buildRegistryContent,
8162
- "bus.ts": buildBusContent,
8163
- "index.ts": buildIndexContent
8164
- };
8165
- 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) => ({
8166
8221
  name,
8167
8222
  outputPath: path12.join(outputDir, name),
8168
- content: builders[name](merged)
8169
- }));
8170
- 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);
8171
8256
  let written = false;
8172
- if (!dryRun && !hasError) {
8173
- fs9.mkdirSync(outputDir, { recursive: true });
8174
- for (const file of files) {
8175
- 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
+ }
8176
8264
  }
8265
+ fs9.writeFileSync(rootBarrel.outputPath, rootBarrel.content);
8177
8266
  written = true;
8178
8267
  }
8179
8268
  return {
8180
- outputDir,
8181
- eventCount: merged.length,
8182
- events: merged,
8183
- issues,
8184
- written,
8185
- files
8269
+ outputRoot,
8270
+ patterns: perPattern,
8271
+ files: allFiles,
8272
+ written
8186
8273
  };
8187
8274
  }
8188
8275
 
@@ -9728,21 +9815,15 @@ var EntityNewCommand = class extends Command2 {
9728
9815
  const generatedDir = resolveGeneratedDir(ctx);
9729
9816
  const architecture = resolveArchitecture(ctx);
9730
9817
  const subsystemsRoot = resolveSubsystemsRoot(ctx);
9731
- const scopeEntityTypePath = path14.resolve(
9732
- subsystemsRoot,
9733
- "jobs/generated/scope-entity-type.ts"
9734
- );
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");
9735
9820
  const eventsDir = resolveEventsDir(ctx);
9736
- const eventCodegenOutputDir = path14.resolve(
9737
- subsystemsRoot,
9738
- "events/generated"
9739
- );
9740
- const bridgeRegistryOutputDir = path14.resolve(
9741
- subsystemsRoot,
9742
- "bridge/generated"
9743
- );
9821
+ const eventCodegenOutputDir = runtimeMode === "package" ? path14.resolve(generatedDir, "events") : path14.resolve(subsystemsRoot, "events/generated");
9822
+ const bridgeInstalledForRegistry = configuredSubsystemNames(
9823
+ ctx.config
9824
+ ).includes("bridge");
9825
+ const bridgeRegistryOutputDir = runtimeMode === "package" ? generatedDir : path14.resolve(subsystemsRoot, "bridge/generated");
9744
9826
  const backendSrcForHandlers = ctx.config?.paths?.backend_src ?? "src";
9745
- const runtimeMode = resolveRuntimeMode(ctx.config);
9746
9827
  const bridgeHandlersDir = path14.resolve(
9747
9828
  ctx.cwd,
9748
9829
  backendSrcForHandlers,
@@ -9787,12 +9868,15 @@ var EntityNewCommand = class extends Command2 {
9787
9868
  entitiesDir,
9788
9869
  eventsDir,
9789
9870
  outputDir: eventCodegenOutputDir,
9871
+ mode: runtimeMode,
9790
9872
  dryRun: true
9791
9873
  });
9792
9874
  const bridgeRegistryPlan = await generateBridgeRegistry({
9793
9875
  handlersDir: bridgeHandlersDir,
9794
9876
  eventsGeneratedDir: eventCodegenOutputDir,
9795
9877
  outputDir: bridgeRegistryOutputDir,
9878
+ mode: runtimeMode,
9879
+ bridgeInstalled: bridgeInstalledForRegistry,
9796
9880
  dryRun: true
9797
9881
  });
9798
9882
  const orchestrationPatterns = await loadOrchestrationPatterns();
@@ -9965,7 +10049,8 @@ var EntityNewCommand = class extends Command2 {
9965
10049
  eventCodegenResult = await generateEventCodegen({
9966
10050
  entitiesDir,
9967
10051
  eventsDir,
9968
- outputDir: eventCodegenOutputDir
10052
+ outputDir: eventCodegenOutputDir,
10053
+ mode: runtimeMode
9969
10054
  });
9970
10055
  if (!isJsonMode()) {
9971
10056
  for (const issue of eventCodegenResult.issues) {
@@ -9987,7 +10072,9 @@ var EntityNewCommand = class extends Command2 {
9987
10072
  bridgeRegistryResult = await generateBridgeRegistry({
9988
10073
  handlersDir: bridgeHandlersDir,
9989
10074
  eventsGeneratedDir: eventCodegenOutputDir,
9990
- outputDir: bridgeRegistryOutputDir
10075
+ outputDir: bridgeRegistryOutputDir,
10076
+ mode: runtimeMode,
10077
+ bridgeInstalled: bridgeInstalledForRegistry
9991
10078
  });
9992
10079
  if (bridgeRegistryResult.skipped && !isJsonMode()) {
9993
10080
  printInfo(