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,631 @@
1
+ import {
2
+ CompositeTriggerEngine,
3
+ } from '../../dist/src/composite/engine.js';
4
+ import {
5
+ evaluateTemporalConditions,
6
+ } from '../../dist/src/composite/temporal.js';
7
+ import {
8
+ parseCompositeTriggerDefinition,
9
+ parseCorrelatableEvent,
10
+ parseTriggerMatchRecord,
11
+ } from '../../dist/src/intelligenceProtocol/index.js';
12
+
13
+ function latest(records) {
14
+ return [...records].sort(
15
+ (a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt),
16
+ )[0] ?? null;
17
+ }
18
+
19
+ function sourceRef(event) {
20
+ return {
21
+ clauseId: event.clauseId,
22
+ serverId: event.serverId ?? null,
23
+ eventName: event.eventName,
24
+ sourceEventId: event.sourceEventId,
25
+ occurredAt: event.occurredAt,
26
+ traceId: event.traceId,
27
+ payloadHash: event.payloadHash ?? null,
28
+ contractVersion:
29
+ event.data?._derived?.contractVersion ?? null,
30
+ schemaFingerprint:
31
+ event.data?._derived?.schemaFingerprint ?? null,
32
+ };
33
+ }
34
+
35
+ function baseExpressionRefs(expression) {
36
+ return expression.kind === 'count'
37
+ ? [expression.ref]
38
+ : expression.refs;
39
+ }
40
+
41
+ function clauseState(definition, match) {
42
+ const seen = new Map();
43
+ for (const event of match?.sourceEvents ?? []) {
44
+ const current = seen.get(event.clauseId) ?? [];
45
+ current.push(sourceRef(event));
46
+ seen.set(event.clauseId, current);
47
+ }
48
+
49
+ return definition.clauses.map((clause) => {
50
+ const evidence = seen.get(clause.id) ?? [];
51
+ const requiredCount =
52
+ definition.expression.kind === 'count' &&
53
+ definition.expression.ref === clause.id
54
+ ? definition.expression.atLeast
55
+ : 1;
56
+ return {
57
+ clauseId: clause.id,
58
+ event: clause.event,
59
+ serverId: clause.serverId ?? null,
60
+ contractVersion: clause.contractVersion ?? null,
61
+ status: evidence.length >= requiredCount ? 'satisfied' : 'waiting',
62
+ observedCount: evidence.length,
63
+ requiredCount,
64
+ evidence,
65
+ };
66
+ });
67
+ }
68
+
69
+ function baseExpressionSatisfied(definition, clauses) {
70
+ const byId = new Map(clauses.map((entry) => [entry.clauseId, entry]));
71
+ const expression = definition.expression;
72
+ if (expression.kind === 'allOf') {
73
+ return expression.refs.every(
74
+ (ref) => byId.get(ref)?.status === 'satisfied',
75
+ );
76
+ }
77
+ if (expression.kind === 'anyOf') {
78
+ return expression.refs.some(
79
+ (ref) => byId.get(ref)?.status === 'satisfied',
80
+ );
81
+ }
82
+ if (expression.kind === 'count') {
83
+ return (byId.get(expression.ref)?.observedCount ?? 0) >= expression.atLeast;
84
+ }
85
+
86
+ const ordered = [];
87
+ for (const ref of expression.refs) {
88
+ const evidence = byId.get(ref)?.evidence ?? [];
89
+ if (!evidence.length) return false;
90
+ ordered.push(evidence[0]);
91
+ }
92
+ return ordered.every(
93
+ (event, index) =>
94
+ index === 0 ||
95
+ Date.parse(event.occurredAt) >
96
+ Date.parse(ordered[index - 1].occurredAt),
97
+ );
98
+ }
99
+
100
+ function explain({
101
+ definition,
102
+ state,
103
+ match,
104
+ clauses,
105
+ temporal,
106
+ wake,
107
+ }) {
108
+ if (state.status === 'paused') {
109
+ return {
110
+ code: 'trigger_paused',
111
+ summary: 'Trigger is paused and will not consume new events.',
112
+ };
113
+ }
114
+ if (state.status === 'completed') {
115
+ return {
116
+ code: 'trigger_completed',
117
+ summary: 'Trigger lifecycle is complete.',
118
+ };
119
+ }
120
+ if (state.status === 'expired') {
121
+ return {
122
+ code: 'trigger_lifecycle_expired',
123
+ summary: 'Trigger lifecycle has expired.',
124
+ };
125
+ }
126
+ if (state.status === 'deleted') {
127
+ return {
128
+ code: 'trigger_deleted',
129
+ summary: 'Trigger has been deleted.',
130
+ };
131
+ }
132
+
133
+ if (!match) {
134
+ return {
135
+ code: 'waiting_for_first_event',
136
+ summary: `Waiting for: ${baseExpressionRefs(definition.expression).join(', ')}.`,
137
+ };
138
+ }
139
+
140
+ if (match.status === 'fired') {
141
+ return {
142
+ code: 'fired',
143
+ summary: wake?.runtimeReceiptId
144
+ ? `Fired and acknowledged by runtime receipt ${wake.runtimeReceiptId}.`
145
+ : 'Fired; runtime acknowledgement is not present in the current wake record.',
146
+ };
147
+ }
148
+ if (match.status === 'emitted') {
149
+ return {
150
+ code: 'derived_event_emitted',
151
+ summary: match.derivedEventIds.length
152
+ ? `Derived event emitted: ${match.derivedEventIds.join(', ')}.`
153
+ : 'Derived event emitted.',
154
+ };
155
+ }
156
+ if (match.status === 'matched') {
157
+ return {
158
+ code: 'matched_waiting_delivery',
159
+ summary: 'All trigger conditions are satisfied; runtime delivery is pending or not yet marked fired.',
160
+ };
161
+ }
162
+ if (match.status === 'expired') {
163
+ return {
164
+ code: 'match_expired',
165
+ summary: 'This match can no longer satisfy the trigger.',
166
+ };
167
+ }
168
+
169
+ const missing = clauses
170
+ .filter((clause) => clause.status !== 'satisfied')
171
+ .map((clause) => clause.clauseId);
172
+
173
+ if (!baseExpressionSatisfied(definition, clauses)) {
174
+ return {
175
+ code: 'waiting_for_event_clauses',
176
+ summary: `Waiting for event clauses: ${missing.join(', ') || 'expression ordering'}.`,
177
+ missingClauses: missing,
178
+ };
179
+ }
180
+
181
+ const blocked = temporal.conditionStates.filter(
182
+ (condition) => condition.status === 'blocked',
183
+ );
184
+ if (blocked.length) {
185
+ return {
186
+ code: 'temporal_blocked',
187
+ summary: `Temporal condition blocked: ${blocked.map((entry) => entry.conditionId).join(', ')}.`,
188
+ temporalConditionIds: blocked.map((entry) => entry.conditionId),
189
+ };
190
+ }
191
+
192
+ const pending = temporal.conditionStates.filter(
193
+ (condition) => condition.status === 'pending',
194
+ );
195
+ if (pending.length) {
196
+ return {
197
+ code: 'waiting_for_temporal_conditions',
198
+ summary: `Waiting for temporal conditions: ${pending.map((entry) => entry.conditionId).join(', ')}.`,
199
+ temporalConditionIds: pending.map((entry) => entry.conditionId),
200
+ };
201
+ }
202
+
203
+ return {
204
+ code: 'waiting_for_correlation',
205
+ summary: 'Event and temporal conditions are present; correlation or semantic evaluation has not completed.',
206
+ };
207
+ }
208
+
209
+ export class TriggerInspector {
210
+ constructor({
211
+ store,
212
+ now = () => new Date(),
213
+ }) {
214
+ this.store = store;
215
+ this.now = now;
216
+ }
217
+
218
+ inspect({
219
+ triggerId,
220
+ version,
221
+ matchId,
222
+ }) {
223
+ const candidates = this.store.listTriggers()
224
+ .filter((definition) => definition.triggerId === triggerId)
225
+ .filter((definition) => !version || definition.version === version);
226
+
227
+ if (!candidates.length) {
228
+ const error = new Error(`Unknown trigger: ${triggerId}`);
229
+ error.code = 'TRIGGER_NOT_FOUND';
230
+ throw error;
231
+ }
232
+
233
+ const definition = version
234
+ ? candidates[0]
235
+ : [...candidates].sort((a, b) =>
236
+ String(b.version).localeCompare(String(a.version), undefined, {
237
+ numeric: true,
238
+ })
239
+ )[0];
240
+
241
+ const state = this.store.getTriggerState(
242
+ definition.triggerId,
243
+ definition.version,
244
+ );
245
+
246
+ const matches = this.store.listTriggerMatches(definition.triggerId)
247
+ .filter((match) => match.triggerVersion === definition.version);
248
+ const match = matchId
249
+ ? matches.find((candidate) => candidate.matchId === matchId) ?? null
250
+ : latest(matches);
251
+
252
+ if (matchId && !match) {
253
+ const error = new Error(`Unknown trigger match: ${matchId}`);
254
+ error.code = 'TRIGGER_MATCH_NOT_FOUND';
255
+ throw error;
256
+ }
257
+
258
+ const clauses = clauseState(definition, match);
259
+ const temporal = match
260
+ ? evaluateTemporalConditions(definition, match, this.now())
261
+ : {
262
+ outcome: definition.temporal?.length ? 'pending' : 'satisfied',
263
+ pendingDeadlines: [],
264
+ conditionStates: (definition.temporal ?? []).map((condition) => ({
265
+ conditionId: condition.id,
266
+ kind: condition.kind,
267
+ status: 'pending',
268
+ reason: 'waiting for a match',
269
+ })),
270
+ };
271
+
272
+ const deadlines = match
273
+ ? this.store.listTemporalDeadlines?.({
274
+ matchId: match.matchId,
275
+ }) ?? []
276
+ : [];
277
+ const pendingDeadlines = deadlines.filter(
278
+ (deadline) => deadline.status === 'pending',
279
+ );
280
+ const nextEvaluationAt = pendingDeadlines
281
+ .map((deadline) => deadline.dueAt)
282
+ .sort()[0] ?? null;
283
+
284
+ const wake = match?.firedWakeId
285
+ ? this.store.latestWake(match.firedWakeId)
286
+ : null;
287
+
288
+ const derivedOutputs = match
289
+ ? (this.store.listDerivedEvents?.({ matchId: match.matchId }) ?? [])
290
+ : [];
291
+
292
+ const matchHistory = match
293
+ ? (this.store.listTriggerMatchHistory?.(match.matchId) ?? [match])
294
+ .map((record) => ({
295
+ status: record.status,
296
+ updatedAt: record.updatedAt,
297
+ firedWakeId: record.firedWakeId,
298
+ derivedEventIds: record.derivedEventIds ?? [],
299
+ sourceEventIds: record.sourceEvents.map(
300
+ (event) => event.sourceEventId,
301
+ ),
302
+ }))
303
+ : [];
304
+
305
+ return {
306
+ trigger: {
307
+ triggerId: definition.triggerId,
308
+ version: definition.version,
309
+ description: definition.description ?? null,
310
+ target: definition.target ?? null,
311
+ derivedEvent: definition.derivedEvent ?? null,
312
+ expression: definition.expression,
313
+ temporal: definition.temporal ?? [],
314
+ lifecycle: definition.lifecycle ?? {},
315
+ },
316
+ lifecycle: state,
317
+ match: match
318
+ ? {
319
+ matchId: match.matchId,
320
+ status: match.status,
321
+ correlationKey: match.correlationKey,
322
+ openedAt: match.openedAt,
323
+ expiresAt: match.expiresAt,
324
+ updatedAt: match.updatedAt,
325
+ }
326
+ : null,
327
+ clauses,
328
+ temporal: {
329
+ outcome: temporal.outcome,
330
+ conditions: temporal.conditionStates,
331
+ },
332
+ deadlines: deadlines.map((deadline) => ({
333
+ deadlineId: deadline.deadlineId,
334
+ conditionId: deadline.conditionId,
335
+ dueAt: deadline.dueAt,
336
+ status: deadline.status,
337
+ })),
338
+ nextEvaluationAt,
339
+ wake: wake
340
+ ? {
341
+ wakeId: wake.wakeId,
342
+ status: wake.status,
343
+ runtimeReceiptId: wake.runtimeReceiptId ?? null,
344
+ target: wake.target,
345
+ createdAt: wake.createdAt,
346
+ }
347
+ : null,
348
+ lineage: {
349
+ evidence: match?.sourceEvents.map(sourceRef) ?? [],
350
+ derivedOutputs: derivedOutputs.map((record) => ({
351
+ eventId: record.event.sourceEventId,
352
+ eventName: record.event.name,
353
+ contractVersion:
354
+ record.event.data?._derived?.contractVersion ?? null,
355
+ schemaFingerprint:
356
+ record.event.data?._derived?.schemaFingerprint ?? null,
357
+ occurredAt: record.event.occurredAt,
358
+ directParents: record.directParents,
359
+ rootEvidence: record.rootEvidence,
360
+ })),
361
+ matchHistory,
362
+ },
363
+ why: explain({
364
+ definition,
365
+ state,
366
+ match,
367
+ clauses,
368
+ temporal,
369
+ wake,
370
+ }),
371
+ };
372
+ }
373
+ }
374
+
375
+ class SimulationStore {
376
+ constructor() {
377
+ this.triggers = [];
378
+ this.matches = new Map();
379
+ this.matchHistory = [];
380
+ this.deadlines = new Map();
381
+ this.audit = [];
382
+ this.wakes = [];
383
+ this.states = new Map();
384
+ }
385
+
386
+ async putTrigger(definitionInput) {
387
+ const definition = parseCompositeTriggerDefinition(definitionInput);
388
+ this.triggers.push(definition);
389
+ return definition;
390
+ }
391
+
392
+ listTriggers() {
393
+ return this.triggers;
394
+ }
395
+
396
+ getTriggerState(triggerId, version) {
397
+ return this.states.get(`${triggerId}@${version}`) ?? {
398
+ triggerId,
399
+ version,
400
+ status: 'active',
401
+ updatedAt: null,
402
+ actor: null,
403
+ owner: null,
404
+ connectionIds: [],
405
+ fireCount: 0,
406
+ lastFiredAt: null,
407
+ revision: 0,
408
+ };
409
+ }
410
+
411
+ async setTriggerState(triggerId, version, status, actor = null, metadata = {}) {
412
+ const key = `${triggerId}@${version}`;
413
+ const previous = this.getTriggerState(triggerId, version);
414
+ const next = {
415
+ ...previous,
416
+ status,
417
+ actor,
418
+ updatedAt: new Date().toISOString(),
419
+ owner: metadata.owner ?? previous.owner,
420
+ connectionIds: metadata.connectionIds ?? previous.connectionIds,
421
+ fireCount: metadata.fireCount ?? previous.fireCount,
422
+ lastFiredAt:
423
+ metadata.lastFiredAt !== undefined
424
+ ? metadata.lastFiredAt
425
+ : previous.lastFiredAt,
426
+ revision: previous.revision + 1,
427
+ };
428
+ this.states.set(key, next);
429
+ return next;
430
+ }
431
+
432
+ async appendTriggerMatch(recordInput) {
433
+ const record = parseTriggerMatchRecord(recordInput);
434
+ this.matches.set(record.matchId, record);
435
+ this.matchHistory.push(record);
436
+ return record;
437
+ }
438
+
439
+ listTriggerMatches(triggerId) {
440
+ return [...this.matches.values()]
441
+ .filter((record) => !triggerId || record.triggerId === triggerId);
442
+ }
443
+
444
+ listTriggerMatchHistory(matchId) {
445
+ return this.matchHistory
446
+ .filter((record) => !matchId || record.matchId === matchId);
447
+ }
448
+
449
+ async appendAudit(input) {
450
+ const record = {
451
+ ...input,
452
+ sequence: this.audit.length,
453
+ };
454
+ this.audit.push(record);
455
+ return record;
456
+ }
457
+
458
+ auditLength() {
459
+ return this.audit.length;
460
+ }
461
+
462
+ async putTemporalDeadline(input) {
463
+ const previous = this.deadlines.get(input.deadlineId);
464
+ const record = {
465
+ ...input,
466
+ createdAt: previous?.createdAt ?? new Date().toISOString(),
467
+ updatedAt: new Date().toISOString(),
468
+ };
469
+ this.deadlines.set(record.deadlineId, record);
470
+ return record;
471
+ }
472
+
473
+ getTemporalDeadline(deadlineId) {
474
+ return this.deadlines.get(deadlineId) ?? null;
475
+ }
476
+
477
+ listTemporalDeadlines({ triggerId, matchId, status } = {}) {
478
+ return [...this.deadlines.values()]
479
+ .filter((deadline) => !triggerId || deadline.triggerId === triggerId)
480
+ .filter((deadline) => !matchId || deadline.matchId === matchId)
481
+ .filter((deadline) => !status || deadline.status === status);
482
+ }
483
+
484
+ listDueTemporalDeadlines(nowIso) {
485
+ return this.listTemporalDeadlines({ status: 'pending' })
486
+ .filter((deadline) =>
487
+ Date.parse(deadline.dueAt) <= Date.parse(nowIso)
488
+ )
489
+ .sort((a, b) => Date.parse(a.dueAt) - Date.parse(b.dueAt));
490
+ }
491
+
492
+ async setTemporalDeadlineStatus(deadlineId, status) {
493
+ const previous = this.deadlines.get(deadlineId);
494
+ if (!previous) return null;
495
+ const next = {
496
+ ...previous,
497
+ status,
498
+ updatedAt: new Date().toISOString(),
499
+ };
500
+ this.deadlines.set(deadlineId, next);
501
+ return next;
502
+ }
503
+
504
+ latestWake(wakeId) {
505
+ return [...this.wakes].reverse()
506
+ .find((wake) => wake.wakeId === wakeId) ?? null;
507
+ }
508
+
509
+ async appendWake(wake) {
510
+ this.wakes.push(wake);
511
+ return wake;
512
+ }
513
+ }
514
+
515
+ export async function simulateTrigger({
516
+ definition: definitionInput,
517
+ events,
518
+ until,
519
+ order = 'provided',
520
+ }) {
521
+ const definition = parseCompositeTriggerDefinition(definitionInput);
522
+ const normalized = (events ?? []).map(parseCorrelatableEvent);
523
+ if (order === 'event_time') {
524
+ normalized.sort(
525
+ (a, b) => Date.parse(a.occurredAt) - Date.parse(b.occurredAt),
526
+ );
527
+ } else if (order !== 'provided') {
528
+ throw new Error('Simulation order must be provided or event_time');
529
+ }
530
+
531
+ let clock = normalized.length
532
+ ? new Date(normalized[0].occurredAt)
533
+ : new Date();
534
+ const store = new SimulationStore();
535
+ const engine = new CompositeTriggerEngine(
536
+ store,
537
+ null,
538
+ () => clock,
539
+ );
540
+ await engine.register(definition);
541
+
542
+ const steps = [];
543
+ for (const event of normalized) {
544
+ clock = new Date(
545
+ Math.max(clock.getTime(), Date.parse(event.occurredAt)),
546
+ );
547
+ const results = await engine.ingest(event);
548
+ steps.push({
549
+ type: 'event',
550
+ sourceEventId: event.sourceEventId,
551
+ eventName: event.name,
552
+ processingTime: clock.toISOString(),
553
+ results: results.map((result) => ({
554
+ triggerId: result.triggerId,
555
+ matchId: result.match?.matchId ?? null,
556
+ status: result.match?.status ?? null,
557
+ matched: result.matched,
558
+ fired: result.fired,
559
+ })),
560
+ });
561
+ }
562
+
563
+ const untilTime = until ? Date.parse(until) : null;
564
+ if (until && !Number.isFinite(untilTime)) {
565
+ throw new Error('Simulation until must be a valid ISO timestamp');
566
+ }
567
+
568
+ if (untilTime !== null) {
569
+ while (true) {
570
+ const due = store
571
+ .listTemporalDeadlines({ status: 'pending' })
572
+ .filter((deadline) => Date.parse(deadline.dueAt) <= untilTime)
573
+ .sort((a, b) => Date.parse(a.dueAt) - Date.parse(b.dueAt))[0];
574
+ if (!due) break;
575
+
576
+ clock = new Date(
577
+ Math.max(clock.getTime(), Date.parse(due.dueAt)),
578
+ );
579
+ const timerEvent = {
580
+ traceId: `simulation:${due.deadlineId}`,
581
+ sourceEventId: `simulation:${due.deadlineId}`,
582
+ name: 'event-intelligence.timer.reached',
583
+ serverId: 'event-intelligence:timer',
584
+ provider: 'event-intelligence',
585
+ occurredAt: clock.toISOString(),
586
+ data: {
587
+ deadlineId: due.deadlineId,
588
+ triggerId: due.triggerId,
589
+ triggerVersion: due.triggerVersion,
590
+ matchId: due.matchId,
591
+ conditionId: due.conditionId,
592
+ dueAt: due.dueAt,
593
+ },
594
+ };
595
+
596
+ const results = await engine.ingest(timerEvent);
597
+ await store.setTemporalDeadlineStatus(due.deadlineId, 'fired');
598
+ steps.push({
599
+ type: 'timer',
600
+ deadlineId: due.deadlineId,
601
+ conditionId: due.conditionId,
602
+ processingTime: clock.toISOString(),
603
+ results: results.map((result) => ({
604
+ triggerId: result.triggerId,
605
+ matchId: result.match?.matchId ?? null,
606
+ status: result.match?.status ?? null,
607
+ matched: result.matched,
608
+ fired: result.fired,
609
+ })),
610
+ });
611
+ }
612
+ clock = new Date(Math.max(clock.getTime(), untilTime));
613
+ }
614
+
615
+ const inspector = new TriggerInspector({
616
+ store,
617
+ now: () => clock,
618
+ });
619
+
620
+ return {
621
+ isolated: true,
622
+ order,
623
+ evaluatedUntil: clock.toISOString(),
624
+ steps,
625
+ inspection: inspector.inspect({
626
+ triggerId: definition.triggerId,
627
+ version: definition.version,
628
+ }),
629
+ auditRecords: store.audit.length,
630
+ };
631
+ }
@@ -0,0 +1,10 @@
1
+ export function canonicalWakeJson(value) {
2
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
3
+ if (Array.isArray(value)) return `[${value.map(canonicalWakeJson).join(',')}]`;
4
+ const entries = Object.entries(value)
5
+ .filter(([, entry]) => entry !== undefined)
6
+ .sort(([a], [b]) => a.localeCompare(b));
7
+ return `{${entries
8
+ .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalWakeJson(entry)}`)
9
+ .join(',')}}`;
10
+ }