@pattern-stack/codegen 0.23.0 → 0.25.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/consumer-skills/integration/SKILL.md +11 -3
  3. package/dist/{chunk-XKWOJZZ4.js → chunk-37PILMIT.js} +4 -4
  4. package/dist/{chunk-2VHZ7EKC.js → chunk-5AAA4LTE.js} +2 -2
  5. package/dist/{chunk-42763UEE.js → chunk-6M6LZEP6.js} +2 -2
  6. package/dist/{chunk-AS3NAZB6.js → chunk-B7SC2V45.js} +2 -2
  7. package/dist/{chunk-W72PRNJY.js → chunk-BPYZCEHS.js} +2 -2
  8. package/dist/{chunk-FIUC6QB5.js → chunk-CKLM57IE.js} +10 -10
  9. package/dist/{chunk-KYR3B3OW.js → chunk-DAHWN63L.js} +26 -5
  10. package/dist/chunk-DAHWN63L.js.map +1 -0
  11. package/dist/{chunk-SH76CFAY.js → chunk-ENAR3F5S.js} +2 -2
  12. package/dist/{chunk-RFH7N6EP.js → chunk-FCPTHS42.js} +2 -2
  13. package/dist/{chunk-FFUDEIFF.js → chunk-HN5HT5WL.js} +2 -2
  14. package/dist/{chunk-4M66MQYA.js → chunk-K4BQQ2NN.js} +4 -2
  15. package/dist/chunk-K4BQQ2NN.js.map +1 -0
  16. package/dist/{chunk-QFUIE37H.js → chunk-KFXXOFDC.js} +4 -4
  17. package/dist/{chunk-O2A6XHGD.js → chunk-LLDJS7PJ.js} +2 -2
  18. package/dist/{chunk-JOBQ6RUU.js → chunk-LQZESSM3.js} +28 -1
  19. package/dist/chunk-LQZESSM3.js.map +1 -0
  20. package/dist/{chunk-JRQO2IOF.js → chunk-MU54DZCC.js} +27 -1
  21. package/dist/chunk-MU54DZCC.js.map +1 -0
  22. package/dist/{chunk-INO47JXD.js → chunk-PBENHIN2.js} +3 -3
  23. package/dist/{chunk-CLWBNXKF.js → chunk-PLUJEQLU.js} +2 -2
  24. package/dist/{chunk-S7C6TIIF.js → chunk-S5G3HO7N.js} +3 -1
  25. package/dist/chunk-S5G3HO7N.js.map +1 -0
  26. package/dist/{chunk-JYBFPNBJ.js → chunk-SJGEBMJT.js} +2 -2
  27. package/dist/{chunk-6XP2Q5SS.js → chunk-WZOPWQN2.js} +2 -2
  28. package/dist/{chunk-TDEHU73T.js → chunk-YIVQ7KLS.js} +46 -5
  29. package/dist/chunk-YIVQ7KLS.js.map +1 -0
  30. package/dist/runtime/base-classes/activity-entity-service.js +3 -3
  31. package/dist/runtime/base-classes/base-service.js +2 -2
  32. package/dist/runtime/base-classes/index.js +20 -20
  33. package/dist/runtime/base-classes/integrated-entity-service.js +3 -3
  34. package/dist/runtime/base-classes/knowledge-entity-service.js +3 -3
  35. package/dist/runtime/base-classes/lifecycle-events.js +1 -1
  36. package/dist/runtime/base-classes/metadata-entity-service.js +3 -3
  37. package/dist/runtime/subsystems/auth/auth.module.js +1 -1
  38. package/dist/runtime/subsystems/auth/index.js +3 -3
  39. package/dist/runtime/subsystems/bridge/bridge.module.js +6 -6
  40. package/dist/runtime/subsystems/bridge/index.js +6 -6
  41. package/dist/runtime/subsystems/events/events.module.js +4 -4
  42. package/dist/runtime/subsystems/events/generated/bus.js +3 -3
  43. package/dist/runtime/subsystems/events/generated/index.d.ts +2 -2
  44. package/dist/runtime/subsystems/events/generated/index.js +9 -3
  45. package/dist/runtime/subsystems/events/generated/registry.d.ts +36 -0
  46. package/dist/runtime/subsystems/events/generated/registry.js +1 -1
  47. package/dist/runtime/subsystems/events/generated/schemas.d.ts +109 -1
  48. package/dist/runtime/subsystems/events/generated/schemas.js +7 -1
  49. package/dist/runtime/subsystems/events/generated/types.d.ts +48 -2
  50. package/dist/runtime/subsystems/events/index.js +4 -4
  51. package/dist/runtime/subsystems/index.d.ts +3 -2
  52. package/dist/runtime/subsystems/index.js +25 -21
  53. package/dist/runtime/subsystems/integration/execute-integration.use-case.d.ts +11 -1
  54. package/dist/runtime/subsystems/integration/execute-integration.use-case.js +2 -2
  55. package/dist/runtime/subsystems/integration/index.d.ts +2 -1
  56. package/dist/runtime/subsystems/integration/index.js +10 -8
  57. package/dist/runtime/subsystems/integration/integration-change-emitter.protocol.d.ts +106 -0
  58. package/dist/runtime/subsystems/integration/integration-change-emitter.protocol.js +1 -0
  59. package/dist/runtime/subsystems/integration/integration-change-emitter.protocol.js.map +1 -0
  60. package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
  61. package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
  62. package/dist/runtime/subsystems/integration/integration.module.js +4 -4
  63. package/dist/runtime/subsystems/integration/integration.tokens.d.ts +11 -1
  64. package/dist/runtime/subsystems/integration/integration.tokens.js +3 -1
  65. package/dist/runtime/subsystems/jobs/index.js +11 -11
  66. package/dist/runtime/subsystems/jobs/job-worker.module.js +5 -5
  67. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +4 -4
  68. package/dist/runtime/subsystems/observability/index.js +3 -3
  69. package/dist/runtime/subsystems/observability/observability.module.js +3 -3
  70. package/dist/runtime/subsystems/observability/observability.service.js +2 -2
  71. package/dist/src/cli/index.js +300 -53
  72. package/dist/src/cli/index.js.map +1 -1
  73. package/dist/src/index.d.ts +13 -0
  74. package/dist/src/index.js +7 -7
  75. package/package.json +1 -1
  76. package/runtime/base-classes/lifecycle-events.ts +39 -6
  77. package/runtime/subsystems/events/generated/registry.ts +27 -0
  78. package/runtime/subsystems/events/generated/schemas.ts +26 -0
  79. package/runtime/subsystems/events/generated/types.ts +52 -0
  80. package/runtime/subsystems/index.ts +23 -0
  81. package/runtime/subsystems/integration/execute-integration.use-case.ts +69 -1
  82. package/runtime/subsystems/integration/index.ts +6 -0
  83. package/runtime/subsystems/integration/integration-change-emitter.protocol.ts +107 -0
  84. package/runtime/subsystems/integration/integration.tokens.ts +11 -0
  85. package/templates/subsystem/jobs/job-orchestration.schema.ejs.t +1 -0
  86. package/templates/subsystem/jobs/main-hook.ejs.t +1 -1
  87. package/templates/subsystem/jobs/prompt.js +40 -2
  88. package/templates/subsystem/jobs/worker.ejs.t +47 -35
  89. package/dist/chunk-4M66MQYA.js.map +0 -1
  90. package/dist/chunk-JOBQ6RUU.js.map +0 -1
  91. package/dist/chunk-JRQO2IOF.js.map +0 -1
  92. package/dist/chunk-KYR3B3OW.js.map +0 -1
  93. package/dist/chunk-S7C6TIIF.js.map +0 -1
  94. package/dist/chunk-TDEHU73T.js.map +0 -1
  95. /package/dist/{chunk-XKWOJZZ4.js.map → chunk-37PILMIT.js.map} +0 -0
  96. /package/dist/{chunk-2VHZ7EKC.js.map → chunk-5AAA4LTE.js.map} +0 -0
  97. /package/dist/{chunk-42763UEE.js.map → chunk-6M6LZEP6.js.map} +0 -0
  98. /package/dist/{chunk-AS3NAZB6.js.map → chunk-B7SC2V45.js.map} +0 -0
  99. /package/dist/{chunk-W72PRNJY.js.map → chunk-BPYZCEHS.js.map} +0 -0
  100. /package/dist/{chunk-FIUC6QB5.js.map → chunk-CKLM57IE.js.map} +0 -0
  101. /package/dist/{chunk-SH76CFAY.js.map → chunk-ENAR3F5S.js.map} +0 -0
  102. /package/dist/{chunk-RFH7N6EP.js.map → chunk-FCPTHS42.js.map} +0 -0
  103. /package/dist/{chunk-FFUDEIFF.js.map → chunk-HN5HT5WL.js.map} +0 -0
  104. /package/dist/{chunk-QFUIE37H.js.map → chunk-KFXXOFDC.js.map} +0 -0
  105. /package/dist/{chunk-O2A6XHGD.js.map → chunk-LLDJS7PJ.js.map} +0 -0
  106. /package/dist/{chunk-INO47JXD.js.map → chunk-PBENHIN2.js.map} +0 -0
  107. /package/dist/{chunk-CLWBNXKF.js.map → chunk-PLUJEQLU.js.map} +0 -0
  108. /package/dist/{chunk-JYBFPNBJ.js.map → chunk-SJGEBMJT.js.map} +0 -0
  109. /package/dist/{chunk-6XP2Q5SS.js.map → chunk-WZOPWQN2.js.map} +0 -0
@@ -44,13 +44,13 @@ import {
44
44
  validateOrchestrationProject,
45
45
  validateProviders,
46
46
  writeManifest
47
- } from "../../chunk-4M66MQYA.js";
47
+ } from "../../chunk-K4BQQ2NN.js";
48
48
  import "../../chunk-KVOWSC5S.js";
49
- import "../../chunk-QFUIE37H.js";
50
- import "../../chunk-FFUDEIFF.js";
51
- import "../../chunk-EO2QPOKH.js";
52
49
  import "../../chunk-PRWIX6UW.js";
53
- import "../../chunk-O2A6XHGD.js";
50
+ import "../../chunk-KFXXOFDC.js";
51
+ import "../../chunk-HN5HT5WL.js";
52
+ import "../../chunk-EO2QPOKH.js";
53
+ import "../../chunk-LLDJS7PJ.js";
54
54
  import "../../chunk-HNWZFNKP.js";
55
55
  import "../../chunk-AHV4GDYM.js";
56
56
  import "../../chunk-SQDOBLBP.js";
@@ -63,8 +63,8 @@ import {
63
63
  } from "../../chunk-5TK7MEN4.js";
64
64
  import "../../chunk-4KNXX6TI.js";
65
65
  import "../../chunk-3CJFPU6Q.js";
66
- import "../../chunk-TDEHU73T.js";
67
- import "../../chunk-S7C6TIIF.js";
66
+ import "../../chunk-YIVQ7KLS.js";
67
+ import "../../chunk-S5G3HO7N.js";
68
68
  import "../../chunk-MZ6GV4YF.js";
69
69
  import "../../chunk-LG57S2SC.js";
70
70
  import "../../chunk-U64T4YZE.js";
@@ -2151,7 +2151,7 @@ function loadEvents(eventsDir, entityNames) {
2151
2151
  function desugarEntityEvents(entity) {
2152
2152
  const entityName = entity.entity.name;
2153
2153
  const entityEvents = entity.events ?? [];
2154
- return entityEvents.map((ev) => {
2154
+ const explicit = entityEvents.map((ev) => {
2155
2155
  const payload = {};
2156
2156
  for (const [key, typeString] of Object.entries(ev.body)) {
2157
2157
  if (!isEventFieldType(typeString)) {
@@ -2173,6 +2173,56 @@ function desugarEntityEvents(entity) {
2173
2173
  };
2174
2174
  return def;
2175
2175
  });
2176
+ const changeTriad = desugarEmitChangeEvents(entity);
2177
+ return [...explicit, ...changeTriad];
2178
+ }
2179
+ var EMIT_CHANGE_SUFFIXES = ["created", "edited", "deleted"];
2180
+ function desugarEmitChangeEvents(entity) {
2181
+ if (entity.integration?.sink?.emit_changes !== true) return [];
2182
+ const entityName = entity.entity.name;
2183
+ const basePayload = {
2184
+ entity_id: {
2185
+ type: "uuid",
2186
+ nullable: false,
2187
+ description: "Local aggregate id the sink wrote/soft-deleted."
2188
+ },
2189
+ external_id: {
2190
+ type: "string",
2191
+ nullable: false,
2192
+ description: "Vendor external id the change keyed on."
2193
+ },
2194
+ provider: {
2195
+ type: "string",
2196
+ nullable: false,
2197
+ description: "Provider label (e.g. 'slack', 'google')."
2198
+ },
2199
+ source: {
2200
+ type: "string",
2201
+ nullable: false,
2202
+ description: "Provenance marker \u2014 always 'integration'. A write-back action reads this to avoid echoing the change back to the vendor."
2203
+ }
2204
+ };
2205
+ return EMIT_CHANGE_SUFFIXES.map((suffix) => {
2206
+ const payload = { ...basePayload };
2207
+ if (suffix !== "deleted") {
2208
+ payload.changed_fields = {
2209
+ type: "json",
2210
+ nullable: true,
2211
+ description: "Differ's per-field before/after map (same value as integration_run_items.changed_fields)."
2212
+ };
2213
+ }
2214
+ const def = {
2215
+ type: `${entityName}_${suffix}`,
2216
+ tier: "domain",
2217
+ direction: "change",
2218
+ aggregate: entityName,
2219
+ payload,
2220
+ retry: { attempts: 3, backoff: "exponential" },
2221
+ version: 1,
2222
+ pool: DIRECTION_TO_POOL.change
2223
+ };
2224
+ return def;
2225
+ });
2176
2226
  }
2177
2227
  function isEventFieldType(s) {
2178
2228
  return EVENT_FIELD_TYPES.includes(s);
@@ -3696,6 +3746,97 @@ import {
3696
3746
  } from "fs";
3697
3747
  import { dirname, join as join8 } from "path";
3698
3748
 
3749
+ // src/cli/shared/change-emitter-emission-generator.ts
3750
+ function generatedBanner(sourceDesc) {
3751
+ return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
3752
+ // Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
3753
+ }
3754
+ function changeEmitterClass(entityClass) {
3755
+ return `${entityClass}ChangeEmitter`;
3756
+ }
3757
+ function generateChangeEmitter(input) {
3758
+ const mode = input.mode ?? "package";
3759
+ const cls = changeEmitterClass(input.entityClass);
3760
+ const eventsImport = subsystemsImport(mode, "events");
3761
+ const integrationImport = subsystemsImport(mode, "integration");
3762
+ const createdType = `${input.entityName}_created`;
3763
+ const editedType = `${input.entityName}_edited`;
3764
+ const deletedType = `${input.entityName}_deleted`;
3765
+ return `${generatedBanner(input.sourceDesc)}
3766
+ import { Inject, Injectable } from '@nestjs/common';
3767
+ import { TYPED_EVENT_BUS } from '${eventsImport}';
3768
+ import type {
3769
+ IIntegrationChangeEmitter,
3770
+ IntegrationChangeNotification,
3771
+ } from '${integrationImport}';
3772
+
3773
+ /**
3774
+ * Minimal publisher shape the emitter calls \u2014 decoupled from BOTH the package's
3775
+ * \`TypedEventBus\` (whose \`EventTypeName\` union is the package's own events) and
3776
+ * the consumer's generated \`TypedEventBus\` (whose union is the consumer's
3777
+ * events). The \`${createdType}\` / \`${editedType}\` / \`${deletedType}\` events
3778
+ * THIS class publishes live in the CONSUMER's registry; the structural \`publish\`
3779
+ * here accepts the string types without depending on either compile-time union.
3780
+ * The TYPED_EVENT_BUS token resolves to the consumer's generated bus at runtime
3781
+ * (bound by EventsModule.forRoot()), which validates the payload against the
3782
+ * generated event schema.
3783
+ */
3784
+ interface ChangeEventPublisher {
3785
+ publish(
3786
+ type: string,
3787
+ aggregateId: string,
3788
+ payload: Record<string, unknown>,
3789
+ opts?: { tx?: unknown; metadata?: Record<string, unknown> },
3790
+ ): Promise<void>;
3791
+ }
3792
+
3793
+ /**
3794
+ * ${cls} \u2014 publishes the typed ${input.entityName} data-level change events
3795
+ * (\`${createdType}\` / \`${editedType}\` / \`${deletedType}\`) after the
3796
+ * integration orchestrator's sink write/soft-delete (EMIT-CHANGES seam).
3797
+ *
3798
+ * Bound to INTEGRATION_CHANGE_EMITTER in the ${input.entityName} integration
3799
+ * assembly; the orchestrator calls \`emitChange\` once per real change. The
3800
+ * payload carries \`source: 'integration'\` so a write-back action can detect an
3801
+ * integration-originated change and decline to echo it back to the vendor.
3802
+ */
3803
+ @Injectable()
3804
+ export class ${cls} implements IIntegrationChangeEmitter {
3805
+ constructor(
3806
+ @Inject(TYPED_EVENT_BUS) private readonly bus: ChangeEventPublisher,
3807
+ ) {}
3808
+
3809
+ async emitChange(notification: IntegrationChangeNotification): Promise<void> {
3810
+ const type =
3811
+ notification.action === 'created'
3812
+ ? '${createdType}'
3813
+ : notification.action === 'updated'
3814
+ ? '${editedType}'
3815
+ : '${deletedType}';
3816
+
3817
+ const payload: Record<string, unknown> = {
3818
+ entityId: notification.entityId,
3819
+ externalId: notification.externalId,
3820
+ provider: notification.provider,
3821
+ source: 'integration',
3822
+ };
3823
+ // changedFields is present on created/edited only (deletes are tombstones).
3824
+ if (notification.changedFields !== undefined) {
3825
+ payload['changedFields'] = notification.changedFields;
3826
+ }
3827
+
3828
+ await this.bus.publish(type, notification.entityId, payload, {
3829
+ tx: notification.tx,
3830
+ metadata:
3831
+ notification.tenantId !== undefined && notification.tenantId !== null
3832
+ ? { tenantId: notification.tenantId }
3833
+ : undefined,
3834
+ });
3835
+ }
3836
+ }
3837
+ `;
3838
+ }
3839
+
3699
3840
  // src/cli/shared/sink-emission-generator.ts
3700
3841
  var USER_ID_FIELD = "userId";
3701
3842
  function sinkNames(entityClass) {
@@ -3916,7 +4057,7 @@ export class ${n.sinkClass} extends ${n.sinkBaseClass} {
3916
4057
 
3917
4058
  // src/cli/shared/assembly-emission-generator.ts
3918
4059
  import { relative, resolve as resolve2, sep } from "path";
3919
- function generatedBanner(sourceDesc) {
4060
+ function generatedBanner2(sourceDesc) {
3920
4061
  return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
3921
4062
  // Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
3922
4063
  }
@@ -3947,19 +4088,30 @@ function generateAssemblyModule(input) {
3947
4088
  const token = integrationUseCaseToken(input.entityName, input.provider);
3948
4089
  const moduleClass = assemblyModuleClass(input.entityName, input.provider);
3949
4090
  const tokensImport = `../../${input.surface}-integration.tokens`;
3950
- return `${generatedBanner(input.sourceDesc)}
4091
+ const emitChanges = input.emitChanges === true;
4092
+ const emitterClass = changeEmitterClass(input.entityClass);
4093
+ const emitterImport = `../../sinks/${input.entityName}.change-emitter`;
4094
+ const integrationTokenImports = emitChanges ? ` INTEGRATION_CHANGE_EMITTER,
4095
+ INTEGRATION_CHANGE_SOURCE,
4096
+ INTEGRATION_SINK,` : ` INTEGRATION_CHANGE_SOURCE,
4097
+ INTEGRATION_SINK,`;
4098
+ const emitterImportLine = emitChanges ? `
4099
+ import { ${emitterClass} } from '${emitterImport}';` : "";
4100
+ const emitterProviderBlock = emitChanges ? `
4101
+ ${emitterClass},
4102
+ { provide: INTEGRATION_CHANGE_EMITTER, useExisting: ${emitterClass} },` : "";
4103
+ return `${generatedBanner2(input.sourceDesc)}
3951
4104
  import { Module } from '@nestjs/common';
3952
4105
  import {
3953
4106
  ExecuteIntegrationUseCase,
3954
- INTEGRATION_CHANGE_SOURCE,
3955
- INTEGRATION_SINK,
4107
+ ${integrationTokenImports}
3956
4108
  } from '${subsystemsImport(input.mode ?? "package", "integration")}';
3957
4109
  import { ${adapterClass} } from '${adapterImport}';
3958
4110
  import { ${adapterModuleClass} } from '${adapterModuleImport}';
3959
4111
  import { ${sinkClass} } from '${sinkImport}';
3960
4112
  import { ${input.repoClass} } from '${input.repoImportSpecifier}';
3961
4113
  import { ${input.moduleClass} } from '${input.moduleImportSpecifier}';
3962
- import { ${token} } from '${tokensImport}';
4114
+ import { ${token} } from '${tokensImport}';${emitterImportLine}
3963
4115
 
3964
4116
  /**
3965
4117
  * ${moduleClass} \u2014 the ${input.surface}/${input.entityName} \u2190 ${input.provider}
@@ -3969,7 +4121,12 @@ import { ${token} } from '${tokensImport}';
3969
4121
  * \`changeSources.${input.entityName}\` and INTEGRATION_SINK from
3970
4122
  * ${sinkClass}, provides a local ExecuteIntegrationUseCase, and aliases+exports
3971
4123
  * it under ${token} (the bare class token is ambiguous at app root \u2014 every
3972
- * assembly provides it). The substrate (cursor store, run recorder, differ,
4124
+ * assembly provides it).${emitChanges ? `
4125
+ *
4126
+ * EMIT-CHANGES: binds INTEGRATION_CHANGE_EMITTER to ${emitterClass} so the
4127
+ * orchestrator publishes typed ${input.entityName}_created/_edited/_deleted events
4128
+ * after every sink write/soft-delete (integration.sink.emit_changes).
4129
+ *` : ""} The substrate (cursor store, run recorder, differ,
3973
4130
  * multi-tenant flag) comes from the global IntegrationModule.forRoot(...) in
3974
4131
  * AppModule, never re-bound here.
3975
4132
  */
@@ -3985,7 +4142,7 @@ import { ${token} } from '${tokensImport}';
3985
4142
  provide: INTEGRATION_SINK,
3986
4143
  useFactory: (repo: ${input.repoClass}) => new ${sinkClass}(repo, '${input.provider}'),
3987
4144
  inject: [${input.repoClass}],
3988
- },
4145
+ },${emitterProviderBlock}
3989
4146
  ExecuteIntegrationUseCase,
3990
4147
  { provide: ${token}, useExisting: ExecuteIntegrationUseCase },
3991
4148
  ],
@@ -4005,7 +4162,7 @@ function generateIntegrationTokens(surface, entries) {
4005
4162
  * (${assemblyModuleClass(e.entityName, e.provider)}). A trigger grabs this to run it. */
4006
4163
  export const ${token} = Symbol.for('${key}');`;
4007
4164
  }).join("\n\n");
4008
- return `${generatedBanner(`surface: ${surface}`)}
4165
+ return `${generatedBanner2(`surface: ${surface}`)}
4009
4166
  /**
4010
4167
  * Use-case handles for the \`${surface}\` surface \u2014 one per (entity, provider)
4011
4168
  * assembly. Each is a Symbol \`provide:\` token a per-entity
@@ -4031,7 +4188,7 @@ function generateIntegrationAggregator(surface, entries) {
4031
4188
  return `import { ${cls} } from '${path36}';`;
4032
4189
  }).join("\n");
4033
4190
  const membersInline = moduleClasses.join(", ");
4034
- return `${generatedBanner(`surface: ${surface}`)}
4191
+ return `${generatedBanner2(`surface: ${surface}`)}
4035
4192
  import { Module } from '@nestjs/common';
4036
4193
  ${importLines || "// no (entity, provider) integration assemblies on this surface yet"}
4037
4194
 
@@ -4184,7 +4341,7 @@ function isClientlessProvider(surfaces) {
4184
4341
  return surfaces.length > 0 && surfaces.every((s) => SURFACE_REGISTRY[s]?.readPrimitive === true);
4185
4342
  }
4186
4343
  var SCAFFOLD_SENTINEL = "// <CODEGEN-SCAFFOLD-V1>";
4187
- function generatedBanner2(sourceDesc) {
4344
+ function generatedBanner3(sourceDesc) {
4188
4345
  return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
4189
4346
  // Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
4190
4347
  }
@@ -4424,7 +4581,7 @@ ${changeSourcesDecl}
4424
4581
  }
4425
4582
  function generateAdapterModule(def, surface) {
4426
4583
  const n = names(def.slug, surface);
4427
- return `${generatedBanner2(`definitions/providers/${def.slug}.yaml (surface: ${surface})`)}
4584
+ return `${generatedBanner3(`definitions/providers/${def.slug}.yaml (surface: ${surface})`)}
4428
4585
  import { Module } from '@nestjs/common';
4429
4586
  import { ${n.providerModuleClass} } from '../../../providers/${def.slug}/${def.slug}.provider.module';
4430
4587
  import { ${n.adapterClass} } from './${def.slug}-${surface}.adapter';
@@ -4442,13 +4599,13 @@ function generateAdaptersBarrel(surface, providerSlugs) {
4442
4599
  const n = names(slug, surface);
4443
4600
  return `export { ${n.adapterModuleClass} } from './${slug}/${slug}-${surface}.adapter.module';`;
4444
4601
  }).join("\n");
4445
- return `${generatedBanner2(`definitions/providers/*.yaml (surface: ${surface})`)}
4602
+ return `${generatedBanner3(`definitions/providers/*.yaml (surface: ${surface})`)}
4446
4603
  ${lines}
4447
4604
  `;
4448
4605
  }
4449
4606
  function generateSurfaceTokens(surface, mode = "package") {
4450
4607
  const n = names("__placeholder__", surface);
4451
- return `${generatedBanner2(`surface: ${surface}`)}
4608
+ return `${generatedBanner3(`surface: ${surface}`)}
4452
4609
  import type { IChangeSource } from '${subsystemsImport(mode, "integration")}';
4453
4610
 
4454
4611
  /** The assembled list of every ${surface} adapter's contribution. */
@@ -4487,7 +4644,7 @@ function generateSurfaceAggregator(surface, providerSlugs, mode = "package") {
4487
4644
  return `${lowerFirst(p.adapterClass)}: ${p.adapterClass}`;
4488
4645
  }).join(", ");
4489
4646
  const injectTokens = per.map((p) => p.adapterClass).join(", ");
4490
- return `${generatedBanner2(`surface: ${surface}`)}
4647
+ return `${generatedBanner3(`surface: ${surface}`)}
4491
4648
  import { Module } from '@nestjs/common';
4492
4649
  import {
4493
4650
  MemoryEntityChangeSourceRegistry,
@@ -4559,7 +4716,7 @@ function generateTypedView(surface, providerSlugs, entities) {
4559
4716
  const providerUnion = slugs.length ? slugs.map((s) => `'${s}'`).join(" | ") : "never";
4560
4717
  const entityUnion = ents.length ? ents.map((e) => `'${e}'`).join(" | ") : "never";
4561
4718
  const mapEntries = slugs.map((s) => ` ${jsKey(s)}: ${surfacePascal}Entity;`).join("\n");
4562
- return `${generatedBanner2(`surface: ${surface}`)}
4719
+ return `${generatedBanner3(`surface: ${surface}`)}
4563
4720
  /**
4564
4721
  * Per-consumer typed view for the \`${surface}\` surface. Surface-scoped unions
4565
4722
  * + a (provider, entity) validity map for compile-time-checked consumer
@@ -4589,6 +4746,7 @@ function emitAdapters(opts) {
4589
4746
  scaffoldsSkipped: [],
4590
4747
  skippedSurfaces: [],
4591
4748
  assembliesWritten: [],
4749
+ changeEmittersWritten: [],
4592
4750
  tokensWritten: [],
4593
4751
  integrationAggregatorsWritten: [],
4594
4752
  skippedAssemblies: []
@@ -4702,6 +4860,19 @@ function emitAdapters(opts) {
4702
4860
  if (!opts.dryRun) writeFile(subclassPath, subclassContent);
4703
4861
  result.scaffoldsWritten.push(subclassPath);
4704
4862
  }
4863
+ const emitChanges = def?.integration?.sink?.emit_changes === true;
4864
+ if (emitChanges) {
4865
+ const emitterPath = join8(sinksDir, `${entityName}.change-emitter.ts`);
4866
+ const emitterContent = generateChangeEmitter({
4867
+ entityName,
4868
+ entityClass: loc.entityClass,
4869
+ surface,
4870
+ sourceDesc: `definitions entity '${entityName}' (integration.sink.emit_changes)`,
4871
+ mode
4872
+ });
4873
+ if (!opts.dryRun) writeIfChanged(emitterPath, emitterContent);
4874
+ result.changeEmittersWritten.push(emitterPath);
4875
+ }
4705
4876
  for (const slug of slugs) {
4706
4877
  const assemblyPath = join8(
4707
4878
  modulesDir,
@@ -4718,7 +4889,8 @@ function emitAdapters(opts) {
4718
4889
  repoImportSpecifier: loc.repoImportSpecifier,
4719
4890
  repoClass: loc.repoClass,
4720
4891
  sourceDesc: `definitions/providers/${slug}.yaml`,
4721
- mode
4892
+ mode,
4893
+ emitChanges
4722
4894
  });
4723
4895
  if (!opts.dryRun) writeIfChanged(assemblyPath, assemblyContent);
4724
4896
  result.assembliesWritten.push(assemblyPath);
@@ -5052,12 +5224,12 @@ function resolveSyncMode(entity, config) {
5052
5224
  // src/emitters/frontend/emit-utils.ts
5053
5225
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
5054
5226
  import { dirname as dirname3 } from "path";
5055
- function generatedBanner3(sourceDesc) {
5227
+ function generatedBanner4(sourceDesc) {
5056
5228
  return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
5057
5229
  // Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
5058
5230
  }
5059
5231
  function withBanner(sourceDesc, body) {
5060
- return `${generatedBanner3(sourceDesc)}
5232
+ return `${generatedBanner4(sourceDesc)}
5061
5233
 
5062
5234
  ${body}`;
5063
5235
  }
@@ -6893,6 +7065,11 @@ var EntityNewCommand = class extends Command2 {
6893
7065
  `integration assembly codegen: ${adapterResult.assembliesWritten.length} module(s) + ${adapterResult.tokensWritten.length} tokens file(s) + ${adapterResult.integrationAggregatorsWritten.length} aggregator(s)`
6894
7066
  );
6895
7067
  }
7068
+ if (adapterResult.changeEmittersWritten.length) {
7069
+ printInfo(
7070
+ `integration change-emitters (emit_changes): ${adapterResult.changeEmittersWritten.length} emitter(s)`
7071
+ );
7072
+ }
6896
7073
  for (const s of adapterResult.scaffoldsSkipped) {
6897
7074
  printInfo(`skipped scaffold ${s} (author-owned)`);
6898
7075
  }
@@ -7231,7 +7408,7 @@ function resolveJobsScaffoldLocals(input) {
7231
7408
  const { cwd, config, fileExists, readFile } = input;
7232
7409
  const jobsBlock = config?.jobs ?? {};
7233
7410
  const subsystemsRoot = resolveSubsystemsRootFromConfig(cwd, config);
7234
- const workerPath = path16.resolve(cwd, "worker.ts");
7411
+ const workerPath = path16.resolve(cwd, "src", "worker.ts");
7235
7412
  const mainTsPath = path16.resolve(cwd, "src/main.ts");
7236
7413
  const configPath = path16.resolve(cwd, "codegen.config.yaml");
7237
7414
  const schemaPath = path16.resolve(
@@ -7249,10 +7426,37 @@ function resolveJobsScaffoldLocals(input) {
7249
7426
  configPath,
7250
7427
  workerExists: fileExists(workerPath),
7251
7428
  workerPath,
7429
+ jobWorkerModuleImport: resolveJobWorkerModuleImport(config),
7430
+ workerForRootOpts: resolveWorkerForRootOpts(jobsBlock),
7252
7431
  schemaPath,
7253
- mainHookInjected
7432
+ mainHookInjected,
7433
+ // #517 — in package mode the schema ships in the package (consumed via the
7434
+ // schema barrel), so the template is skipped; vendored mode renders it.
7435
+ skipSchema: resolveRuntimeMode(config) === "package"
7254
7436
  };
7255
7437
  }
7438
+ function resolveJobWorkerModuleImport(config) {
7439
+ return runtimeImport(resolveRuntimeMode(config), "subsystems/jobs/index");
7440
+ }
7441
+ function resolveWorkerForRootOpts(jobsBlock) {
7442
+ const backend = jobsBlock.backend ?? "drizzle";
7443
+ const parts = [`mode: 'standalone'`];
7444
+ if (backend === "bullmq") {
7445
+ parts.push(`backend: 'bullmq'`);
7446
+ const bullExt = jobsBlock.extensions?.bullmq;
7447
+ if (bullExt) {
7448
+ parts.push(`domainModuleExtensions: { bullmq: ${jsonToTs(bullExt)} }`);
7449
+ }
7450
+ } else {
7451
+ const workerExtClause = drizzleExtensionsClause(
7452
+ drizzleJobsExtensions(backend, jobsBlock),
7453
+ "domainModuleExtensions"
7454
+ );
7455
+ if (workerExtClause) parts.push(workerExtClause);
7456
+ }
7457
+ parts.push(`allPools: true`);
7458
+ return `{ ${parts.join(", ")} }`;
7459
+ }
7256
7460
  function normaliseWorkerMode(raw) {
7257
7461
  if (raw === "standalone") return "standalone";
7258
7462
  return "embedded";
@@ -7260,6 +7464,9 @@ function normaliseWorkerMode(raw) {
7260
7464
  function normaliseMultiTenant2(raw) {
7261
7465
  return raw === true;
7262
7466
  }
7467
+ function encodeWorkerForRootOpts(opts) {
7468
+ return Buffer.from(opts, "utf-8").toString("base64");
7469
+ }
7263
7470
  function localsToHygenArgs2(locals) {
7264
7471
  return [
7265
7472
  "--appName",
@@ -7276,10 +7483,19 @@ function localsToHygenArgs2(locals) {
7276
7483
  workerSkipValue(locals.workerExists),
7277
7484
  "--workerPath",
7278
7485
  locals.workerPath,
7486
+ "--jobWorkerModuleImport",
7487
+ locals.jobWorkerModuleImport,
7488
+ "--workerForRootOpts",
7489
+ encodeWorkerForRootOpts(locals.workerForRootOpts),
7279
7490
  "--schemaPath",
7280
7491
  locals.schemaPath,
7281
7492
  "--mainHookInjected",
7282
- workerSkipValue(locals.mainHookInjected)
7493
+ workerSkipValue(locals.mainHookInjected),
7494
+ // #517 — boolean-ish for `skip_if` (same '' / 'true' encoding as
7495
+ // workerExists / mainHookInjected). 'true' in package mode → the schema
7496
+ // template is skipped (the package ships the schema).
7497
+ "--skipSchema",
7498
+ workerSkipValue(locals.skipSchema)
7283
7499
  ];
7284
7500
  }
7285
7501
 
@@ -8137,6 +8353,12 @@ var SubsystemInstallCommand = class extends Command3 {
8137
8353
  return 1;
8138
8354
  }
8139
8355
  if (this.dryRun) {
8356
+ const jobsScaffoldDryRun = desc.name === "jobs" ? runJobsScaffold(ctx.cwd, ctx.config, {
8357
+ dryRun: true,
8358
+ json: isJsonMode(),
8359
+ forceConfig: this.forceConfig,
8360
+ skipConfigBlock: true
8361
+ }) : null;
8140
8362
  if (isJsonMode()) {
8141
8363
  printJson({
8142
8364
  command: "subsystem install",
@@ -8144,7 +8366,8 @@ var SubsystemInstallCommand = class extends Command3 {
8144
8366
  runtime: "package",
8145
8367
  dryRun: true,
8146
8368
  installList: already ? installed : [...installed, desc.name],
8147
- configBlockOutcome
8369
+ configBlockOutcome,
8370
+ ...jobsScaffoldDryRun ? { scaffold: jobsScaffoldDryRun } : {}
8148
8371
  });
8149
8372
  } else {
8150
8373
  printInfo(`Dry run \u2014 runtime: package (no files vendored).`);
@@ -8153,6 +8376,14 @@ var SubsystemInstallCommand = class extends Command3 {
8153
8376
  printInfo(` ${desc.name} config block would be ${configBlockOutcome}`);
8154
8377
  }
8155
8378
  printInfo(" would regenerate <generated>/subsystems.ts + subsystems-schema.ts");
8379
+ if (jobsScaffoldDryRun?.planned?.length) {
8380
+ printInfo(
8381
+ ` jobs scaffold \u2014 ${jobsScaffoldDryRun.planned.length} template targets (worker + main.ts hook; schema ships in the package)`
8382
+ );
8383
+ for (const p of jobsScaffoldDryRun.planned) {
8384
+ console.log(` ${theme.muted(icons.arrow)} ${path24.relative(ctx.cwd, p) || p}`);
8385
+ }
8386
+ }
8156
8387
  }
8157
8388
  return 0;
8158
8389
  }
@@ -8197,6 +8428,12 @@ var SubsystemInstallCommand = class extends Command3 {
8197
8428
  const msg = err instanceof Error ? err.message : String(err);
8198
8429
  printWarning(`barrel regeneration failed \u2014 ${msg}`);
8199
8430
  }
8431
+ const jobsScaffold = desc.name === "jobs" ? runJobsScaffold(ctx.cwd, refreshed.config, {
8432
+ dryRun: false,
8433
+ json: isJsonMode(),
8434
+ forceConfig: this.forceConfig,
8435
+ skipConfigBlock: true
8436
+ }) : null;
8200
8437
  if (isJsonMode()) {
8201
8438
  printJson({
8202
8439
  command: "subsystem install",
@@ -8208,7 +8445,8 @@ var SubsystemInstallCommand = class extends Command3 {
8208
8445
  installOutcome: installResult.outcome,
8209
8446
  configBlockOutcome,
8210
8447
  barrelEmitted,
8211
- schemaEmitted
8448
+ schemaEmitted,
8449
+ ...jobsScaffold ? { scaffold: jobsScaffold } : {}
8212
8450
  });
8213
8451
  return 0;
8214
8452
  }
@@ -8216,6 +8454,17 @@ var SubsystemInstallCommand = class extends Command3 {
8216
8454
  if (installResult.outcome === "added") {
8217
8455
  printInfo(`Added '${desc.name}' to subsystems.install.`);
8218
8456
  }
8457
+ if (jobsScaffold) {
8458
+ if (jobsScaffold.ok) {
8459
+ printSuccess(
8460
+ `jobs scaffold applied (emitted src/worker.ts + src/main.ts hook; schema ships in the package).`
8461
+ );
8462
+ } else {
8463
+ printWarning(
8464
+ `jobs scaffold (Hygen) failed \u2014 config + barrels were written; re-run after fixing: ${jobsScaffold.error ?? "unknown error"}`
8465
+ );
8466
+ }
8467
+ }
8219
8468
  printInfo(
8220
8469
  `Regenerated <generated>/subsystems.ts (${barrelEmitted.join(", ") || "none"}) + subsystems-schema.ts (${schemaEmitted.join(", ") || "none"}).`
8221
8470
  );
@@ -8445,14 +8694,10 @@ function runJobsScaffold(cwd, config, opts) {
8445
8694
  const planned = [
8446
8695
  ...!locals.workerExists ? [locals.workerPath] : [],
8447
8696
  locals.mainTsPath,
8448
- locals.configPath,
8449
- locals.schemaPath
8697
+ ...opts.skipConfigBlock ? [] : [locals.configPath],
8698
+ ...locals.skipSchema ? [] : [locals.schemaPath]
8450
8699
  ];
8451
- const configBlockOutcome = planConfigBlockAction(
8452
- locals.configPath,
8453
- "jobs",
8454
- opts.forceConfig
8455
- );
8700
+ const configBlockOutcome = opts.skipConfigBlock ? void 0 : planConfigBlockAction(locals.configPath, "jobs", opts.forceConfig);
8456
8701
  if (configBlockOutcome === "parse-error") {
8457
8702
  return { ok: false, planned, configBlockOutcome };
8458
8703
  }
@@ -8475,21 +8720,23 @@ function runJobsScaffold(cwd, config, opts) {
8475
8720
  configBlockOutcome
8476
8721
  };
8477
8722
  }
8478
- const configResult = runConfigBlockAction({
8479
- cwd,
8480
- actionFolder: "jobs-config",
8481
- configPath: locals.configPath,
8482
- subsystem: "jobs",
8483
- outcome: configBlockOutcome,
8484
- json: opts.json
8485
- });
8486
- if (!configResult.ok) {
8487
- return {
8488
- ok: false,
8489
- planned,
8490
- error: configResult.error,
8491
- configBlockOutcome
8492
- };
8723
+ if (!opts.skipConfigBlock && configBlockOutcome) {
8724
+ const configResult = runConfigBlockAction({
8725
+ cwd,
8726
+ actionFolder: "jobs-config",
8727
+ configPath: locals.configPath,
8728
+ subsystem: "jobs",
8729
+ outcome: configBlockOutcome,
8730
+ json: opts.json
8731
+ });
8732
+ if (!configResult.ok) {
8733
+ return {
8734
+ ok: false,
8735
+ planned,
8736
+ error: configResult.error,
8737
+ configBlockOutcome
8738
+ };
8739
+ }
8493
8740
  }
8494
8741
  return { ok: true, planned, configBlockOutcome };
8495
8742
  }