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.
- package/CHANGELOG.md +80 -0
- package/LICENSE +201 -0
- package/README.md +298 -0
- package/SECURITY.md +42 -0
- package/bin/mcp-event-intelligence.mjs +52 -0
- package/dist/src/composite/engine.d.ts +87 -0
- package/dist/src/composite/engine.js +667 -0
- package/dist/src/composite/temporal.d.ts +32 -0
- package/dist/src/composite/temporal.js +317 -0
- package/dist/src/intelligenceProtocol/audit.d.ts +19 -0
- package/dist/src/intelligenceProtocol/audit.js +56 -0
- package/dist/src/intelligenceProtocol/canonical.d.ts +2 -0
- package/dist/src/intelligenceProtocol/canonical.js +21 -0
- package/dist/src/intelligenceProtocol/index.d.ts +12 -0
- package/dist/src/intelligenceProtocol/index.js +8 -0
- package/dist/src/intelligenceProtocol/invariants.d.ts +3 -0
- package/dist/src/intelligenceProtocol/invariants.js +36 -0
- package/dist/src/intelligenceProtocol/lifecycle.d.ts +4 -0
- package/dist/src/intelligenceProtocol/lifecycle.js +24 -0
- package/dist/src/intelligenceProtocol/mapper.d.ts +14 -0
- package/dist/src/intelligenceProtocol/mapper.js +28 -0
- package/dist/src/intelligenceProtocol/schemas.d.ts +325 -0
- package/dist/src/intelligenceProtocol/schemas.js +182 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +488 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.js +379 -0
- package/dist/src/intelligenceProtocol/version.d.ts +7 -0
- package/dist/src/intelligenceProtocol/version.js +7 -0
- package/dist/src/mcpEvents/consumer.d.ts +7 -0
- package/dist/src/mcpEvents/consumer.js +14 -0
- package/dist/src/mcpEvents/server.d.ts +41 -0
- package/dist/src/mcpEvents/server.js +122 -0
- package/dist/src/protocol/types.d.ts +37 -0
- package/dist/src/protocol/types.js +1 -0
- package/dist/src/semantic/conditionEngine.d.ts +7 -0
- package/dist/src/semantic/conditionEngine.js +65 -0
- package/dist/src/semantic/types.d.ts +31 -0
- package/dist/src/semantic/types.js +1 -0
- package/dist/src/semantic/typesafeJevEvaluator.d.ts +23 -0
- package/dist/src/semantic/typesafeJevEvaluator.js +95 -0
- package/package.json +85 -0
- package/scripts/host-integration.d.mts +86 -0
- package/scripts/host-integration.mjs +216 -0
- package/scripts/lib/composite-event-consumer.mjs +172 -0
- package/scripts/lib/composite-wake-coordinator.mjs +185 -0
- package/scripts/lib/derived-contract-registry.mjs +210 -0
- package/scripts/lib/derived-event-coordinator.mjs +242 -0
- package/scripts/lib/event-processor.mjs +409 -0
- package/scripts/lib/generic-runtime-wake.mjs +76 -0
- package/scripts/lib/github-mcp-events-adapter.mjs +66 -0
- package/scripts/lib/github-webhook.mjs +88 -0
- package/scripts/lib/http-wake-deliverer.mjs +31 -0
- package/scripts/lib/local-event-intelligence-runtime.mjs +193 -0
- package/scripts/lib/mcp-events-client.mjs +519 -0
- package/scripts/lib/persistent-event-store.mjs +769 -0
- package/scripts/lib/temporal-deadline-scheduler.mjs +111 -0
- package/scripts/lib/trigger-control-plane.mjs +939 -0
- package/scripts/lib/trigger-inspector.mjs +631 -0
- package/scripts/lib/wake-signature.mjs +10 -0
- package/scripts/mcp-stdio-server.mjs +380 -0
- package/scripts/mcp-stdio.mjs +3 -0
- package/scripts/run-tests.mjs +51 -0
- package/scripts/service.mjs +756 -0
- 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
|
+
}
|