mcp-event-intelligence 0.3.1 → 0.5.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 +81 -0
- package/README.md +123 -23
- package/SECURITY.md +4 -4
- package/bin/mcp-event-intelligence.mjs +3 -3
- package/dist/src/composite/engine.d.ts +6 -0
- package/dist/src/composite/engine.js +599 -10
- package/dist/src/composite/matching.d.ts +1 -1
- package/dist/src/composite/matching.js +66 -1
- package/dist/src/intelligenceProtocol/activationSchemas.d.ts +2 -0
- package/dist/src/intelligenceProtocol/activationSchemas.js +1 -0
- package/dist/src/intelligenceProtocol/index.d.ts +9 -2
- package/dist/src/intelligenceProtocol/index.js +5 -1
- package/dist/src/intelligenceProtocol/operatorRegistry.d.ts +567 -0
- package/dist/src/intelligenceProtocol/operatorRegistry.js +477 -0
- package/dist/src/intelligenceProtocol/patternV2Schemas.d.ts +188 -0
- package/dist/src/intelligenceProtocol/patternV2Schemas.js +801 -0
- package/dist/src/intelligenceProtocol/schemas.d.ts +6 -0
- package/dist/src/intelligenceProtocol/schemas.js +3 -0
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +179 -4
- package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +38 -4
- package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +262 -0
- package/dist/src/intelligenceProtocol/triggerSchemas.js +164 -2
- package/dist/src/mcpEvents/consumer.d.ts +2 -0
- package/dist/src/mcpEvents/consumer.js +2 -0
- package/dist/src/mcpEvents/erpnextProvider.d.ts +0 -2
- package/dist/src/mcpEvents/erpnextProvider.js +0 -3
- package/dist/src/mcpEvents/provider.d.ts +24 -10
- package/dist/src/mcpEvents/provider.js +124 -55
- package/dist/src/mcpEvents/server.d.ts +3 -1
- package/dist/src/mcpEvents/server.js +20 -19
- package/dist/src/patternV2/evaluator.d.ts +43 -0
- package/dist/src/patternV2/evaluator.js +927 -0
- package/dist/src/patternV2/legacyCompiler.d.ts +3 -0
- package/dist/src/patternV2/legacyCompiler.js +184 -0
- package/dist/src/protocol/types.d.ts +1 -0
- package/package.json +5 -2
- package/scripts/host-integration.d.mts +47 -0
- package/scripts/host-integration.mjs +9 -0
- package/scripts/lib/activation-hydrator.mjs +1 -0
- package/scripts/lib/composite-event-consumer.mjs +4 -0
- package/scripts/lib/derived-event-coordinator.mjs +75 -1
- package/scripts/lib/json-schema.mjs +134 -0
- package/scripts/lib/local-event-intelligence-runtime.mjs +1 -0
- package/scripts/lib/mcp-events-client.mjs +798 -253
- package/scripts/lib/mcp-events-compatibility.mjs +54 -0
- package/scripts/lib/persistent-event-store.mjs +68 -9
- package/scripts/lib/trigger-control-plane.mjs +180 -0
- package/scripts/lib/trigger-inspector.mjs +15 -0
- package/scripts/lib/trigger-planner.mjs +213 -12
- package/scripts/mcp-stdio-server.mjs +38 -52
- package/server.json +4 -4
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CompositeTriggerDefinition } from '../intelligenceProtocol/triggerSchemas.js';
|
|
2
|
+
import { type PatternAstV2Definition } from '../intelligenceProtocol/patternV2Schemas.js';
|
|
3
|
+
export declare function compileLegacyTriggerToPatternV2(definition: CompositeTriggerDefinition): PatternAstV2Definition;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { PatternAstV2DefinitionSchema, } from '../intelligenceProtocol/patternV2Schemas.js';
|
|
2
|
+
function eventNode(ref) {
|
|
3
|
+
return { kind: 'event', ref };
|
|
4
|
+
}
|
|
5
|
+
function expressionNode(definition) {
|
|
6
|
+
const expression = definition.expression;
|
|
7
|
+
if (expression.kind === 'allOf') {
|
|
8
|
+
return {
|
|
9
|
+
kind: 'allOf',
|
|
10
|
+
children: expression.refs.map(eventNode),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (expression.kind === 'anyOf') {
|
|
14
|
+
return {
|
|
15
|
+
kind: 'anyOf',
|
|
16
|
+
children: expression.refs.map(eventNode),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (expression.kind === 'sequence') {
|
|
20
|
+
return {
|
|
21
|
+
kind: 'sequence',
|
|
22
|
+
children: expression.refs.map(eventNode),
|
|
23
|
+
contiguity: 'followedBy',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
kind: 'repeat',
|
|
28
|
+
child: eventNode(expression.ref),
|
|
29
|
+
min: expression.atLeast,
|
|
30
|
+
mode: 'greedy',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function wrapTemporal(root, condition) {
|
|
34
|
+
if (condition.kind === 'calendar') {
|
|
35
|
+
return {
|
|
36
|
+
kind: 'calendar',
|
|
37
|
+
child: root,
|
|
38
|
+
ref: condition.ref,
|
|
39
|
+
timezone: condition.timezone,
|
|
40
|
+
...(condition.before ? { before: condition.before } : {}),
|
|
41
|
+
...(condition.after ? { after: condition.after } : {}),
|
|
42
|
+
...(condition.weekdays ? { weekdays: condition.weekdays } : {}),
|
|
43
|
+
...(condition.dates ? { dates: condition.dates } : {}),
|
|
44
|
+
...(condition.dateRange ? { dateRange: condition.dateRange } : {}),
|
|
45
|
+
...(condition.dayOfMonth ? { dayOfMonth: condition.dayOfMonth } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (condition.kind === 'absence') {
|
|
49
|
+
return {
|
|
50
|
+
kind: 'absence',
|
|
51
|
+
id: condition.id,
|
|
52
|
+
child: root,
|
|
53
|
+
ref: condition.ref,
|
|
54
|
+
afterRef: condition.afterRef,
|
|
55
|
+
...(condition.forMs ? { forMs: condition.forMs } : {}),
|
|
56
|
+
...(condition.untilLocalTime
|
|
57
|
+
? { untilLocalTime: condition.untilLocalTime }
|
|
58
|
+
: {}),
|
|
59
|
+
...(condition.timezone ? { timezone: condition.timezone } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (condition.kind === 'not' || condition.kind === 'unless') {
|
|
63
|
+
return {
|
|
64
|
+
kind: 'notPresent',
|
|
65
|
+
child: root,
|
|
66
|
+
ref: condition.ref,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (condition.kind === 'after') {
|
|
70
|
+
return {
|
|
71
|
+
kind: 'after',
|
|
72
|
+
child: root,
|
|
73
|
+
ref: condition.ref,
|
|
74
|
+
afterRef: condition.afterRef,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (condition.kind === 'until') {
|
|
78
|
+
return {
|
|
79
|
+
kind: 'until',
|
|
80
|
+
child: root,
|
|
81
|
+
ref: condition.ref,
|
|
82
|
+
beforeRef: condition.beforeRef,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (condition.kind === 'debounce') {
|
|
86
|
+
return {
|
|
87
|
+
kind: 'debounce',
|
|
88
|
+
id: condition.id,
|
|
89
|
+
child: root,
|
|
90
|
+
ref: condition.ref,
|
|
91
|
+
forMs: condition.forMs,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (condition.kind === 'threshold') {
|
|
95
|
+
return {
|
|
96
|
+
kind: 'threshold',
|
|
97
|
+
child: root,
|
|
98
|
+
ref: condition.ref,
|
|
99
|
+
atLeast: condition.atLeast,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (condition.kind === 'rate') {
|
|
103
|
+
return {
|
|
104
|
+
kind: 'rate',
|
|
105
|
+
child: root,
|
|
106
|
+
ref: condition.ref,
|
|
107
|
+
atLeast: condition.atLeast,
|
|
108
|
+
perMs: condition.perMs,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
kind: 'distinct',
|
|
113
|
+
child: root,
|
|
114
|
+
ref: condition.ref,
|
|
115
|
+
path: condition.path,
|
|
116
|
+
atLeast: condition.atLeast,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function compileLegacyTriggerToPatternV2(definition) {
|
|
120
|
+
let root = expressionNode(definition);
|
|
121
|
+
const deterministic = definition.correlation?.deterministic;
|
|
122
|
+
if (deterministic?.fields?.length) {
|
|
123
|
+
const [anchorField, ...otherFields] = deterministic.fields;
|
|
124
|
+
if (!anchorField) {
|
|
125
|
+
throw new Error('Deterministic correlation requires at least one field');
|
|
126
|
+
}
|
|
127
|
+
for (const field of otherFields) {
|
|
128
|
+
root = {
|
|
129
|
+
kind: 'compare',
|
|
130
|
+
child: root,
|
|
131
|
+
left: {
|
|
132
|
+
kind: 'field',
|
|
133
|
+
ref: anchorField.ref,
|
|
134
|
+
path: anchorField.path,
|
|
135
|
+
select: 'last',
|
|
136
|
+
},
|
|
137
|
+
op: 'eq',
|
|
138
|
+
right: {
|
|
139
|
+
kind: 'field',
|
|
140
|
+
ref: field.ref,
|
|
141
|
+
path: field.path,
|
|
142
|
+
select: 'last',
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const condition of definition.temporal ?? []) {
|
|
148
|
+
root = wrapTemporal(root, condition);
|
|
149
|
+
}
|
|
150
|
+
if (definition.correlation?.semantic) {
|
|
151
|
+
const semantic = definition.correlation.semantic;
|
|
152
|
+
root = {
|
|
153
|
+
kind: 'semantic',
|
|
154
|
+
id: 'legacy-semantic-correlation',
|
|
155
|
+
child: root,
|
|
156
|
+
refs: [...new Set(semantic.input
|
|
157
|
+
.map((path) => path.split('.')[0])
|
|
158
|
+
.filter(Boolean))],
|
|
159
|
+
instruction: semantic.instruction,
|
|
160
|
+
input: semantic.input,
|
|
161
|
+
matchThreshold: semantic.matchThreshold,
|
|
162
|
+
rejectThreshold: semantic.rejectThreshold,
|
|
163
|
+
uncertain: semantic.uncertain,
|
|
164
|
+
execution: {
|
|
165
|
+
cache: true,
|
|
166
|
+
timeoutMs: 5000,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return PatternAstV2DefinitionSchema.parse({
|
|
171
|
+
version: '2',
|
|
172
|
+
root,
|
|
173
|
+
partitionBy: [],
|
|
174
|
+
selection: {
|
|
175
|
+
overlap: 'disallow',
|
|
176
|
+
afterMatch: 'skipPastLast',
|
|
177
|
+
maxMatchesPerEvent: 10,
|
|
178
|
+
},
|
|
179
|
+
execution: {
|
|
180
|
+
maxCandidates: 512,
|
|
181
|
+
maxSemanticEvaluations: 16,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
@@ -26,6 +26,7 @@ export interface EventDescriptor {
|
|
|
26
26
|
delivery: Array<'poll' | 'push' | 'webhook'>;
|
|
27
27
|
inputSchema: Record<string, unknown>;
|
|
28
28
|
payloadSchema: Record<string, unknown>;
|
|
29
|
+
_meta?: Record<string, unknown>;
|
|
29
30
|
}
|
|
30
31
|
export interface EventOccurrence<TData extends Record<string, unknown> = Record<string, unknown>> {
|
|
31
32
|
eventId: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-event-intelligence",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "MCP-native complex event processing for durable agent workflows: event-time patterns, semantic predicates, derived events, and targeted wakeups.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/src/intelligenceProtocol/index.js",
|
|
@@ -61,6 +61,9 @@
|
|
|
61
61
|
"model-context-protocol",
|
|
62
62
|
"ai-agents",
|
|
63
63
|
"event-driven",
|
|
64
|
+
"complex-event-processing",
|
|
65
|
+
"cep",
|
|
66
|
+
"event-time",
|
|
64
67
|
"temporal",
|
|
65
68
|
"durable-execution",
|
|
66
69
|
"triggers",
|
|
@@ -11,6 +11,33 @@ export interface HostMcpClientLike {
|
|
|
11
11
|
getServerCapabilities?(): unknown;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface HostMcpEventDeliveryCallbacks {
|
|
15
|
+
subscriptionId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
arguments: Record<string, unknown>;
|
|
18
|
+
cursor: string | null;
|
|
19
|
+
onEvent(event: Record<string, unknown>): Promise<void>;
|
|
20
|
+
onActive(state?: Record<string, unknown>): Promise<void>;
|
|
21
|
+
onGap(state?: Record<string, unknown>): Promise<void>;
|
|
22
|
+
onError(error: unknown): void;
|
|
23
|
+
onTerminated(detail?: unknown): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type HostMcpEventDeliveryHandle =
|
|
27
|
+
| (() => void | Promise<void>)
|
|
28
|
+
| {
|
|
29
|
+
cursor?: string | null;
|
|
30
|
+
truncated?: boolean;
|
|
31
|
+
close?(): void | Promise<void>;
|
|
32
|
+
cancel?(): void | Promise<void>;
|
|
33
|
+
unsubscribe?(): void | Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type HostMcpEventDeliveryOpener = (
|
|
37
|
+
input: HostMcpEventDeliveryCallbacks,
|
|
38
|
+
) => HostMcpEventDeliveryHandle | Promise<HostMcpEventDeliveryHandle>;
|
|
39
|
+
|
|
40
|
+
|
|
14
41
|
export interface HostMcpConnectionOptions {
|
|
15
42
|
connectionId?: string;
|
|
16
43
|
id?: string;
|
|
@@ -18,6 +45,8 @@ export interface HostMcpConnectionOptions {
|
|
|
18
45
|
serverId?: string;
|
|
19
46
|
client?: HostMcpClientLike;
|
|
20
47
|
request?: (method: string, params?: unknown) => Promise<unknown>;
|
|
48
|
+
getCapabilities?: () => unknown | Promise<unknown>;
|
|
49
|
+
capabilities?: Record<string, unknown>;
|
|
21
50
|
enabled?: boolean;
|
|
22
51
|
/** Isolation partition used by the shared EI host. Defaults to \"default\". */
|
|
23
52
|
scopeId?: string;
|
|
@@ -25,6 +54,14 @@ export interface HostMcpConnectionOptions {
|
|
|
25
54
|
maxEvents?: number;
|
|
26
55
|
/** Maximum number of immediately drained pages when the provider reports hasMore. */
|
|
27
56
|
maxPollBatches?: number;
|
|
57
|
+
/** Prefer one advertised delivery mode when the host can provide it. */
|
|
58
|
+
preferredDelivery?: 'poll' | 'push' | 'webhook';
|
|
59
|
+
/** Versioned MCP Events wire profile. Defaults to auto/current experimental snapshot. */
|
|
60
|
+
compatibilityProfile?: 'auto' | 'experimental-extension-2026-09-25' | string;
|
|
61
|
+
/** Host-owned adapter for events/stream delivery. */
|
|
62
|
+
openEventStream?: HostMcpEventDeliveryOpener;
|
|
63
|
+
/** Host-owned adapter for webhook subscription/receiver delivery. */
|
|
64
|
+
createWebhookSubscription?: HostMcpEventDeliveryOpener;
|
|
28
65
|
}
|
|
29
66
|
|
|
30
67
|
export interface HostMcpRegistry {
|
|
@@ -78,6 +115,10 @@ export interface EventIntelligenceScopedHost {
|
|
|
78
115
|
readonly triggerPlanner: any;
|
|
79
116
|
readonly activationHydrator: any;
|
|
80
117
|
readonly eventSources: any[];
|
|
118
|
+
describeTriggerLanguage(input?: {
|
|
119
|
+
category?: 'predicates' | 'composition' | 'temporal' | 'pattern' | 'windows' | 'aggregates' | 'state' | 'selection' | 'semantic' | 'execution' | 'correlation' | 'timing' | 'lifecycle';
|
|
120
|
+
operator?: string;
|
|
121
|
+
}): Record<string, unknown>;
|
|
81
122
|
planTrigger(input: TriggerPlanInput): Promise<{
|
|
82
123
|
planVersion: '1';
|
|
83
124
|
definition: any;
|
|
@@ -113,9 +154,15 @@ export function createHostMcpEventsConnection(
|
|
|
113
154
|
connectionId: string;
|
|
114
155
|
serverId: string;
|
|
115
156
|
request(method: string, params?: unknown): Promise<unknown>;
|
|
157
|
+
getCapabilities(): unknown | Promise<unknown>;
|
|
116
158
|
enabled: boolean;
|
|
117
159
|
pollIntervalMs: number;
|
|
118
160
|
maxEvents: number;
|
|
161
|
+
maxPollBatches: number;
|
|
162
|
+
preferredDelivery: 'poll' | 'push' | 'webhook' | null;
|
|
163
|
+
compatibilityProfile: string;
|
|
164
|
+
openEventStream: HostMcpEventDeliveryOpener | null;
|
|
165
|
+
createWebhookSubscription: HostMcpEventDeliveryOpener | null;
|
|
119
166
|
};
|
|
120
167
|
|
|
121
168
|
export function createEventIntelligenceHost(
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describeTriggerLanguage,
|
|
3
|
+
} from '../dist/src/intelligenceProtocol/index.js';
|
|
1
4
|
import {
|
|
2
5
|
createLocalEventIntelligenceRuntime,
|
|
3
6
|
} from './lib/local-event-intelligence-runtime.mjs';
|
|
@@ -79,6 +82,9 @@ function scopedHostView(runtime, context) {
|
|
|
79
82
|
planTrigger(input) {
|
|
80
83
|
return context.triggerPlanner.plan(input);
|
|
81
84
|
},
|
|
85
|
+
describeTriggerLanguage(input = {}) {
|
|
86
|
+
return describeTriggerLanguage(input);
|
|
87
|
+
},
|
|
82
88
|
hydrateWake(wakeId) {
|
|
83
89
|
return context.activationHydrator.hydrateWake(wakeId);
|
|
84
90
|
},
|
|
@@ -240,6 +246,9 @@ export async function createEventIntelligenceHost({
|
|
|
240
246
|
planTrigger(input) {
|
|
241
247
|
return runtime.triggerPlanner.plan(input);
|
|
242
248
|
},
|
|
249
|
+
describeTriggerLanguage(input = {}) {
|
|
250
|
+
return describeTriggerLanguage(input);
|
|
251
|
+
},
|
|
243
252
|
hydrateWake(wakeId) {
|
|
244
253
|
return runtime.activationHydrator.hydrateWake(wakeId);
|
|
245
254
|
},
|
|
@@ -100,6 +100,7 @@ export class ActivationHydrator {
|
|
|
100
100
|
eventName: event.eventName,
|
|
101
101
|
traceId: event.traceId,
|
|
102
102
|
occurredAt: event.occurredAt,
|
|
103
|
+
...(event.receivedAt ? { receivedAt: event.receivedAt } : {}),
|
|
103
104
|
payloadHash: event.payloadHash ?? null,
|
|
104
105
|
...(includeData ? { data: event.data } : {}),
|
|
105
106
|
}));
|
|
@@ -34,6 +34,8 @@ export class CompositeEventConsumer {
|
|
|
34
34
|
serverId,
|
|
35
35
|
provider,
|
|
36
36
|
traceId,
|
|
37
|
+
subscriptionArguments,
|
|
38
|
+
receivedAt,
|
|
37
39
|
}) {
|
|
38
40
|
const correlatable = await mcpOccurrenceToCorrelatableEvent(
|
|
39
41
|
event,
|
|
@@ -41,6 +43,8 @@ export class CompositeEventConsumer {
|
|
|
41
43
|
traceId,
|
|
42
44
|
...(provider ? { provider } : {}),
|
|
43
45
|
...(serverId ? { serverId } : {}),
|
|
46
|
+
subscriptionArguments: subscriptionArguments ?? {},
|
|
47
|
+
...(receivedAt ? { receivedAt } : {}),
|
|
44
48
|
},
|
|
45
49
|
);
|
|
46
50
|
|
|
@@ -2,6 +2,12 @@ import {
|
|
|
2
2
|
parseDerivedEventRecord,
|
|
3
3
|
sha256Hex,
|
|
4
4
|
} from '../../dist/src/intelligenceProtocol/index.js';
|
|
5
|
+
import {
|
|
6
|
+
evaluateTemporalConditions,
|
|
7
|
+
} from '../../dist/src/composite/temporal.js';
|
|
8
|
+
import {
|
|
9
|
+
evaluatePatternMeasure,
|
|
10
|
+
} from '../../dist/src/patternV2/evaluator.js';
|
|
5
11
|
|
|
6
12
|
export const DERIVED_EVENT_SERVER_ID = 'event-intelligence:derived';
|
|
7
13
|
export const DERIVED_EVENT_CONNECTION_ID = 'event-intelligence:derived';
|
|
@@ -26,6 +32,7 @@ function evidenceRef(source) {
|
|
|
26
32
|
sourceEventId: source.sourceEventId,
|
|
27
33
|
traceId: source.traceId,
|
|
28
34
|
occurredAt: source.occurredAt,
|
|
35
|
+
...(source.receivedAt ? { receivedAt: source.receivedAt } : {}),
|
|
29
36
|
payloadHash: source.payloadHash ?? null,
|
|
30
37
|
contractVersion:
|
|
31
38
|
source.data?._derived?.contractVersion ?? null,
|
|
@@ -60,6 +67,51 @@ function latestForClause(match, clauseId) {
|
|
|
60
67
|
)[0] ?? null;
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
function patternCandidateForMatch(match) {
|
|
71
|
+
const bindings = {};
|
|
72
|
+
for (const event of match.sourceEvents) {
|
|
73
|
+
bindings[event.clauseId] ??= [];
|
|
74
|
+
bindings[event.clauseId].push(event);
|
|
75
|
+
}
|
|
76
|
+
for (const events of Object.values(bindings)) {
|
|
77
|
+
events.sort((a, b) =>
|
|
78
|
+
Date.parse(a.occurredAt) - Date.parse(b.occurredAt) ||
|
|
79
|
+
a.sourceEventId.localeCompare(b.sourceEventId)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const events = [...match.sourceEvents].sort((a, b) =>
|
|
83
|
+
Date.parse(a.occurredAt) - Date.parse(b.occurredAt) ||
|
|
84
|
+
a.sourceEventId.localeCompare(b.sourceEventId)
|
|
85
|
+
);
|
|
86
|
+
return {
|
|
87
|
+
bindings,
|
|
88
|
+
events,
|
|
89
|
+
startIndex: events.length ? 0 : -1,
|
|
90
|
+
endIndex: events.length ? events.length - 1 : -1,
|
|
91
|
+
semanticDecisions: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function derivedOccurredAt(match, definition) {
|
|
96
|
+
const candidates = match.sourceEvents
|
|
97
|
+
.map((event) => Date.parse(event.occurredAt))
|
|
98
|
+
.filter(Number.isFinite);
|
|
99
|
+
|
|
100
|
+
const temporal = evaluateTemporalConditions(
|
|
101
|
+
definition,
|
|
102
|
+
match,
|
|
103
|
+
new Date(match.updatedAt),
|
|
104
|
+
);
|
|
105
|
+
for (const state of temporal.conditionStates) {
|
|
106
|
+
if (state.status !== 'satisfied' || !state.dueAt) continue;
|
|
107
|
+
const dueAt = Date.parse(state.dueAt);
|
|
108
|
+
if (Number.isFinite(dueAt)) candidates.push(dueAt);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!candidates.length) return match.updatedAt;
|
|
112
|
+
return new Date(Math.max(...candidates)).toISOString();
|
|
113
|
+
}
|
|
114
|
+
|
|
63
115
|
export class DerivedEventCoordinator {
|
|
64
116
|
constructor({
|
|
65
117
|
store,
|
|
@@ -147,6 +199,26 @@ export class DerivedEventCoordinator {
|
|
|
147
199
|
}
|
|
148
200
|
}
|
|
149
201
|
|
|
202
|
+
const measured = {};
|
|
203
|
+
if (spec.measures?.length) {
|
|
204
|
+
const candidate = patternCandidateForMatch(match);
|
|
205
|
+
for (const measure of spec.measures) {
|
|
206
|
+
const value = evaluatePatternMeasure(candidate, measure);
|
|
207
|
+
if (
|
|
208
|
+
value === undefined ||
|
|
209
|
+
(
|
|
210
|
+
value !== null &&
|
|
211
|
+
!['string', 'number', 'boolean'].includes(typeof value)
|
|
212
|
+
)
|
|
213
|
+
) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Derived event measure must resolve to scalar or null: ${measure.key}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
measured[measure.key] = value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
150
222
|
const contract = this.store.getDerivedContract(
|
|
151
223
|
spec.name,
|
|
152
224
|
spec.contractVersion,
|
|
@@ -162,6 +234,7 @@ export class DerivedEventCoordinator {
|
|
|
162
234
|
const data = {
|
|
163
235
|
...(spec.constants ?? {}),
|
|
164
236
|
...projected,
|
|
237
|
+
...measured,
|
|
165
238
|
_derived: {
|
|
166
239
|
triggerId: definition.triggerId,
|
|
167
240
|
triggerVersion: definition.version,
|
|
@@ -176,7 +249,8 @@ export class DerivedEventCoordinator {
|
|
|
176
249
|
traceId: `derived:${eventId}`,
|
|
177
250
|
sourceEventId: eventId,
|
|
178
251
|
name: spec.name,
|
|
179
|
-
occurredAt: match
|
|
252
|
+
occurredAt: derivedOccurredAt(match, definition),
|
|
253
|
+
receivedAt: this.now().toISOString(),
|
|
180
254
|
provider: 'event-intelligence',
|
|
181
255
|
serverId: DERIVED_EVENT_SERVER_ID,
|
|
182
256
|
payloadHash,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
function assertRecord(value, message) {
|
|
2
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
3
|
+
throw new Error(message);
|
|
4
|
+
}
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function schemaTypes(schema) {
|
|
9
|
+
if (Array.isArray(schema?.type)) return schema.type.map(String);
|
|
10
|
+
return schema?.type === undefined ? [] : [String(schema.type)];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function assertJsonSchemaValue(schemaInput, value, path = '$') {
|
|
14
|
+
if (!schemaInput || Array.isArray(schemaInput) || typeof schemaInput !== 'object') {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const schema = schemaInput;
|
|
18
|
+
|
|
19
|
+
if (
|
|
20
|
+
Object.prototype.hasOwnProperty.call(schema, 'const') &&
|
|
21
|
+
!Object.is(schema.const, value)
|
|
22
|
+
) {
|
|
23
|
+
throw new Error(`JSON Schema violation at ${path}: const mismatch`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
Array.isArray(schema.enum) &&
|
|
28
|
+
!schema.enum.some((entry) => Object.is(entry, value))
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(`JSON Schema violation at ${path}: value not in enum`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const types = schemaTypes(schema);
|
|
34
|
+
if (types.length) {
|
|
35
|
+
const matches = types.some((type) => {
|
|
36
|
+
if (type === 'null') return value === null;
|
|
37
|
+
if (type === 'array') return Array.isArray(value);
|
|
38
|
+
if (type === 'object') {
|
|
39
|
+
return value !== null && !Array.isArray(value) && typeof value === 'object';
|
|
40
|
+
}
|
|
41
|
+
if (type === 'integer') return Number.isInteger(value);
|
|
42
|
+
if (type === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
43
|
+
if (type === 'string') return typeof value === 'string';
|
|
44
|
+
if (type === 'boolean') return typeof value === 'boolean';
|
|
45
|
+
return false;
|
|
46
|
+
});
|
|
47
|
+
if (!matches) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`JSON Schema violation at ${path}: expected ${types.join('|')}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof value === 'string') {
|
|
55
|
+
if (typeof schema.minLength === 'number' && value.length < schema.minLength) {
|
|
56
|
+
throw new Error(`JSON Schema violation at ${path}: string too short`);
|
|
57
|
+
}
|
|
58
|
+
if (typeof schema.maxLength === 'number' && value.length > schema.maxLength) {
|
|
59
|
+
throw new Error(`JSON Schema violation at ${path}: string too long`);
|
|
60
|
+
}
|
|
61
|
+
if (typeof schema.pattern === 'string') {
|
|
62
|
+
const re = new RegExp(schema.pattern);
|
|
63
|
+
if (!re.test(value)) {
|
|
64
|
+
throw new Error(`JSON Schema violation at ${path}: pattern mismatch`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
70
|
+
if (typeof schema.minimum === 'number' && value < schema.minimum) {
|
|
71
|
+
throw new Error(`JSON Schema violation at ${path}: below minimum`);
|
|
72
|
+
}
|
|
73
|
+
if (typeof schema.maximum === 'number' && value > schema.maximum) {
|
|
74
|
+
throw new Error(`JSON Schema violation at ${path}: above maximum`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (value !== null && !Array.isArray(value) && typeof value === 'object') {
|
|
79
|
+
const record = value;
|
|
80
|
+
const required = Array.isArray(schema.required)
|
|
81
|
+
? schema.required.map(String)
|
|
82
|
+
: [];
|
|
83
|
+
for (const key of required) {
|
|
84
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`JSON Schema violation at ${path}: missing required property ${key}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const properties =
|
|
92
|
+
schema.properties &&
|
|
93
|
+
!Array.isArray(schema.properties) &&
|
|
94
|
+
typeof schema.properties === 'object'
|
|
95
|
+
? schema.properties
|
|
96
|
+
: {};
|
|
97
|
+
|
|
98
|
+
for (const [key, childSchema] of Object.entries(properties)) {
|
|
99
|
+
if (
|
|
100
|
+
Object.prototype.hasOwnProperty.call(record, key) &&
|
|
101
|
+
childSchema &&
|
|
102
|
+
!Array.isArray(childSchema) &&
|
|
103
|
+
typeof childSchema === 'object'
|
|
104
|
+
) {
|
|
105
|
+
assertJsonSchemaValue(childSchema, record[key], `${path}.${key}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (schema.additionalProperties === false) {
|
|
110
|
+
for (const key of Object.keys(record)) {
|
|
111
|
+
if (!Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`JSON Schema violation at ${path}: unexpected property ${key}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
Array.isArray(value) &&
|
|
122
|
+
schema.items &&
|
|
123
|
+
!Array.isArray(schema.items) &&
|
|
124
|
+
typeof schema.items === 'object'
|
|
125
|
+
) {
|
|
126
|
+
value.forEach((entry, index) =>
|
|
127
|
+
assertJsonSchemaValue(schema.items, entry, `${path}[${index}]`),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function assertJsonObject(value, message) {
|
|
133
|
+
return assertRecord(value, message);
|
|
134
|
+
}
|