mcp-event-intelligence 0.1.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 (63) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/LICENSE +201 -0
  3. package/README.md +298 -0
  4. package/SECURITY.md +42 -0
  5. package/bin/mcp-event-intelligence.mjs +52 -0
  6. package/dist/src/composite/engine.d.ts +87 -0
  7. package/dist/src/composite/engine.js +667 -0
  8. package/dist/src/composite/temporal.d.ts +32 -0
  9. package/dist/src/composite/temporal.js +317 -0
  10. package/dist/src/intelligenceProtocol/audit.d.ts +19 -0
  11. package/dist/src/intelligenceProtocol/audit.js +56 -0
  12. package/dist/src/intelligenceProtocol/canonical.d.ts +2 -0
  13. package/dist/src/intelligenceProtocol/canonical.js +21 -0
  14. package/dist/src/intelligenceProtocol/index.d.ts +12 -0
  15. package/dist/src/intelligenceProtocol/index.js +8 -0
  16. package/dist/src/intelligenceProtocol/invariants.d.ts +3 -0
  17. package/dist/src/intelligenceProtocol/invariants.js +36 -0
  18. package/dist/src/intelligenceProtocol/lifecycle.d.ts +4 -0
  19. package/dist/src/intelligenceProtocol/lifecycle.js +24 -0
  20. package/dist/src/intelligenceProtocol/mapper.d.ts +14 -0
  21. package/dist/src/intelligenceProtocol/mapper.js +28 -0
  22. package/dist/src/intelligenceProtocol/schemas.d.ts +325 -0
  23. package/dist/src/intelligenceProtocol/schemas.js +182 -0
  24. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +488 -0
  25. package/dist/src/intelligenceProtocol/triggerSchemas.js +379 -0
  26. package/dist/src/intelligenceProtocol/version.d.ts +7 -0
  27. package/dist/src/intelligenceProtocol/version.js +7 -0
  28. package/dist/src/mcpEvents/consumer.d.ts +7 -0
  29. package/dist/src/mcpEvents/consumer.js +14 -0
  30. package/dist/src/mcpEvents/server.d.ts +41 -0
  31. package/dist/src/mcpEvents/server.js +122 -0
  32. package/dist/src/protocol/types.d.ts +37 -0
  33. package/dist/src/protocol/types.js +1 -0
  34. package/dist/src/semantic/conditionEngine.d.ts +7 -0
  35. package/dist/src/semantic/conditionEngine.js +65 -0
  36. package/dist/src/semantic/types.d.ts +31 -0
  37. package/dist/src/semantic/types.js +1 -0
  38. package/dist/src/semantic/typesafeJevEvaluator.d.ts +23 -0
  39. package/dist/src/semantic/typesafeJevEvaluator.js +95 -0
  40. package/package.json +85 -0
  41. package/scripts/host-integration.d.mts +86 -0
  42. package/scripts/host-integration.mjs +216 -0
  43. package/scripts/lib/composite-event-consumer.mjs +172 -0
  44. package/scripts/lib/composite-wake-coordinator.mjs +185 -0
  45. package/scripts/lib/derived-contract-registry.mjs +210 -0
  46. package/scripts/lib/derived-event-coordinator.mjs +242 -0
  47. package/scripts/lib/event-processor.mjs +409 -0
  48. package/scripts/lib/generic-runtime-wake.mjs +76 -0
  49. package/scripts/lib/github-mcp-events-adapter.mjs +66 -0
  50. package/scripts/lib/github-webhook.mjs +88 -0
  51. package/scripts/lib/http-wake-deliverer.mjs +31 -0
  52. package/scripts/lib/local-event-intelligence-runtime.mjs +193 -0
  53. package/scripts/lib/mcp-events-client.mjs +519 -0
  54. package/scripts/lib/persistent-event-store.mjs +769 -0
  55. package/scripts/lib/temporal-deadline-scheduler.mjs +111 -0
  56. package/scripts/lib/trigger-control-plane.mjs +939 -0
  57. package/scripts/lib/trigger-inspector.mjs +631 -0
  58. package/scripts/lib/wake-signature.mjs +10 -0
  59. package/scripts/mcp-stdio-server.mjs +380 -0
  60. package/scripts/mcp-stdio.mjs +3 -0
  61. package/scripts/run-tests.mjs +51 -0
  62. package/scripts/service.mjs +756 -0
  63. package/server.json +68 -0
@@ -0,0 +1,939 @@
1
+ import {
2
+ parseCompositeTriggerDefinition,
3
+ sha256Hex,
4
+ } from '../../dist/src/intelligenceProtocol/index.js';
5
+ import {
6
+ DerivedContractRegistry,
7
+ } from './derived-contract-registry.mjs';
8
+
9
+ function actorIdentity(actor) {
10
+ if (!actor || typeof actor !== 'object') {
11
+ throw new Error('actor is required');
12
+ }
13
+ const type = String(actor.type || '');
14
+ const principalId = String(actor.principal_id || '');
15
+ if (!type || !principalId) {
16
+ throw new Error('actor.type and actor.principal_id are required');
17
+ }
18
+ return {
19
+ type,
20
+ principal_id: principalId,
21
+ ...(actor.tenant_id ? { tenant_id: String(actor.tenant_id) } : {}),
22
+ };
23
+ }
24
+
25
+ function samePrincipal(left, right) {
26
+ if (!left || !right) return false;
27
+ return (
28
+ String(left.type || '') === String(right.type || '') &&
29
+ String(left.principal_id || '') === String(right.principal_id || '') &&
30
+ String(left.tenant_id || '') === String(right.tenant_id || '')
31
+ );
32
+ }
33
+
34
+ async function stableId(prefix, input) {
35
+ const hash = await sha256Hex(input);
36
+ return `${prefix}_${hash.slice(0, 24)}`;
37
+ }
38
+
39
+ function sourceKey(eventName, serverId) {
40
+ return `${String(serverId || '')}::${String(eventName || '')}`;
41
+ }
42
+
43
+ const DERIVED_EVENT_SERVER_ID = 'event-intelligence:derived';
44
+ const DERIVED_EVENT_CONNECTION_ID = 'event-intelligence:derived';
45
+
46
+ function getSchemaNode(schema, path) {
47
+ if (!schema || typeof schema !== 'object') return null;
48
+ let node = schema;
49
+ for (const part of String(path).split('.')) {
50
+ if (!node || typeof node !== 'object') return null;
51
+ const properties = node.properties;
52
+ if (!properties || typeof properties !== 'object' || !(part in properties)) {
53
+ return null;
54
+ }
55
+ node = properties[part];
56
+ }
57
+ return node;
58
+ }
59
+
60
+ function scalarSchemaForConstant(value) {
61
+ if (typeof value === 'string') return { type: 'string' };
62
+ if (typeof value === 'boolean') return { type: 'boolean' };
63
+ if (typeof value === 'number') {
64
+ return Number.isInteger(value) ? { type: 'integer' } : { type: 'number' };
65
+ }
66
+ return {};
67
+ }
68
+
69
+ function assertAdvertisedPath(source, path, purpose) {
70
+ const payloadSchema = source?.payloadSchema;
71
+ if (
72
+ !payloadSchema ||
73
+ typeof payloadSchema !== 'object' ||
74
+ Object.keys(payloadSchema).length === 0
75
+ ) {
76
+ return;
77
+ }
78
+
79
+ if (!getSchemaNode(payloadSchema, path)) {
80
+ const error = new Error(
81
+ `${purpose} field unavailable: ${source.serverId}/${source.eventName}.${path}`,
82
+ );
83
+ error.code = 'TRIGGER_SOURCE_FIELD_UNAVAILABLE';
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ export class TriggerControlPlane {
89
+ constructor({
90
+ store,
91
+ triggerEngine,
92
+ now = () => new Date(),
93
+ }) {
94
+ this.store = store;
95
+ this.triggerEngine = triggerEngine;
96
+ this.now = now;
97
+ this.contractRegistry = new DerivedContractRegistry({
98
+ store,
99
+ now,
100
+ audit: (entry) => this.audit(entry),
101
+ });
102
+ }
103
+
104
+ listEventSources({ connectionIds } = {}) {
105
+ return this.store.listEventSources({
106
+ connectionIds,
107
+ enabledOnly: true,
108
+ });
109
+ }
110
+
111
+ listTriggers({ owner: ownerInput } = {}) {
112
+ const owner = ownerInput ? actorIdentity(ownerInput) : null;
113
+ return this.store.listTriggers()
114
+ .map((definition) => ({
115
+ definition,
116
+ state: this.store.getTriggerState(
117
+ definition.triggerId,
118
+ definition.version,
119
+ ),
120
+ }))
121
+ .filter((entry) => !owner || samePrincipal(entry.state.owner, owner));
122
+ }
123
+
124
+ async registerEventSource(input, actorInput) {
125
+ const actor = actorIdentity(actorInput);
126
+ const source = await this.store.putEventSource(input);
127
+
128
+ await this.audit({
129
+ kind: 'event_source.registered',
130
+ entityType: 'event_source',
131
+ entityId: source.sourceId,
132
+ traceId: `event-source:${source.sourceId}`,
133
+ details: {
134
+ actor,
135
+ connectionId: source.connectionId,
136
+ serverId: source.serverId,
137
+ eventName: source.eventName,
138
+ },
139
+ });
140
+
141
+ return source;
142
+ }
143
+
144
+ async createTrigger({
145
+ definition: definitionInput,
146
+ connectionIds,
147
+ actor: actorInput,
148
+ owner: ownerInput,
149
+ confirmationId,
150
+ }) {
151
+ const actor = actorIdentity(actorInput);
152
+ const owner = actorIdentity(ownerInput);
153
+ if (
154
+ actor.type === 'agent' &&
155
+ (typeof confirmationId !== 'string' || !confirmationId.trim())
156
+ ) {
157
+ const error = new Error(
158
+ 'Agent-authored durable triggers require explicit confirmationId',
159
+ );
160
+ error.code = 'TRIGGER_CONFIRMATION_REQUIRED';
161
+ throw error;
162
+ }
163
+
164
+ const definition = parseCompositeTriggerDefinition(definitionInput);
165
+ const allowedConnectionIds = Array.isArray(connectionIds)
166
+ ? [...new Set(connectionIds.map(String).filter(Boolean))]
167
+ : [];
168
+
169
+ if (!allowedConnectionIds.length) {
170
+ const error = new Error(
171
+ 'At least one active event-source connection is required',
172
+ );
173
+ error.code = 'TRIGGER_CONNECTION_REQUIRED';
174
+ throw error;
175
+ }
176
+
177
+ this.assertSourcesAvailable(definition, allowedConnectionIds);
178
+
179
+ if (this.store.listTriggers().some((candidate) =>
180
+ candidate.triggerId === definition.triggerId &&
181
+ candidate.version === definition.version
182
+ )) {
183
+ const error = new Error(
184
+ `Trigger ${definition.triggerId}@${definition.version} already exists`,
185
+ );
186
+ error.code = 'TRIGGER_ALREADY_EXISTS';
187
+ throw error;
188
+ }
189
+
190
+ await this.assertDerivedOutputContract(definition);
191
+
192
+ const stored = await this.triggerEngine.register(definition);
193
+ const state = await this.store.setTriggerState(
194
+ stored.triggerId,
195
+ stored.version,
196
+ 'active',
197
+ actor,
198
+ {
199
+ owner,
200
+ connectionIds: allowedConnectionIds,
201
+ },
202
+ );
203
+
204
+ await this.registerDerivedOutputSource(stored);
205
+
206
+ const receiptId = await stableId(
207
+ 'trigger_receipt',
208
+ `${stored.triggerId}:${stored.version}:create:${confirmationId || actor.principal_id}`,
209
+ );
210
+
211
+ await this.audit({
212
+ kind: 'trigger.created',
213
+ entityType: 'trigger',
214
+ entityId: stored.triggerId,
215
+ traceId: `trigger:${stored.triggerId}`,
216
+ details: {
217
+ actor,
218
+ owner,
219
+ version: stored.version,
220
+ connectionIds: allowedConnectionIds,
221
+ confirmationId: confirmationId || null,
222
+ receiptId,
223
+ clauseSources: stored.clauses.map((clause) => ({
224
+ clauseId: clause.id,
225
+ serverId: clause.serverId,
226
+ eventName: clause.event,
227
+ })),
228
+ },
229
+ });
230
+
231
+ return {
232
+ receiptId,
233
+ action: 'create',
234
+ definition: stored,
235
+ state,
236
+ };
237
+ }
238
+
239
+ async updateTrigger({
240
+ triggerId,
241
+ expectedVersion,
242
+ definition: definitionInput,
243
+ connectionIds,
244
+ actor: actorInput,
245
+ owner: ownerInput,
246
+ confirmationId,
247
+ }) {
248
+ const actor = actorIdentity(actorInput);
249
+ const owner = actorIdentity(ownerInput);
250
+
251
+ if (
252
+ actor.type === 'agent' &&
253
+ (typeof confirmationId !== 'string' || !confirmationId.trim())
254
+ ) {
255
+ const error = new Error(
256
+ 'Agent-authored trigger mutations require explicit confirmationId',
257
+ );
258
+ error.code = 'TRIGGER_CONFIRMATION_REQUIRED';
259
+ throw error;
260
+ }
261
+
262
+ const current = this.store.listTriggers().find(
263
+ (candidate) =>
264
+ candidate.triggerId === triggerId &&
265
+ candidate.version === expectedVersion,
266
+ );
267
+ if (!current) {
268
+ const error = new Error(
269
+ `Expected trigger version not found: ${triggerId}@${expectedVersion}`,
270
+ );
271
+ error.code = 'TRIGGER_VERSION_CONFLICT';
272
+ throw error;
273
+ }
274
+
275
+ const currentState = this.store.getTriggerState(
276
+ triggerId,
277
+ expectedVersion,
278
+ );
279
+ if (!samePrincipal(currentState.owner, owner)) {
280
+ const error = new Error('Trigger owner mismatch');
281
+ error.code = 'TRIGGER_OWNER_MISMATCH';
282
+ throw error;
283
+ }
284
+ if (!['active', 'paused'].includes(currentState.status)) {
285
+ const error = new Error(
286
+ `Trigger version is no longer mutable: ${currentState.status}`,
287
+ );
288
+ error.code = 'TRIGGER_VERSION_CONFLICT';
289
+ throw error;
290
+ }
291
+
292
+ const competing = this.store.listTriggers()
293
+ .filter((candidate) => candidate.triggerId === triggerId)
294
+ .find((candidate) => {
295
+ if (candidate.version === expectedVersion) return false;
296
+ const state = this.store.getTriggerState(
297
+ candidate.triggerId,
298
+ candidate.version,
299
+ );
300
+ return state.status === 'active' || state.status === 'paused';
301
+ });
302
+ if (competing) {
303
+ const error = new Error(
304
+ `Another trigger version is current: ${triggerId}@${competing.version}`,
305
+ );
306
+ error.code = 'TRIGGER_VERSION_CONFLICT';
307
+ throw error;
308
+ }
309
+
310
+ const definition = parseCompositeTriggerDefinition(definitionInput);
311
+ if (definition.triggerId !== triggerId) {
312
+ const error = new Error('Updated definition must keep triggerId');
313
+ error.code = 'TRIGGER_ID_IMMUTABLE';
314
+ throw error;
315
+ }
316
+ if (definition.version === expectedVersion) {
317
+ const error = new Error('Updated definition requires a new version');
318
+ error.code = 'TRIGGER_NEW_VERSION_REQUIRED';
319
+ throw error;
320
+ }
321
+ if (this.store.listTriggers().some((candidate) =>
322
+ candidate.triggerId === triggerId &&
323
+ candidate.version === definition.version
324
+ )) {
325
+ const error = new Error(
326
+ `Trigger ${triggerId}@${definition.version} already exists`,
327
+ );
328
+ error.code = 'TRIGGER_ALREADY_EXISTS';
329
+ throw error;
330
+ }
331
+
332
+ const allowedConnectionIds = Array.isArray(connectionIds) && connectionIds.length
333
+ ? [...new Set(connectionIds.map(String).filter(Boolean))]
334
+ : currentState.connectionIds;
335
+
336
+ this.assertSourcesAvailable(definition, allowedConnectionIds);
337
+
338
+ await this.retireMatches(
339
+ triggerId,
340
+ expectedVersion,
341
+ 'superseded_by_update',
342
+ );
343
+ const retiredState = await this.store.setTriggerState(
344
+ triggerId,
345
+ expectedVersion,
346
+ 'completed',
347
+ actor,
348
+ {
349
+ owner,
350
+ connectionIds: currentState.connectionIds,
351
+ fireCount: currentState.fireCount,
352
+ lastFiredAt: currentState.lastFiredAt,
353
+ },
354
+ );
355
+
356
+ await this.assertDerivedOutputContract(definition);
357
+
358
+ const stored = await this.triggerEngine.register(definition);
359
+ const state = await this.store.setTriggerState(
360
+ stored.triggerId,
361
+ stored.version,
362
+ 'active',
363
+ actor,
364
+ {
365
+ owner,
366
+ connectionIds: allowedConnectionIds,
367
+ },
368
+ );
369
+
370
+ await this.registerDerivedOutputSource(stored);
371
+
372
+ const receiptId = await stableId(
373
+ 'trigger_receipt',
374
+ `${triggerId}:${expectedVersion}->${stored.version}:update:${confirmationId || actor.principal_id}`,
375
+ );
376
+
377
+ await this.audit({
378
+ kind: 'trigger.updated',
379
+ entityType: 'trigger',
380
+ entityId: triggerId,
381
+ traceId: `trigger:${triggerId}`,
382
+ details: {
383
+ actor,
384
+ owner,
385
+ fromVersion: expectedVersion,
386
+ toVersion: stored.version,
387
+ previousRevision: retiredState.revision,
388
+ newRevision: state.revision,
389
+ receiptId,
390
+ confirmationId: confirmationId || null,
391
+ },
392
+ });
393
+
394
+ return {
395
+ receiptId,
396
+ action: 'update',
397
+ previous: {
398
+ definition: current,
399
+ state: retiredState,
400
+ },
401
+ definition: stored,
402
+ state,
403
+ };
404
+ }
405
+
406
+ async deleteTrigger({
407
+ triggerId,
408
+ version,
409
+ actor: actorInput,
410
+ owner: ownerInput,
411
+ confirmationId,
412
+ }) {
413
+ const actor = actorIdentity(actorInput);
414
+ const owner = actorIdentity(ownerInput);
415
+
416
+ if (
417
+ actor.type === 'agent' &&
418
+ (typeof confirmationId !== 'string' || !confirmationId.trim())
419
+ ) {
420
+ const error = new Error(
421
+ 'Agent-authored trigger mutations require explicit confirmationId',
422
+ );
423
+ error.code = 'TRIGGER_CONFIRMATION_REQUIRED';
424
+ throw error;
425
+ }
426
+
427
+ const definition = this.store.listTriggers().find(
428
+ (candidate) =>
429
+ candidate.triggerId === triggerId &&
430
+ candidate.version === version,
431
+ );
432
+ if (!definition) {
433
+ const error = new Error(`Unknown trigger ${triggerId}@${version}`);
434
+ error.code = 'TRIGGER_NOT_FOUND';
435
+ throw error;
436
+ }
437
+
438
+ const currentState = this.store.getTriggerState(triggerId, version);
439
+ if (!samePrincipal(currentState.owner, owner)) {
440
+ const error = new Error('Trigger owner mismatch');
441
+ error.code = 'TRIGGER_OWNER_MISMATCH';
442
+ throw error;
443
+ }
444
+
445
+ await this.retireMatches(triggerId, version, 'trigger_deleted');
446
+ const state = await this.store.setTriggerState(
447
+ triggerId,
448
+ version,
449
+ 'deleted',
450
+ actor,
451
+ {
452
+ owner,
453
+ connectionIds: currentState.connectionIds,
454
+ fireCount: currentState.fireCount,
455
+ lastFiredAt: currentState.lastFiredAt,
456
+ },
457
+ );
458
+
459
+ const receiptId = await stableId(
460
+ 'trigger_receipt',
461
+ `${triggerId}:${version}:delete:${confirmationId || actor.principal_id}:${state.updatedAt}`,
462
+ );
463
+
464
+ await this.audit({
465
+ kind: 'trigger.deleted',
466
+ entityType: 'trigger',
467
+ entityId: triggerId,
468
+ traceId: `trigger:${triggerId}`,
469
+ details: {
470
+ actor,
471
+ owner,
472
+ version,
473
+ confirmationId: confirmationId || null,
474
+ receiptId,
475
+ },
476
+ });
477
+
478
+ return {
479
+ receiptId,
480
+ action: 'delete',
481
+ definition,
482
+ state,
483
+ };
484
+ }
485
+
486
+ async pauseTrigger({
487
+ triggerId,
488
+ version,
489
+ actor: actorInput,
490
+ owner,
491
+ confirmationId,
492
+ }) {
493
+ return this.setTriggerLifecycle({
494
+ triggerId,
495
+ version,
496
+ status: 'paused',
497
+ kind: 'trigger.paused',
498
+ action: 'pause',
499
+ actor: actorInput,
500
+ owner,
501
+ confirmationId,
502
+ });
503
+ }
504
+
505
+ async resumeTrigger({
506
+ triggerId,
507
+ version,
508
+ actor: actorInput,
509
+ owner,
510
+ confirmationId,
511
+ }) {
512
+ return this.setTriggerLifecycle({
513
+ triggerId,
514
+ version,
515
+ status: 'active',
516
+ kind: 'trigger.resumed',
517
+ action: 'resume',
518
+ actor: actorInput,
519
+ owner,
520
+ confirmationId,
521
+ });
522
+ }
523
+
524
+ async setTriggerLifecycle({
525
+ triggerId,
526
+ version,
527
+ status,
528
+ kind,
529
+ action,
530
+ actor: actorInput,
531
+ owner: ownerInput,
532
+ confirmationId,
533
+ }) {
534
+ const actor = actorIdentity(actorInput);
535
+ const owner = actorIdentity(ownerInput);
536
+ if (
537
+ actor.type === 'agent' &&
538
+ (typeof confirmationId !== 'string' || !confirmationId.trim())
539
+ ) {
540
+ const error = new Error(
541
+ 'Agent-authored trigger mutations require explicit confirmationId',
542
+ );
543
+ error.code = 'TRIGGER_CONFIRMATION_REQUIRED';
544
+ throw error;
545
+ }
546
+
547
+ const definition = this.store.listTriggers().find(
548
+ (candidate) =>
549
+ candidate.triggerId === triggerId &&
550
+ candidate.version === version,
551
+ );
552
+ if (!definition) {
553
+ const error = new Error(`Unknown trigger ${triggerId}@${version}`);
554
+ error.code = 'TRIGGER_NOT_FOUND';
555
+ throw error;
556
+ }
557
+
558
+ const currentState = this.store.getTriggerState(triggerId, version);
559
+ if (!samePrincipal(currentState.owner, owner)) {
560
+ const error = new Error('Trigger owner mismatch');
561
+ error.code = 'TRIGGER_OWNER_MISMATCH';
562
+ throw error;
563
+ }
564
+
565
+ if (
566
+ (action === 'pause' && currentState.status !== 'active') ||
567
+ (action === 'resume' && currentState.status !== 'paused')
568
+ ) {
569
+ const error = new Error(
570
+ `Invalid trigger lifecycle transition: ${currentState.status} -> ${status}`,
571
+ );
572
+ error.code = 'TRIGGER_LIFECYCLE_CONFLICT';
573
+ throw error;
574
+ }
575
+
576
+ const state = await this.store.setTriggerState(
577
+ triggerId,
578
+ version,
579
+ status,
580
+ actor,
581
+ {
582
+ owner,
583
+ connectionIds: currentState.connectionIds,
584
+ },
585
+ );
586
+ const receiptId = await stableId(
587
+ 'trigger_receipt',
588
+ `${triggerId}:${version}:${action}:${confirmationId || actor.principal_id}:${state.updatedAt}`,
589
+ );
590
+
591
+ await this.audit({
592
+ kind,
593
+ entityType: 'trigger',
594
+ entityId: triggerId,
595
+ traceId: `trigger:${triggerId}`,
596
+ details: {
597
+ actor,
598
+ owner,
599
+ version,
600
+ confirmationId: confirmationId || null,
601
+ receiptId,
602
+ status,
603
+ },
604
+ });
605
+
606
+ return {
607
+ receiptId,
608
+ action,
609
+ definition,
610
+ state,
611
+ };
612
+ }
613
+
614
+ assertSourcesAvailable(definition, connectionIds) {
615
+ const allowedConnectionIds = Array.isArray(connectionIds)
616
+ ? [...new Set(connectionIds.map(String).filter(Boolean))]
617
+ : [];
618
+ if (!allowedConnectionIds.length) {
619
+ const error = new Error(
620
+ 'At least one active event-source connection is required',
621
+ );
622
+ error.code = 'TRIGGER_CONNECTION_REQUIRED';
623
+ throw error;
624
+ }
625
+
626
+ const sources = this.listEventSources({
627
+ connectionIds: allowedConnectionIds,
628
+ });
629
+
630
+ const resolveSource = (clause) => {
631
+ if (!clause.serverId) {
632
+ const error = new Error(
633
+ `Trigger clause ${clause.id} must declare serverId`,
634
+ );
635
+ error.code = 'TRIGGER_SOURCE_SCOPE_REQUIRED';
636
+ throw error;
637
+ }
638
+
639
+ let candidates = sources.filter(
640
+ (source) =>
641
+ source.eventName === clause.event &&
642
+ source.serverId === clause.serverId,
643
+ );
644
+
645
+ if (!candidates.length) {
646
+ const error = new Error(
647
+ `Event source unavailable: ${clause.serverId}/${clause.event}`,
648
+ );
649
+ error.code = 'TRIGGER_EVENT_SOURCE_UNAVAILABLE';
650
+ throw error;
651
+ }
652
+
653
+ if (clause.serverId === DERIVED_EVENT_SERVER_ID) {
654
+ const versions = [...new Set(
655
+ candidates
656
+ .map((source) => source.metadata?.contractVersion)
657
+ .filter(Boolean)
658
+ .map(String),
659
+ )];
660
+
661
+ if (clause.contractVersion) {
662
+ candidates = candidates.filter(
663
+ (source) =>
664
+ String(source.metadata?.contractVersion || '') ===
665
+ String(clause.contractVersion),
666
+ );
667
+ if (!candidates.length) {
668
+ const error = new Error(
669
+ `Derived contract unavailable: ${clause.event}@${clause.contractVersion}`,
670
+ );
671
+ error.code = 'DERIVED_CONTRACT_NOT_FOUND';
672
+ throw error;
673
+ }
674
+ } else if (versions.length === 1) {
675
+ clause.contractVersion = versions[0];
676
+ candidates = candidates.filter(
677
+ (source) =>
678
+ String(source.metadata?.contractVersion || '') === versions[0],
679
+ );
680
+ } else if (versions.length > 1) {
681
+ const error = new Error(
682
+ `Derived event consumer is ambiguous for ${clause.event}; explicit contractVersion required`,
683
+ );
684
+ error.code = 'DERIVED_CONTRACT_AMBIGUOUS';
685
+ error.details = {
686
+ eventName: clause.event,
687
+ availableVersions: versions,
688
+ };
689
+ throw error;
690
+ }
691
+ }
692
+
693
+ return candidates[0];
694
+ };
695
+
696
+ for (const clause of definition.clauses) {
697
+ const source = resolveSource(clause);
698
+ for (const predicate of clause.where ?? []) {
699
+ assertAdvertisedPath(
700
+ source,
701
+ predicate.path,
702
+ `Predicate ${clause.id}`,
703
+ );
704
+ }
705
+ }
706
+
707
+ for (const field of definition.correlation?.deterministic?.fields ?? []) {
708
+ const clause = definition.clauses.find(
709
+ (candidate) => candidate.id === field.ref,
710
+ );
711
+ const source = clause ? resolveSource(clause) : null;
712
+ assertAdvertisedPath(
713
+ source,
714
+ field.path,
715
+ `Correlation ${field.ref}`,
716
+ );
717
+ }
718
+
719
+ for (const projection of definition.derivedEvent?.projections ?? []) {
720
+ const clause = definition.clauses.find(
721
+ (candidate) => candidate.id === projection.ref,
722
+ );
723
+ const source = clause ? resolveSource(clause) : null;
724
+ const payloadSchema = source?.payloadSchema;
725
+ if (
726
+ payloadSchema &&
727
+ typeof payloadSchema === 'object' &&
728
+ Object.keys(payloadSchema).length
729
+ ) {
730
+ const node = getSchemaNode(payloadSchema, projection.path);
731
+ if (!node) {
732
+ const error = new Error(
733
+ `Derived projection field unavailable: ${projection.ref}.${projection.path}`,
734
+ );
735
+ error.code = 'TRIGGER_DERIVED_PROJECTION_FIELD_UNAVAILABLE';
736
+ throw error;
737
+ }
738
+ const type = Array.isArray(node.type) ? node.type : [node.type];
739
+ if (type.includes('object') || type.includes('array')) {
740
+ const error = new Error(
741
+ `Derived projection must be scalar: ${projection.ref}.${projection.path}`,
742
+ );
743
+ error.code = 'TRIGGER_DERIVED_PROJECTION_NON_SCALAR';
744
+ throw error;
745
+ }
746
+ }
747
+ }
748
+
749
+ return allowedConnectionIds;
750
+ }
751
+
752
+ buildDerivedOutputPayloadSchema(definition) {
753
+ if (!definition.derivedEvent) return null;
754
+
755
+ const properties = {
756
+ _derived: {
757
+ type: 'object',
758
+ properties: {
759
+ triggerId: { type: 'string' },
760
+ triggerVersion: { type: 'string' },
761
+ matchId: { type: 'string' },
762
+ correlationKey: { type: ['string', 'null'] },
763
+ contractVersion: { type: 'string' },
764
+ schemaFingerprint: { type: 'string' },
765
+ },
766
+ required: [
767
+ 'triggerId',
768
+ 'triggerVersion',
769
+ 'matchId',
770
+ 'contractVersion',
771
+ 'schemaFingerprint',
772
+ ],
773
+ },
774
+ };
775
+
776
+ for (const [key, value] of Object.entries(
777
+ definition.derivedEvent.constants ?? {},
778
+ )) {
779
+ properties[key] = scalarSchemaForConstant(value);
780
+ }
781
+
782
+ const allSources = this.store.listEventSources();
783
+ for (const projection of definition.derivedEvent.projections ?? []) {
784
+ const clause = definition.clauses.find(
785
+ (candidate) => candidate.id === projection.ref,
786
+ );
787
+ const source = clause
788
+ ? allSources.find(
789
+ (candidate) =>
790
+ candidate.eventName === clause.event &&
791
+ candidate.serverId === clause.serverId &&
792
+ (
793
+ !clause.contractVersion ||
794
+ String(candidate.metadata?.contractVersion || '') ===
795
+ String(clause.contractVersion)
796
+ ),
797
+ )
798
+ : null;
799
+ properties[projection.key] =
800
+ getSchemaNode(source?.payloadSchema, projection.path) ?? {};
801
+ }
802
+
803
+ return {
804
+ type: 'object',
805
+ properties,
806
+ required: [
807
+ '_derived',
808
+ ...Object.keys(definition.derivedEvent.constants ?? {}),
809
+ ],
810
+ };
811
+ }
812
+
813
+ async assertDerivedOutputContract(definition) {
814
+ if (!definition.derivedEvent) return null;
815
+ const payloadSchema = this.buildDerivedOutputPayloadSchema(definition);
816
+ try {
817
+ return await this.contractRegistry.assertCompatible({
818
+ eventName: definition.derivedEvent.name,
819
+ contractVersion: definition.derivedEvent.contractVersion,
820
+ payloadSchema,
821
+ });
822
+ } catch (error) {
823
+ if (error?.code === 'DERIVED_CONTRACT_SCHEMA_CONFLICT') {
824
+ await this.audit({
825
+ kind: 'derived_contract.conflict',
826
+ entityType: 'derived_contract',
827
+ entityId:
828
+ `${definition.derivedEvent.name}@${definition.derivedEvent.contractVersion}`,
829
+ traceId:
830
+ `derived-contract:${definition.derivedEvent.name}@${definition.derivedEvent.contractVersion}`,
831
+ details: {
832
+ triggerId: definition.triggerId,
833
+ triggerVersion: definition.version,
834
+ ...(error.details ?? {}),
835
+ },
836
+ });
837
+ }
838
+ throw error;
839
+ }
840
+ }
841
+
842
+ async registerDerivedOutputSource(definition) {
843
+ if (!definition.derivedEvent) return null;
844
+
845
+ const contractVersion = String(definition.derivedEvent.contractVersion);
846
+ const payloadSchema = this.buildDerivedOutputPayloadSchema(definition);
847
+
848
+ const contract = await this.contractRegistry.registerProducer({
849
+ eventName: definition.derivedEvent.name,
850
+ contractVersion,
851
+ payloadSchema,
852
+ producer: {
853
+ triggerId: definition.triggerId,
854
+ triggerVersion: definition.version,
855
+ },
856
+ });
857
+
858
+ const sourceId =
859
+ `derived:${definition.triggerId}:${definition.version}:${definition.derivedEvent.name}@${contractVersion}`;
860
+ return this.registerEventSource(
861
+ {
862
+ sourceId,
863
+ connectionId: DERIVED_EVENT_CONNECTION_ID,
864
+ serverId: DERIVED_EVENT_SERVER_ID,
865
+ eventName: definition.derivedEvent.name,
866
+ description:
867
+ `Derived by ${definition.triggerId}@${definition.version} as ${definition.derivedEvent.name}@${contractVersion}`,
868
+ delivery: ['internal'],
869
+ enabled: true,
870
+ experimental: false,
871
+ payloadSchema: contract.record.payloadSchema,
872
+ metadata: {
873
+ derived: true,
874
+ triggerId: definition.triggerId,
875
+ triggerVersion: definition.version,
876
+ contractVersion,
877
+ schemaFingerprint: contract.record.schemaFingerprint,
878
+ },
879
+ },
880
+ {
881
+ type: 'system',
882
+ principal_id: 'event-intelligence:derived-events',
883
+ },
884
+ );
885
+ }
886
+
887
+ async retireMatches(triggerId, version, reason) {
888
+ const now = this.now().toISOString();
889
+ const matches = this.store.listTriggerMatches(triggerId)
890
+ .filter((record) =>
891
+ record.triggerVersion === version &&
892
+ (record.status === 'partial' || record.status === 'matched')
893
+ );
894
+
895
+ for (const record of matches) {
896
+ await this.store.appendTriggerMatch({
897
+ ...record,
898
+ status: 'expired',
899
+ updatedAt: now,
900
+ });
901
+ for (const deadline of this.store.listTemporalDeadlines?.({
902
+ matchId: record.matchId,
903
+ status: 'pending',
904
+ }) ?? []) {
905
+ await this.store.setTemporalDeadlineStatus?.(
906
+ deadline.deadlineId,
907
+ 'cancelled',
908
+ );
909
+ }
910
+ }
911
+
912
+ return {
913
+ reason,
914
+ retiredMatchIds: matches.map((record) => record.matchId),
915
+ };
916
+ }
917
+
918
+ async audit({
919
+ kind,
920
+ entityType,
921
+ entityId,
922
+ traceId,
923
+ details,
924
+ }) {
925
+ const timestamp = this.now().toISOString();
926
+ await this.store.appendAudit({
927
+ auditId: await stableId(
928
+ 'audit',
929
+ `${kind}:${entityType}:${entityId}:${timestamp}:${this.store.auditLength()}`,
930
+ ),
931
+ traceId,
932
+ timestamp,
933
+ kind,
934
+ entityType,
935
+ entityId,
936
+ details,
937
+ });
938
+ }
939
+ }