@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
@@ -1531,12 +1531,15 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
1531
1531
  sink: z.ZodOptional<z.ZodObject<{
1532
1532
  delete: z.ZodOptional<z.ZodEnum<["soft", "tombstone", "noop"]>>;
1533
1533
  exclude_fields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1534
+ emit_changes: z.ZodOptional<z.ZodBoolean>;
1534
1535
  }, "strict", z.ZodTypeAny, {
1535
1536
  delete?: "noop" | "soft" | "tombstone" | undefined;
1536
1537
  exclude_fields?: string[] | undefined;
1538
+ emit_changes?: boolean | undefined;
1537
1539
  }, {
1538
1540
  delete?: "noop" | "soft" | "tombstone" | undefined;
1539
1541
  exclude_fields?: string[] | undefined;
1542
+ emit_changes?: boolean | undefined;
1540
1543
  }>>;
1541
1544
  }, "strip", z.ZodTypeAny, {
1542
1545
  electric: boolean;
@@ -1550,6 +1553,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
1550
1553
  sink?: {
1551
1554
  delete?: "noop" | "soft" | "tombstone" | undefined;
1552
1555
  exclude_fields?: string[] | undefined;
1556
+ emit_changes?: boolean | undefined;
1553
1557
  } | undefined;
1554
1558
  }, {
1555
1559
  electric?: boolean | undefined;
@@ -1563,6 +1567,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
1563
1567
  sink?: {
1564
1568
  delete?: "noop" | "soft" | "tombstone" | undefined;
1565
1569
  exclude_fields?: string[] | undefined;
1570
+ emit_changes?: boolean | undefined;
1566
1571
  } | undefined;
1567
1572
  }>>;
1568
1573
  detection: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<"mode", [z.ZodObject<{
@@ -2208,6 +2213,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
2208
2213
  sink?: {
2209
2214
  delete?: "noop" | "soft" | "tombstone" | undefined;
2210
2215
  exclude_fields?: string[] | undefined;
2216
+ emit_changes?: boolean | undefined;
2211
2217
  } | undefined;
2212
2218
  } | undefined;
2213
2219
  detection?: Record<string, {
@@ -2425,6 +2431,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
2425
2431
  sink?: {
2426
2432
  delete?: "noop" | "soft" | "tombstone" | undefined;
2427
2433
  exclude_fields?: string[] | undefined;
2434
+ emit_changes?: boolean | undefined;
2428
2435
  } | undefined;
2429
2436
  } | undefined;
2430
2437
  detection?: Record<string, {
@@ -2642,6 +2649,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
2642
2649
  sink?: {
2643
2650
  delete?: "noop" | "soft" | "tombstone" | undefined;
2644
2651
  exclude_fields?: string[] | undefined;
2652
+ emit_changes?: boolean | undefined;
2645
2653
  } | undefined;
2646
2654
  } | undefined;
2647
2655
  detection?: Record<string, {
@@ -2859,6 +2867,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
2859
2867
  sink?: {
2860
2868
  delete?: "noop" | "soft" | "tombstone" | undefined;
2861
2869
  exclude_fields?: string[] | undefined;
2870
+ emit_changes?: boolean | undefined;
2862
2871
  } | undefined;
2863
2872
  } | undefined;
2864
2873
  detection?: Record<string, {
@@ -3076,6 +3085,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
3076
3085
  sink?: {
3077
3086
  delete?: "noop" | "soft" | "tombstone" | undefined;
3078
3087
  exclude_fields?: string[] | undefined;
3088
+ emit_changes?: boolean | undefined;
3079
3089
  } | undefined;
3080
3090
  } | undefined;
3081
3091
  detection?: Record<string, {
@@ -3293,6 +3303,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
3293
3303
  sink?: {
3294
3304
  delete?: "noop" | "soft" | "tombstone" | undefined;
3295
3305
  exclude_fields?: string[] | undefined;
3306
+ emit_changes?: boolean | undefined;
3296
3307
  } | undefined;
3297
3308
  } | undefined;
3298
3309
  detection?: Record<string, {
@@ -3510,6 +3521,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
3510
3521
  sink?: {
3511
3522
  delete?: "noop" | "soft" | "tombstone" | undefined;
3512
3523
  exclude_fields?: string[] | undefined;
3524
+ emit_changes?: boolean | undefined;
3513
3525
  } | undefined;
3514
3526
  } | undefined;
3515
3527
  detection?: Record<string, {
@@ -3727,6 +3739,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.Z
3727
3739
  sink?: {
3728
3740
  delete?: "noop" | "soft" | "tombstone" | undefined;
3729
3741
  exclude_fields?: string[] | undefined;
3742
+ emit_changes?: boolean | undefined;
3730
3743
  } | undefined;
3731
3744
  } | undefined;
3732
3745
  detection?: Record<string, {
package/dist/src/index.js CHANGED
@@ -45,13 +45,13 @@ import {
45
45
  validateOrchestrationProject,
46
46
  validatePatternComposition,
47
47
  validatePatternProject
48
- } from "../chunk-4M66MQYA.js";
48
+ } from "../chunk-K4BQQ2NN.js";
49
49
  import "../chunk-KVOWSC5S.js";
50
- import "../chunk-QFUIE37H.js";
51
- import "../chunk-FFUDEIFF.js";
52
- import "../chunk-EO2QPOKH.js";
53
50
  import "../chunk-PRWIX6UW.js";
54
- import "../chunk-O2A6XHGD.js";
51
+ import "../chunk-KFXXOFDC.js";
52
+ import "../chunk-HN5HT5WL.js";
53
+ import "../chunk-EO2QPOKH.js";
54
+ import "../chunk-LLDJS7PJ.js";
55
55
  import "../chunk-HNWZFNKP.js";
56
56
  import "../chunk-AHV4GDYM.js";
57
57
  import "../chunk-SQDOBLBP.js";
@@ -62,8 +62,8 @@ import "../chunk-JEINYUJH.js";
62
62
  import "../chunk-5TK7MEN4.js";
63
63
  import "../chunk-4KNXX6TI.js";
64
64
  import "../chunk-3CJFPU6Q.js";
65
- import "../chunk-TDEHU73T.js";
66
- import "../chunk-S7C6TIIF.js";
65
+ import "../chunk-YIVQ7KLS.js";
66
+ import "../chunk-S5G3HO7N.js";
67
67
  import "../chunk-MZ6GV4YF.js";
68
68
  import "../chunk-LG57S2SC.js";
69
69
  import "../chunk-U64T4YZE.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pattern-stack/codegen",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Entity-driven code generation for full-stack TypeScript applications",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,8 +23,17 @@
23
23
  */
24
24
 
25
25
  import { randomUUID } from 'crypto';
26
+ import { Logger } from '@nestjs/common';
26
27
  import type { IEventBus, DomainEvent } from '../subsystems/events/event-bus.protocol';
27
28
 
29
+ /**
30
+ * Module-level logger for fire-and-forget emission failures. Routed through the
31
+ * Nest `Logger` (not bare `console`) so consumers configuring `app.useLogger`
32
+ * or the factory `logger:` option can format and filter it like any other
33
+ * framework log line.
34
+ */
35
+ const logger = new Logger('LifecycleEvents');
36
+
28
37
  // ============================================================================
29
38
  // Event categories (subset of pattern-stack's EventCategory)
30
39
  // ============================================================================
@@ -99,7 +108,17 @@ export function buildLifecycleEvent(
99
108
  aggregateType: entityName,
100
109
  payload: snapshot ? { snapshot } : {},
101
110
  occurredAt: new Date(),
102
- metadata: { category: 'lifecycle' as EventCategory },
111
+ // AUDIT tier: lifecycle/change events are untyped audit-trail records —
112
+ // never bridge-routed, no pool/direction. The `domain_events`
113
+ // `domain_events_tier_routing_check` CHECK requires `tier='audit' ⇔
114
+ // (pool IS NULL AND direction IS NULL)`; the DEFAULT `tier='domain'`
115
+ // (applied by toInsertValues when absent) requires non-null routing
116
+ // fields, so an un-tiered lifecycle row violates the constraint and the
117
+ // INSERT is rejected — silently, pre-fix, by emitSafely's catch. Stamp
118
+ // `tier:'audit'` so these rows land (and surface under the
119
+ // observability viewer's audit-tier toggle). The bridge guard keeps
120
+ // audit-tier events out of job routing.
121
+ metadata: { category: 'lifecycle' as EventCategory, tier: 'audit' },
103
122
  };
104
123
  }
105
124
 
@@ -119,7 +138,9 @@ export function buildChangeEvents(
119
138
  newValue: c.newValue,
120
139
  },
121
140
  occurredAt: new Date(),
122
- metadata: { category: 'change' as EventCategory },
141
+ // AUDIT tier see buildLifecycleEvent. Change events are audit-trail
142
+ // records; tier:'audit' satisfies the tier-routing CHECK constraint.
143
+ metadata: { category: 'change' as EventCategory, tier: 'audit' },
123
144
  }));
124
145
  }
125
146
 
@@ -144,9 +165,21 @@ export async function emitSafely(
144
165
  } else {
145
166
  await eventBus.publishMany(events);
146
167
  }
147
- } catch {
148
- // Log but never fail the CRUD operation.
149
- // In production, this would use a structured logger.
150
- console.warn(`[lifecycle-events] failed to emit ${events.length} event(s)`);
168
+ } catch (err) {
169
+ // Never fail the CRUD operation — but surface the cause. The bare
170
+ // `catch` that used to live here swallowed the error entirely, so a
171
+ // failing bus printed `failed to emit N event(s)` with zero
172
+ // diagnosability. Route through the Nest Logger (not bare console) at
173
+ // warn level, including the distinct event types and the error message;
174
+ // the stack follows at debug so it's available without noising the
175
+ // default-threshold output.
176
+ const message = err instanceof Error ? err.message : String(err);
177
+ const types = [...new Set(events.map((e) => e.type))].join(', ');
178
+ logger.warn(
179
+ `failed to emit ${events.length} event(s) [${types}]: ${message}`,
180
+ );
181
+ if (err instanceof Error && err.stack) {
182
+ logger.debug(err.stack);
183
+ }
151
184
  }
152
185
  }
@@ -71,6 +71,33 @@ export const eventRegistry = {
71
71
  version: 1,
72
72
  retry: { attempts: 3, backoff: 'exponential' },
73
73
  },
74
+ 'message_created': {
75
+ type: 'message_created',
76
+ tier: 'domain',
77
+ direction: 'change',
78
+ pool: 'events_change',
79
+ aggregate: 'message',
80
+ version: 1,
81
+ retry: { attempts: 3, backoff: 'exponential' },
82
+ },
83
+ 'message_deleted': {
84
+ type: 'message_deleted',
85
+ tier: 'domain',
86
+ direction: 'change',
87
+ pool: 'events_change',
88
+ aggregate: 'message',
89
+ version: 1,
90
+ retry: { attempts: 3, backoff: 'exponential' },
91
+ },
92
+ 'message_edited': {
93
+ type: 'message_edited',
94
+ tier: 'domain',
95
+ direction: 'change',
96
+ pool: 'events_change',
97
+ aggregate: 'message',
98
+ version: 1,
99
+ retry: { attempts: 3, backoff: 'exponential' },
100
+ },
74
101
  'stripe_payment_received': {
75
102
  type: 'stripe_payment_received',
76
103
  tier: 'domain',
@@ -39,6 +39,29 @@ export const dealStageChangedPayloadSchema = z.object({
39
39
  oldStage: z.string(),
40
40
  }).strict();
41
41
 
42
+ export const messageCreatedPayloadSchema = z.object({
43
+ changedFields: z.record(z.unknown()).nullable(),
44
+ entityId: z.string().uuid(),
45
+ externalId: z.string(),
46
+ provider: z.string(),
47
+ source: z.string(),
48
+ }).strict();
49
+
50
+ export const messageDeletedPayloadSchema = z.object({
51
+ entityId: z.string().uuid(),
52
+ externalId: z.string(),
53
+ provider: z.string(),
54
+ source: z.string(),
55
+ }).strict();
56
+
57
+ export const messageEditedPayloadSchema = z.object({
58
+ changedFields: z.record(z.unknown()).nullable(),
59
+ entityId: z.string().uuid(),
60
+ externalId: z.string(),
61
+ provider: z.string(),
62
+ source: z.string(),
63
+ }).strict();
64
+
42
65
  export const stripePaymentReceivedPayloadSchema = z.object({
43
66
  amountCents: z.number(),
44
67
  currency: z.string(),
@@ -60,6 +83,9 @@ export const eventPayloadSchemas = {
60
83
  'crm_sync_started': crmSyncStartedPayloadSchema,
61
84
  'deal_created': dealCreatedPayloadSchema,
62
85
  'deal_stage_changed': dealStageChangedPayloadSchema,
86
+ 'message_created': messageCreatedPayloadSchema,
87
+ 'message_deleted': messageDeletedPayloadSchema,
88
+ 'message_edited': messageEditedPayloadSchema,
63
89
  'stripe_payment_received': stripePaymentReceivedPayloadSchema,
64
90
  'webhook_outbound_contact_sync': webhookOutboundContactSyncPayloadSchema,
65
91
  } as const satisfies Record<EventTypeName, z.ZodType>;
@@ -64,6 +64,55 @@ export interface DealStageChangedEvent extends DomainEvent {
64
64
  };
65
65
  }
66
66
 
67
+ export interface MessageCreatedEvent extends DomainEvent {
68
+ readonly type: 'message_created';
69
+ readonly aggregateType: 'message';
70
+ readonly payload: {
71
+ /** Differ's per-field before/after map (same value as integration_run_items.changed_fields). */
72
+ changedFields: Record<string, unknown> | null;
73
+ /** Local aggregate id the sink wrote/soft-deleted. */
74
+ entityId: string;
75
+ /** Vendor external id the change keyed on. */
76
+ externalId: string;
77
+ /** Provider label (e.g. 'slack', 'google'). */
78
+ provider: string;
79
+ /** Provenance marker — always 'integration'. A write-back action reads this to avoid echoing the change back to the vendor. */
80
+ source: string;
81
+ };
82
+ }
83
+
84
+ export interface MessageDeletedEvent extends DomainEvent {
85
+ readonly type: 'message_deleted';
86
+ readonly aggregateType: 'message';
87
+ readonly payload: {
88
+ /** Local aggregate id the sink wrote/soft-deleted. */
89
+ entityId: string;
90
+ /** Vendor external id the change keyed on. */
91
+ externalId: string;
92
+ /** Provider label (e.g. 'slack', 'google'). */
93
+ provider: string;
94
+ /** Provenance marker — always 'integration'. A write-back action reads this to avoid echoing the change back to the vendor. */
95
+ source: string;
96
+ };
97
+ }
98
+
99
+ export interface MessageEditedEvent extends DomainEvent {
100
+ readonly type: 'message_edited';
101
+ readonly aggregateType: 'message';
102
+ readonly payload: {
103
+ /** Differ's per-field before/after map (same value as integration_run_items.changed_fields). */
104
+ changedFields: Record<string, unknown> | null;
105
+ /** Local aggregate id the sink wrote/soft-deleted. */
106
+ entityId: string;
107
+ /** Vendor external id the change keyed on. */
108
+ externalId: string;
109
+ /** Provider label (e.g. 'slack', 'google'). */
110
+ provider: string;
111
+ /** Provenance marker — always 'integration'. A write-back action reads this to avoid echoing the change back to the vendor. */
112
+ source: string;
113
+ };
114
+ }
115
+
67
116
  /** Stripe charge.succeeded webhook, post-signature-verification. */
68
117
  export interface StripePaymentReceivedEvent extends DomainEvent {
69
118
  readonly type: 'stripe_payment_received';
@@ -97,6 +146,9 @@ export type AppDomainEvent =
97
146
  | CrmSyncStartedEvent
98
147
  | DealCreatedEvent
99
148
  | DealStageChangedEvent
149
+ | MessageCreatedEvent
150
+ | MessageDeletedEvent
151
+ | MessageEditedEvent
100
152
  | StripePaymentReceivedEvent
101
153
  | WebhookOutboundContactSyncEvent;
102
154
 
@@ -12,6 +12,17 @@ export { EVENT_BUS } from './events';
12
12
  // mode reaches it via `@shared/subsystems/events`; both must export it.
13
13
  export type { DomainEvent, IEventBus, DrizzleTransaction } from './events';
14
14
  export { EventsModule, DrizzleEventBus, MemoryEventBus } from './events';
15
+ // The `TYPED_EVENT_BUS` token is re-exported here so package-mode generated code
16
+ // that publishes events from the single `@pattern-stack/codegen/subsystems`
17
+ // barrel resolves it — notably the EMIT-CHANGES integration change-emitter
18
+ // (`<entity>.change-emitter.ts`), which injects `TYPED_EVENT_BUS`. The emitter
19
+ // types the bus with a local structural `publish` shape (NOT the package's
20
+ // `TypedEventBus`, whose `EventTypeName` union is the package's own events — the
21
+ // consumer's `<entity>_*` events live in the CONSUMER registry), so only the
22
+ // token needs forwarding. The CONSUMER binds their generated `TypedEventBus` to
23
+ // the token via `EventsModule.forRoot()`. Vendored mode reaches it via
24
+ // `@shared/subsystems/events`.
25
+ export { TYPED_EVENT_BUS } from './events';
15
26
 
16
27
  // Jobs — orchestration schema only (JOB-1). Protocols / modules land in JOB-2 / JOB-5.
17
28
  export { jobs, jobRuns, jobSteps } from './jobs';
@@ -131,6 +142,18 @@ export {
131
142
  } from './integration';
132
143
  export type { IIntegrationSink } from './integration';
133
144
 
145
+ // Integration — EMIT-CHANGES seam. The generated per-entity change-emitter
146
+ // (`<entity>.change-emitter.ts`) imports the `INTEGRATION_CHANGE_EMITTER` token
147
+ // + the `IIntegrationChangeEmitter` port + `IntegrationChangeNotification` from
148
+ // `@pattern-stack/codegen/subsystems`. Forwarded here so the emitted opt-in
149
+ // emitter resolves them across the package boundary (package mode).
150
+ export { INTEGRATION_CHANGE_EMITTER } from './integration';
151
+ export type {
152
+ IIntegrationChangeEmitter,
153
+ IntegrationChangeAction,
154
+ IntegrationChangeNotification,
155
+ } from './integration';
156
+
134
157
  // Auth
135
158
  export {
136
159
  ENCRYPTION_KEY,
@@ -50,8 +50,13 @@ import type { ICursorStore } from './integration-cursor-store.protocol';
50
50
  import type { IFieldDiffer, FieldDiff } from './integration-field-diff.protocol';
51
51
  import type { IIntegrationSink } from './integration-sink.protocol';
52
52
  import type { IIntegrationRunRecorder } from './integration-run-recorder.protocol';
53
+ import type {
54
+ IIntegrationChangeEmitter,
55
+ IntegrationChangeAction,
56
+ } from './integration-change-emitter.protocol';
53
57
  import { assertTenantId } from './integration-errors';
54
58
  import {
59
+ INTEGRATION_CHANGE_EMITTER,
55
60
  INTEGRATION_CHANGE_SOURCE,
56
61
  INTEGRATION_CURSOR_STORE,
57
62
  INTEGRATION_FIELD_DIFFER,
@@ -118,6 +123,13 @@ export class ExecuteIntegrationUseCase<T extends Record<string, unknown>> {
118
123
  @Optional()
119
124
  @Inject(INTEGRATION_MULTI_TENANT)
120
125
  private readonly multiTenant: boolean = false,
126
+ // EMIT-CHANGES seam — optional post-upsert domain-event emitter. Bound only
127
+ // by codegen-emitted assemblies whose entity opts into
128
+ // `integration.sink.emit_changes: true`. Unbound (the default) ⇒ no events
129
+ // published, zero behavior change.
130
+ @Optional()
131
+ @Inject(INTEGRATION_CHANGE_EMITTER)
132
+ private readonly emitter: IIntegrationChangeEmitter | null = null,
121
133
  ) {}
122
134
 
123
135
  async execute(input: ExecuteIntegrationInput<T>): Promise<ExecuteIntegrationResult> {
@@ -263,6 +275,12 @@ export class ExecuteIntegrationUseCase<T extends Record<string, unknown>> {
263
275
  changedFields: {},
264
276
  tenantId: input.tenantId,
265
277
  });
278
+ // EMIT-CHANGES: a real tombstone (a local row existed and was soft-deleted)
279
+ // is a `<entity>_deleted` event. A delete that hit no local row is a noop —
280
+ // nothing changed, nothing to emit.
281
+ if (result) {
282
+ await this.emitChange(input, change.externalId, result.id, 'deleted');
283
+ }
266
284
  return;
267
285
  }
268
286
 
@@ -321,15 +339,65 @@ export class ExecuteIntegrationUseCase<T extends Record<string, unknown>> {
321
339
  input.provider,
322
340
  );
323
341
 
342
+ const action: IntegrationChangeAction =
343
+ existing === null ? 'created' : 'updated';
344
+
324
345
  await this.recorder.recordItem({
325
346
  integrationRunId: runId,
326
347
  entityType: input.subscription.domain,
327
348
  externalId: change.externalId,
328
349
  localId,
329
- operation: existing === null ? 'created' : 'updated',
350
+ operation: action,
330
351
  status: 'success',
331
352
  changedFields: diff as FieldDiff,
332
353
  tenantId: input.tenantId,
333
354
  });
355
+
356
+ // EMIT-CHANGES: a real create/update (the diff was NOT noop) publishes a
357
+ // typed `<entity>_created` / `<entity>_edited` event. The noop-reproject path
358
+ // above intentionally does NOT emit — the canonical state is unchanged.
359
+ await this.emitChange(
360
+ input,
361
+ change.externalId,
362
+ localId,
363
+ action,
364
+ diff as FieldDiff,
365
+ );
366
+ }
367
+
368
+ /**
369
+ * Publish one typed data-level change event via the optional emitter
370
+ * (EMIT-CHANGES seam). No-op when no emitter is bound (the back-compat
371
+ * default). A failed publish is logged but never aborts the run — the row is
372
+ * already written; emission is best-effort (the outbox tx, when the generated
373
+ * adapter rides on one, gives the at-least-once guarantee, not this try/catch).
374
+ */
375
+ private async emitChange(
376
+ input: ExecuteIntegrationInput<T>,
377
+ externalId: string,
378
+ entityId: string,
379
+ action: IntegrationChangeAction,
380
+ changedFields?: FieldDiff,
381
+ ): Promise<void> {
382
+ if (this.emitter === null) return;
383
+ try {
384
+ await this.emitter.emitChange({
385
+ entityId,
386
+ externalId,
387
+ provider: input.provider,
388
+ action,
389
+ changedFields:
390
+ action === 'deleted'
391
+ ? undefined
392
+ : (changedFields as Record<string, unknown> | undefined),
393
+ tenantId: input.tenantId,
394
+ });
395
+ } catch (err) {
396
+ const message = err instanceof Error ? err.message : String(err);
397
+ this.logger.warn(
398
+ `integration change-emit failed: subscription=${input.subscription.id} ` +
399
+ `externalId=${externalId} action=${action}: ${message}`,
400
+ );
401
+ }
334
402
  }
335
403
  }
@@ -35,6 +35,11 @@ export {
35
35
  FieldDiffValueSchema,
36
36
  } from './integration-field-diff.protocol';
37
37
  export type { IIntegrationSink } from './integration-sink.protocol';
38
+ export type {
39
+ IIntegrationChangeEmitter,
40
+ IntegrationChangeAction,
41
+ IntegrationChangeNotification,
42
+ } from './integration-change-emitter.protocol';
38
43
  export type {
39
44
  CompleteRunInput,
40
45
  IIntegrationRunRecorder,
@@ -125,6 +130,7 @@ export { buildChangeSource } from './build-change-source';
125
130
  // Tokens
126
131
  export {
127
132
  ENTITY_CHANGE_SOURCE_REGISTRY,
133
+ INTEGRATION_CHANGE_EMITTER,
128
134
  INTEGRATION_CHANGE_SOURCE,
129
135
  INTEGRATION_CURSOR_STORE,
130
136
  INTEGRATION_FIELD_DIFFER,
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Integration subsystem — change-emitter protocol (port).
3
+ *
4
+ * `IIntegrationChangeEmitter` is the OPT-IN seam through which the generic
5
+ * orchestrator (`ExecuteIntegrationUseCase`) publishes a typed, data-level
6
+ * domain event after every sink write/soft-delete. It is the upstream-generalized
7
+ * form of the per-sink event emission swe-brain hand-built (ADR-0009 Amendment B):
8
+ * the differ already records `changed_fields` on `integration_run_items`, but
9
+ * nothing publishes a "this entity changed" domain event for downstream
10
+ * trigger→action consumers. This port closes that gap.
11
+ *
12
+ * ## Why a port (not a direct TypedEventBus call in the orchestrator)
13
+ *
14
+ * The orchestrator is strictly provider- AND entity-agnostic: `entityType` is a
15
+ * bare `string` and the canonical record is generic `T` (see the use-case header
16
+ * "No CRM bleed"). It therefore cannot know the typed event NAME
17
+ * (`<entity>_created`) or the typed payload shape at compile time. The typed
18
+ * knowledge lives at the per-entity assembly wiring (codegen knows the entity
19
+ * name there). So the orchestrator depends on this thin, untyped port; codegen
20
+ * binds a per-entity adapter that maps `(operation) → <entity>_<verb>` and calls
21
+ * the project's generated `TypedEventBus.publish(...)` with the typed payload.
22
+ *
23
+ * ## Backwards compatibility
24
+ *
25
+ * The port is `@Optional()` on the orchestrator. Entities that do NOT opt in
26
+ * (`integration.sink.emit_changes` absent/false) bind no emitter, so the
27
+ * orchestrator's `this.emitter` is `undefined` and NOTHING is published — zero
28
+ * behavior change. This is the invariant the snapshot fixture (which opts none
29
+ * in) keeps green.
30
+ *
31
+ * ## Provenance — loop-breaking
32
+ *
33
+ * Every emitted event carries `source: 'integration'` in its payload. A future
34
+ * write-back action (the Intervention layer in swe-brain terms) that subscribes
35
+ * to these events can detect `source === 'integration'` and decline to echo the
36
+ * change back to the vendor, breaking the inbound→writeback→inbound loop. This
37
+ * is the data-layer counterpart of the loopback middleware that already guards
38
+ * the read side (`createLoopbackMiddleware`).
39
+ *
40
+ * ## Transactionality
41
+ *
42
+ * `emitChange` receives the same `tx` the sink wrote under (when the sink exposes
43
+ * one — today the sink owns its own transaction internally, so `tx` is reserved
44
+ * for the future where the orchestrator drives the transaction). The generated
45
+ * adapter forwards `tx` into `TypedEventBus.publish(type, id, payload, { tx })`,
46
+ * so the event lands in the outbox iff the row commits (the events subsystem's
47
+ * outbox guarantee). When `tx` is absent the publish is post-commit best-effort,
48
+ * matching today's sink-owns-its-own-transaction reality.
49
+ */
50
+
51
+ /** The data-level action the orchestrator observed. Maps onto the generated
52
+ * event verb: `created → <entity>_created`, `updated → <entity>_edited`
53
+ * (per swe-brain ADR-0009 B1 — `_edited`, never `_updated`),
54
+ * `deleted → <entity>_deleted` (tombstone soft-delete). `noop` never emits. */
55
+ export type IntegrationChangeAction = 'created' | 'updated' | 'deleted';
56
+
57
+ /**
58
+ * The vendor-blind change descriptor the orchestrator hands the emitter. The
59
+ * generated adapter reshapes this into the typed `<entity>_<verb>` payload
60
+ * (`{ entityId, externalId, provider, changedFields?, source: 'integration' }`).
61
+ */
62
+ export interface IntegrationChangeNotification {
63
+ /** The local row id the sink wrote/soft-deleted (the domain aggregate id —
64
+ * becomes `aggregateId` on the published event AND `entityId` in the payload). */
65
+ readonly entityId: string;
66
+ /** Vendor-prefixed-or-bare external id the change keyed on (e.g. `slack:123`). */
67
+ readonly externalId: string;
68
+ /** Provider label from `ExecuteIntegrationInput.provider` (e.g. `'slack'`). */
69
+ readonly provider: string;
70
+ /** The observed action. `created`/`updated` come from the existing-row check;
71
+ * `deleted` from the soft-delete path. */
72
+ readonly action: IntegrationChangeAction;
73
+ /** The differ's structured per-field before/after map (the same value written
74
+ * to `integration_run_items.changed_fields`). Absent on deletes. */
75
+ readonly changedFields?: Record<string, unknown>;
76
+ /** Multi-tenant deployments thread the tenant id through to the event metadata. */
77
+ readonly tenantId?: string | null;
78
+ /** The transaction the sink wrote under, when the orchestrator drives one.
79
+ * Forwarded to `TypedEventBus.publish(..., { tx })` for the outbox guarantee.
80
+ * Reserved: today the sink owns its own transaction, so this is usually
81
+ * `undefined` and the publish is post-commit. Typed `unknown` here so the
82
+ * port stays free of a Drizzle type dependency (the generated adapter narrows). */
83
+ readonly tx?: unknown;
84
+ }
85
+
86
+ /**
87
+ * Post-upsert change-event emission port.
88
+ *
89
+ * One implementation per opted-in (entity, provider) assembly — codegen-emitted,
90
+ * bound to `INTEGRATION_CHANGE_EMITTER` in that assembly module. The orchestrator
91
+ * injects it `@Optional()`; an unbound token means no emission (back-compat).
92
+ */
93
+ export interface IIntegrationChangeEmitter {
94
+ /**
95
+ * Publish the typed `<entity>_<verb>` domain event for one observed change.
96
+ *
97
+ * MUST be called only for real changes — the orchestrator never calls this on
98
+ * a `noop` diff (canonical state unchanged) or a delete that hit no local row
99
+ * (no tombstone created). Implementations should treat a call as "this thing
100
+ * happened" and publish unconditionally.
101
+ *
102
+ * Errors are the orchestrator's concern: it wraps the call so a failed publish
103
+ * does not abort the run (the row is already written; emission is best-effort
104
+ * unless ridden on the outbox tx). See `ExecuteIntegrationUseCase.processChange`.
105
+ */
106
+ emitChange(notification: IntegrationChangeNotification): Promise<void>;
107
+ }
@@ -25,6 +25,17 @@ export const INTEGRATION_CURSOR_STORE = 'INTEGRATION_CURSOR_STORE' as const;
25
25
  export const INTEGRATION_FIELD_DIFFER = 'INTEGRATION_FIELD_DIFFER' as const;
26
26
  export const INTEGRATION_SINK = 'INTEGRATION_SINK' as const;
27
27
 
28
+ /**
29
+ * Optional post-upsert change-event emitter token (EMIT-CHANGES seam).
30
+ *
31
+ * Backed by `IIntegrationChangeEmitter`. Bound ONLY by codegen-emitted assembly
32
+ * modules whose entity opts in via `integration.sink.emit_changes: true`. The
33
+ * orchestrator injects it `@Optional()` — an unbound token means no domain
34
+ * events are published (the back-compat default for non-opted-in entities).
35
+ * See `integration-change-emitter.protocol.ts`.
36
+ */
37
+ export const INTEGRATION_CHANGE_EMITTER = 'INTEGRATION_CHANGE_EMITTER' as const;
38
+
28
39
  /**
29
40
  * Run-recorder token (SYNC-5). Backed by `IIntegrationRunRecorder`. Drizzle impl
30
41
  * lands in SYNC-4; tests provide inline fakes.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  to: "<%= schemaPath %>"
3
3
  force: true
4
+ skip_if: "<%= skipSchema %>"
4
5
  ---
5
6
  <%- typeof generatedBanner !== 'undefined' ? generatedBanner : '' %>
6
7
  /**
@@ -7,5 +7,5 @@ skip_if: "<%= mainHookInjected %>"
7
7
  // JOBS — Embedded worker mode (optional)
8
8
  // To run the job worker in-process (single-process deploy), add to AppModule imports:
9
9
  // JobWorkerModule.forRoot({ mode: 'embedded' })
10
- // For standalone worker (separate process), use worker.ts at the project root.
10
+ // For standalone worker (separate process), run src/worker.ts (bun src/worker.ts).
11
11
  // See codegen.config.yaml jobs.worker_mode to toggle the documented default.