mcp-event-intelligence 0.4.0 → 0.6.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 (64) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +107 -26
  3. package/SECURITY.md +4 -4
  4. package/bin/mcp-event-intelligence.mjs +3 -3
  5. package/dist/src/composite/engine.d.ts +14 -4
  6. package/dist/src/composite/engine.js +533 -208
  7. package/dist/src/composite/matching.d.ts +1 -5
  8. package/dist/src/composite/matching.js +45 -54
  9. package/dist/src/composite/runtimeSupport.js +10 -6
  10. package/dist/src/composite/store.d.ts +38 -4
  11. package/dist/src/composite/temporal.d.ts +0 -17
  12. package/dist/src/composite/temporal.js +0 -266
  13. package/dist/src/intelligenceProtocol/activationSchemas.d.ts +38 -3
  14. package/dist/src/intelligenceProtocol/activationSchemas.js +5 -3
  15. package/dist/src/intelligenceProtocol/index.d.ts +12 -9
  16. package/dist/src/intelligenceProtocol/index.js +6 -5
  17. package/dist/src/intelligenceProtocol/lifecycle.js +2 -10
  18. package/dist/src/intelligenceProtocol/operatorRegistry.d.ts +300 -0
  19. package/dist/src/intelligenceProtocol/operatorRegistry.js +338 -0
  20. package/dist/src/intelligenceProtocol/patternV2Schemas.d.ts +188 -0
  21. package/dist/src/intelligenceProtocol/patternV2Schemas.js +801 -0
  22. package/dist/src/intelligenceProtocol/schemas.d.ts +4 -189
  23. package/dist/src/intelligenceProtocol/schemas.js +3 -97
  24. package/dist/src/intelligenceProtocol/triggerPlanSchemas.d.ts +114 -38
  25. package/dist/src/intelligenceProtocol/triggerPlanSchemas.js +27 -18
  26. package/dist/src/intelligenceProtocol/triggerSchemas.d.ts +268 -213
  27. package/dist/src/intelligenceProtocol/triggerSchemas.js +155 -169
  28. package/dist/src/intelligenceProtocol/version.d.ts +2 -2
  29. package/dist/src/intelligenceProtocol/version.js +2 -2
  30. package/dist/src/mcpEvents/consumer.d.ts +1 -0
  31. package/dist/src/mcpEvents/consumer.js +1 -0
  32. package/dist/src/patternV2/evaluator.d.ts +51 -0
  33. package/dist/src/patternV2/evaluator.js +986 -0
  34. package/dist/src/semantic/types.d.ts +6 -0
  35. package/dist/src/semantic/typesafeJevEvaluator.d.ts +1 -0
  36. package/dist/src/semantic/typesafeJevEvaluator.js +6 -0
  37. package/package.json +9 -3
  38. package/scripts/host-integration.d.mts +54 -4
  39. package/scripts/host-integration.mjs +25 -4
  40. package/scripts/lib/activation-hydrator.mjs +13 -12
  41. package/scripts/lib/composite-event-consumer.mjs +10 -28
  42. package/scripts/lib/composite-wake-coordinator.mjs +19 -19
  43. package/scripts/lib/derived-contract-registry.mjs +4 -4
  44. package/scripts/lib/derived-event-coordinator.mjs +69 -6
  45. package/scripts/lib/local-event-intelligence-runtime.mjs +35 -3
  46. package/scripts/lib/mcp-events-client.mjs +97 -55
  47. package/scripts/lib/mcp-events-compatibility.mjs +54 -0
  48. package/scripts/lib/persistent-event-store.mjs +228 -61
  49. package/scripts/lib/store-capabilities.mjs +88 -0
  50. package/scripts/lib/store-conformance.mjs +127 -0
  51. package/scripts/lib/temporal-deadline-scheduler.mjs +3 -3
  52. package/scripts/lib/trigger-control-plane.mjs +202 -53
  53. package/scripts/lib/trigger-inspector.mjs +50 -147
  54. package/scripts/lib/trigger-planner.mjs +141 -52
  55. package/scripts/lib/wake-retry-scheduler.mjs +2 -19
  56. package/scripts/mcp-stdio-server.mjs +46 -61
  57. package/scripts/service.mjs +19 -138
  58. package/server.json +4 -4
  59. package/dist/src/intelligenceProtocol/invariants.d.ts +0 -3
  60. package/dist/src/intelligenceProtocol/invariants.js +0 -36
  61. package/dist/src/intelligenceProtocol/mapper.d.ts +0 -14
  62. package/dist/src/intelligenceProtocol/mapper.js +0 -28
  63. package/scripts/lib/event-processor.mjs +0 -624
  64. package/scripts/lib/http-wake-deliverer.mjs +0 -31
@@ -1,5 +1,15 @@
1
- import { appendFile, mkdir, readFile, readdir } from 'node:fs/promises';
1
+ import {
2
+ appendFile,
3
+ mkdir,
4
+ readFile,
5
+ readdir,
6
+ rename,
7
+ writeFile,
8
+ } from 'node:fs/promises';
2
9
  import path from 'node:path';
10
+ import {
11
+ REFERENCE_STORE_CAPABILITIES,
12
+ } from './store-capabilities.mjs';
3
13
  import {
4
14
  AuditChain,
5
15
  parseAuditRecord,
@@ -61,11 +71,17 @@ async function readJsonLines(filePath) {
61
71
  }
62
72
  }
63
73
 
74
+ async function writeJsonLinesAtomic(filePath, records) {
75
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
76
+ const content = records.length
77
+ ? `${records.map((record) => JSON.stringify(record)).join('\n')}\n`
78
+ : '';
79
+ await writeFile(tempPath, content, 'utf8');
80
+ await rename(tempPath, filePath);
81
+ }
82
+
64
83
  export class PersistentEventStore {
65
84
  #queue = Promise.resolve();
66
- #eventKeys = new Set();
67
- #events = [];
68
- #decisions = [];
69
85
  #wakes = [];
70
86
  #wakeDeliveries = new Map();
71
87
  #triggers = new Map();
@@ -79,14 +95,14 @@ export class PersistentEventStore {
79
95
  #temporalDeadlines = new Map();
80
96
  #derivedEvents = new Map();
81
97
  #derivedContracts = new Map();
98
+ #semanticDecisionCache = new Map();
99
+ #partitionLeases = new Map();
82
100
  #auditChain = new AuditChain();
83
101
  #scopeStores = new Map();
84
102
 
85
103
  constructor(dataDir) {
86
104
  this.dataDir = dataDir;
87
105
  this.files = {
88
- events: path.join(dataDir, 'events.jsonl'),
89
- decisions: path.join(dataDir, 'decisions.jsonl'),
90
106
  wakes: path.join(dataDir, 'wakes.jsonl'),
91
107
  wakeDeliveries: path.join(dataDir, 'wake-deliveries.jsonl'),
92
108
  triggers: path.join(dataDir, 'triggers.jsonl'),
@@ -98,10 +114,16 @@ export class PersistentEventStore {
98
114
  temporalDeadlines: path.join(dataDir, 'temporal-deadlines.jsonl'),
99
115
  derivedEvents: path.join(dataDir, 'derived-events.jsonl'),
100
116
  derivedContracts: path.join(dataDir, 'derived-contracts.jsonl'),
117
+ semanticDecisionCache: path.join(dataDir, 'semantic-decision-cache.jsonl'),
118
+ partitionLeases: path.join(dataDir, 'partition-leases.jsonl'),
101
119
  audit: path.join(dataDir, 'audit.jsonl'),
102
120
  };
103
121
  }
104
122
 
123
+ storeCapabilities() {
124
+ return { ...REFERENCE_STORE_CAPABILITIES };
125
+ }
126
+
105
127
  async forScope(scopeIdInput = DEFAULT_EVENT_SCOPE_ID) {
106
128
  const scopeId = normalizeEventScopeId(scopeIdInput);
107
129
  if (scopeId === DEFAULT_EVENT_SCOPE_ID) return this;
@@ -141,8 +163,6 @@ export class PersistentEventStore {
141
163
  await mkdir(this.dataDir, { recursive: true });
142
164
 
143
165
  const [
144
- events,
145
- decisions,
146
166
  wakes,
147
167
  wakeDeliveries,
148
168
  triggers,
@@ -154,10 +174,10 @@ export class PersistentEventStore {
154
174
  temporalDeadlines,
155
175
  derivedEvents,
156
176
  derivedContracts,
177
+ semanticDecisionCache,
178
+ partitionLeases,
157
179
  audit,
158
180
  ] = await Promise.all([
159
- readJsonLines(this.files.events),
160
- readJsonLines(this.files.decisions),
161
181
  readJsonLines(this.files.wakes),
162
182
  readJsonLines(this.files.wakeDeliveries),
163
183
  readJsonLines(this.files.triggers),
@@ -169,16 +189,16 @@ export class PersistentEventStore {
169
189
  readJsonLines(this.files.temporalDeadlines),
170
190
  readJsonLines(this.files.derivedEvents),
171
191
  readJsonLines(this.files.derivedContracts),
192
+ readJsonLines(this.files.semanticDecisionCache),
193
+ readJsonLines(this.files.partitionLeases),
172
194
  readJsonLines(this.files.audit),
173
195
  ]);
174
196
 
175
- this.#events = events;
176
- this.#decisions = decisions;
177
197
  this.#wakes = wakes;
178
198
 
179
199
  this.#wakeDeliveries = new Map();
180
200
  for (const raw of wakeDeliveries) {
181
- if (!raw?.wakeId || !raw?.runtime || (!raw?.sourceId && !raw?.matchId)) continue;
201
+ if (!raw?.wakeId || !raw?.runtime || !raw?.matchId) continue;
182
202
  const status = [
183
203
  'pending',
184
204
  'claimed',
@@ -190,8 +210,6 @@ export class PersistentEventStore {
190
210
  : 'pending';
191
211
  const record = {
192
212
  wakeId: String(raw.wakeId),
193
- sourceType: raw.sourceType === 'event' ? 'event' : 'composite',
194
- sourceId: String(raw.sourceId || raw.matchId),
195
213
  matchId: raw.matchId ? String(raw.matchId) : '',
196
214
  triggerId: String(raw.triggerId || ''),
197
215
  triggerVersion: String(raw.triggerVersion || '1'),
@@ -379,19 +397,48 @@ export class PersistentEventStore {
379
397
  );
380
398
  }
381
399
 
382
- this.#auditChain = new AuditChain(audit.map(parseAuditRecord));
400
+ this.#partitionLeases = new Map();
401
+ for (const raw of partitionLeases) {
402
+ if (!raw?.partitionKey) continue;
403
+ const key = String(raw.partitionKey);
404
+ if (raw.status === 'released') {
405
+ this.#partitionLeases.delete(key);
406
+ continue;
407
+ }
408
+ if (!raw.workerId || !raw.leaseUntil) continue;
409
+ this.#partitionLeases.set(key, {
410
+ partitionKey: key,
411
+ workerId: String(raw.workerId),
412
+ leaseUntil: String(raw.leaseUntil),
413
+ updatedAt: String(raw.updatedAt || new Date(0).toISOString()),
414
+ status: 'active',
415
+ });
416
+ }
383
417
 
384
- for (const record of events) {
385
- this.#eventKeys.add(this.#eventKey(record.lineage));
418
+ this.#semanticDecisionCache = new Map();
419
+ for (const raw of semanticDecisionCache) {
420
+ if (!raw?.key || !raw?.decision || typeof raw.decision !== 'object') {
421
+ continue;
422
+ }
423
+ this.#semanticDecisionCache.set(String(raw.key), {
424
+ key: String(raw.key),
425
+ decision: raw.decision,
426
+ createdAt: String(
427
+ raw.createdAt || raw.updatedAt || new Date(0).toISOString()
428
+ ),
429
+ updatedAt: String(
430
+ raw.updatedAt || raw.createdAt || new Date(0).toISOString()
431
+ ),
432
+ });
386
433
  }
387
434
 
435
+ this.#auditChain = new AuditChain(audit.map(parseAuditRecord));
436
+
388
437
  if (!(await this.#auditChain.verify())) {
389
438
  throw new Error('Persistent audit chain verification failed');
390
439
  }
391
440
 
392
441
  return {
393
- events: events.length,
394
- decisions: decisions.length,
395
442
  wakes: wakes.length,
396
443
  wakeDeliveries: this.#wakeDeliveries.size,
397
444
  triggers: this.#triggers.size,
@@ -404,30 +451,12 @@ export class PersistentEventStore {
404
451
  temporalDeadlines: this.#temporalDeadlines.size,
405
452
  derivedEvents: this.#derivedEvents.size,
406
453
  derivedContracts: this.#derivedContracts.size,
454
+ semanticDecisionCache: this.#semanticDecisionCache.size,
455
+ partitionLeases: this.#partitionLeases.size,
407
456
  audit: audit.length,
408
457
  };
409
458
  }
410
459
 
411
- async registerEvent(record) {
412
- return this.#serialized(async () => {
413
- const key = this.#eventKey(record.lineage);
414
- if (this.#eventKeys.has(key)) return false;
415
-
416
- this.#eventKeys.add(key);
417
- this.#events.push(record);
418
- await appendFile(this.files.events, `${JSON.stringify(record)}\n`, 'utf8');
419
- return true;
420
- });
421
- }
422
-
423
- async appendDecision(record) {
424
- return this.#serialized(async () => {
425
- this.#decisions.push(record);
426
- await appendFile(this.files.decisions, `${JSON.stringify(record)}\n`, 'utf8');
427
- return record;
428
- });
429
- }
430
-
431
460
  async appendWake(record) {
432
461
  return this.#serialized(async () => {
433
462
  this.#wakes.push(record);
@@ -452,12 +481,7 @@ export class PersistentEventStore {
452
481
  }
453
482
  if (record.status === 'delivered') {
454
483
  const latestWake = this.latestWake(record.wakeId);
455
- const wakeFinalized =
456
- latestWake?.status === 'delivered' ||
457
- latestWake?.status === 'handled';
458
- if (record.sourceType === 'event') {
459
- return !wakeFinalized;
460
- }
484
+ const wakeFinalized = latestWake?.status === 'delivered';
461
485
  const match = this.#triggerMatches.get(record.matchId);
462
486
  return !wakeFinalized || match?.status !== 'fired';
463
487
  }
@@ -472,15 +496,10 @@ export class PersistentEventStore {
472
496
  async ensureWakeDelivery(input) {
473
497
  return this.#serialized(async () => {
474
498
  const wakeId = String(input.wakeId || '');
475
- const sourceType = input.sourceType === 'event' ? 'event' : 'composite';
476
499
  const matchId = String(input.matchId || '');
477
- const sourceId = String(input.sourceId || matchId || '');
478
500
  const runtime = String(input.runtime || '');
479
- if (!wakeId || !sourceId || !runtime) {
480
- throw new Error('Wake delivery requires wakeId, sourceId and runtime');
481
- }
482
- if (sourceType === 'composite' && !matchId) {
483
- throw new Error('Composite wake delivery requires matchId');
501
+ if (!wakeId || !matchId || !runtime) {
502
+ throw new Error('Wake delivery requires wakeId, matchId and runtime');
484
503
  }
485
504
  const existing = this.#wakeDeliveries.get(wakeId);
486
505
  if (existing) return existing;
@@ -488,8 +507,6 @@ export class PersistentEventStore {
488
507
  const now = String(input.now ?? new Date().toISOString());
489
508
  const record = {
490
509
  wakeId,
491
- sourceType,
492
- sourceId,
493
510
  matchId,
494
511
  triggerId: String(input.triggerId || ''),
495
512
  triggerVersion: String(input.triggerVersion || '1'),
@@ -1077,6 +1094,162 @@ export class PersistentEventStore {
1077
1094
  });
1078
1095
  }
1079
1096
 
1097
+ getPartitionLease(partitionKeyInput) {
1098
+ return this.#partitionLeases.get(String(partitionKeyInput || '')) ?? null;
1099
+ }
1100
+
1101
+ async claimPartitionLease(partitionKeyInput, {
1102
+ workerId,
1103
+ now = new Date().toISOString(),
1104
+ leaseMs = 30000,
1105
+ } = {}) {
1106
+ return this.#serialized(async () => {
1107
+ const partitionKey = String(partitionKeyInput || '').trim();
1108
+ const owner = String(workerId || '').trim();
1109
+ if (!partitionKey || !owner) {
1110
+ throw new Error('Partition lease requires partitionKey and workerId');
1111
+ }
1112
+ const nowMs = Date.parse(now);
1113
+ const current = this.#partitionLeases.get(partitionKey);
1114
+ if (
1115
+ current &&
1116
+ current.workerId !== owner &&
1117
+ Date.parse(current.leaseUntil) > nowMs
1118
+ ) {
1119
+ return null;
1120
+ }
1121
+ const record = {
1122
+ partitionKey,
1123
+ workerId: owner,
1124
+ leaseUntil: new Date(
1125
+ nowMs + Math.max(1000, Number(leaseMs) || 30000),
1126
+ ).toISOString(),
1127
+ updatedAt: now,
1128
+ status: 'active',
1129
+ };
1130
+ this.#partitionLeases.set(partitionKey, record);
1131
+ await appendFile(
1132
+ this.files.partitionLeases,
1133
+ `${JSON.stringify(record)}\n`,
1134
+ 'utf8',
1135
+ );
1136
+ return record;
1137
+ });
1138
+ }
1139
+
1140
+ async renewPartitionLease(partitionKeyInput, {
1141
+ workerId,
1142
+ now = new Date().toISOString(),
1143
+ leaseMs = 30000,
1144
+ } = {}) {
1145
+ return this.#serialized(async () => {
1146
+ const partitionKey = String(partitionKeyInput || '').trim();
1147
+ const owner = String(workerId || '').trim();
1148
+ const current = this.#partitionLeases.get(partitionKey);
1149
+ if (!current || current.workerId !== owner) return null;
1150
+ const record = {
1151
+ ...current,
1152
+ leaseUntil: new Date(
1153
+ Date.parse(now) + Math.max(1000, Number(leaseMs) || 30000),
1154
+ ).toISOString(),
1155
+ updatedAt: now,
1156
+ };
1157
+ this.#partitionLeases.set(partitionKey, record);
1158
+ await appendFile(
1159
+ this.files.partitionLeases,
1160
+ `${JSON.stringify(record)}\n`,
1161
+ 'utf8',
1162
+ );
1163
+ return record;
1164
+ });
1165
+ }
1166
+
1167
+ async releasePartitionLease(partitionKeyInput, {
1168
+ workerId,
1169
+ now = new Date().toISOString(),
1170
+ } = {}) {
1171
+ return this.#serialized(async () => {
1172
+ const partitionKey = String(partitionKeyInput || '').trim();
1173
+ const owner = String(workerId || '').trim();
1174
+ const current = this.#partitionLeases.get(partitionKey);
1175
+ if (!current || current.workerId !== owner) return false;
1176
+ this.#partitionLeases.delete(partitionKey);
1177
+ await appendFile(
1178
+ this.files.partitionLeases,
1179
+ `${JSON.stringify({
1180
+ partitionKey,
1181
+ workerId: owner,
1182
+ status: 'released',
1183
+ updatedAt: now,
1184
+ })}\n`,
1185
+ 'utf8',
1186
+ );
1187
+ return true;
1188
+ });
1189
+ }
1190
+
1191
+ async compactMutableState() {
1192
+ return this.#serialized(async () => {
1193
+ const snapshots = [
1194
+ [this.files.wakeDeliveries, [...this.#wakeDeliveries.values()]],
1195
+ [this.files.triggerStates, [...this.#triggerStates.values()]],
1196
+ [this.files.eventSources, [...this.#eventSources.values()]],
1197
+ [this.files.mcpClientStates, [...this.#mcpClientStates.values()]],
1198
+ [this.files.temporalDeadlines, [...this.#temporalDeadlines.values()]],
1199
+ [this.files.derivedContracts, [...this.#derivedContracts.values()]],
1200
+ [this.files.semanticDecisionCache, [...this.#semanticDecisionCache.values()]],
1201
+ [this.files.partitionLeases, [...this.#partitionLeases.values()]],
1202
+ ];
1203
+ for (const [filePath, records] of snapshots) {
1204
+ await writeJsonLinesAtomic(filePath, records);
1205
+ }
1206
+ return {
1207
+ ok: true,
1208
+ compacted: snapshots.map(([filePath, records]) => ({
1209
+ file: path.basename(filePath),
1210
+ records: records.length,
1211
+ })),
1212
+ };
1213
+ });
1214
+ }
1215
+
1216
+ getSemanticDecisionCache(keyInput) {
1217
+ const record = this.#semanticDecisionCache.get(
1218
+ String(keyInput || '')
1219
+ );
1220
+ return record?.decision ?? null;
1221
+ }
1222
+
1223
+ async putSemanticDecisionCache(keyInput, decision) {
1224
+ return this.#serialized(async () => {
1225
+ const key = String(keyInput || '').trim();
1226
+ if (!key || !decision || typeof decision !== 'object') {
1227
+ throw new Error(
1228
+ 'Semantic decision cache requires key and decision'
1229
+ );
1230
+ }
1231
+ const previous = this.#semanticDecisionCache.get(key);
1232
+ const now = new Date().toISOString();
1233
+ const record = {
1234
+ key,
1235
+ decision,
1236
+ createdAt: previous?.createdAt ?? now,
1237
+ updatedAt: now,
1238
+ };
1239
+ this.#semanticDecisionCache.set(key, record);
1240
+ await appendFile(
1241
+ this.files.semanticDecisionCache,
1242
+ `${JSON.stringify(record)}\n`,
1243
+ 'utf8',
1244
+ );
1245
+ return record;
1246
+ });
1247
+ }
1248
+
1249
+ semanticDecisionCacheSize() {
1250
+ return this.#semanticDecisionCache.size;
1251
+ }
1252
+
1080
1253
  async appendAudit(input) {
1081
1254
  return this.#serialized(async () => {
1082
1255
  const record = await this.#auditChain.append(input);
@@ -1094,8 +1267,6 @@ export class PersistentEventStore {
1094
1267
 
1095
1268
  trace(traceId) {
1096
1269
  return {
1097
- events: this.#events.filter((record) => record.lineage?.traceId === traceId),
1098
- decisions: this.#decisions.filter((record) => record.traceId === traceId),
1099
1270
  wakes: this.#wakes.filter((record) => record.traceId === traceId),
1100
1271
  triggerMatches: this.listTriggerMatches().filter((record) =>
1101
1272
  record.sourceEvents.some((event) => event.traceId === traceId),
@@ -1128,10 +1299,6 @@ export class PersistentEventStore {
1128
1299
  await this.drain();
1129
1300
  }
1130
1301
 
1131
- #eventKey(lineage) {
1132
- return `${lineage.environmentId}:${lineage.subscriptionId}:${lineage.sourceEventId}`;
1133
- }
1134
-
1135
1302
  #triggerKey(definition) {
1136
1303
  return `${definition.triggerId}@${definition.version}`;
1137
1304
  }
@@ -0,0 +1,88 @@
1
+ export const EVENT_INTELLIGENCE_STORE_CAPABILITIES_VERSION = '1';
2
+
3
+ export const UNDECLARED_STORE_CAPABILITIES = Object.freeze({
4
+ version: EVENT_INTELLIGENCE_STORE_CAPABILITIES_VERSION,
5
+ sharedState: 'none',
6
+ scopeIsolation: 'unknown',
7
+ wakeClaims: 'unknown',
8
+ partitionLeases: 'none',
9
+ mutableCompaction: 'none',
10
+ readModel: 'undeclared',
11
+ });
12
+
13
+ export const REFERENCE_STORE_CAPABILITIES = Object.freeze({
14
+ version: EVENT_INTELLIGENCE_STORE_CAPABILITIES_VERSION,
15
+ sharedState: 'single-process',
16
+ scopeIsolation: 'strong',
17
+ wakeClaims: 'process-atomic',
18
+ partitionLeases: 'process-atomic',
19
+ mutableCompaction: 'atomic-snapshot',
20
+ readModel: 'synchronous-materialized',
21
+ });
22
+
23
+ export function describeStoreCapabilities(store) {
24
+ if (
25
+ store &&
26
+ typeof store.storeCapabilities === 'function'
27
+ ) {
28
+ return {
29
+ ...UNDECLARED_STORE_CAPABILITIES,
30
+ ...store.storeCapabilities(),
31
+ };
32
+ }
33
+ return { ...UNDECLARED_STORE_CAPABILITIES };
34
+ }
35
+
36
+ export function validateSharedStoreCapabilities(store) {
37
+ const capabilities = describeStoreCapabilities(store);
38
+ const errors = [];
39
+
40
+ if (capabilities.sharedState !== 'strong') {
41
+ errors.push(
42
+ `sharedState must be strong; received ${capabilities.sharedState}`,
43
+ );
44
+ }
45
+ if (capabilities.scopeIsolation !== 'strong') {
46
+ errors.push(
47
+ `scopeIsolation must be strong; received ${capabilities.scopeIsolation}`,
48
+ );
49
+ }
50
+ if (capabilities.wakeClaims !== 'distributed-atomic') {
51
+ errors.push(
52
+ `wakeClaims must be distributed-atomic; received ${capabilities.wakeClaims}`,
53
+ );
54
+ }
55
+ if (capabilities.partitionLeases !== 'distributed-atomic') {
56
+ errors.push(
57
+ `partitionLeases must be distributed-atomic; received ${capabilities.partitionLeases}`,
58
+ );
59
+ }
60
+ for (const method of [
61
+ 'claimPartitionLease',
62
+ 'renewPartitionLease',
63
+ 'releasePartitionLease',
64
+ ]) {
65
+ if (typeof store?.[method] !== 'function') {
66
+ errors.push(`shared store is missing ${method}()`);
67
+ }
68
+ }
69
+
70
+ return {
71
+ ok: errors.length === 0,
72
+ capabilities,
73
+ errors,
74
+ };
75
+ }
76
+
77
+ export function assertSharedStoreCapabilities(store) {
78
+ const result = validateSharedStoreCapabilities(store);
79
+ if (result.ok) return result.capabilities;
80
+
81
+ const error = new Error(
82
+ 'Event Intelligence shared-store requirements failed: ' +
83
+ result.errors.join('; '),
84
+ );
85
+ error.code = 'EVENT_INTELLIGENCE_SHARED_STORE_REQUIRED';
86
+ error.details = result;
87
+ throw error;
88
+ }
@@ -0,0 +1,127 @@
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ describeStoreCapabilities,
4
+ validateSharedStoreCapabilities,
5
+ } from './store-capabilities.mjs';
6
+
7
+ export async function runStoreConformance({
8
+ createStore,
9
+ expectShared = false,
10
+ } = {}) {
11
+ if (typeof createStore !== 'function') {
12
+ throw new Error('runStoreConformance requires createStore()');
13
+ }
14
+
15
+ const store = await createStore();
16
+ if (typeof store.init === 'function') await store.init();
17
+
18
+ const report = {
19
+ schema: 'event-intelligence.store-conformance.v1',
20
+ capabilities: describeStoreCapabilities(store),
21
+ checks: [],
22
+ };
23
+
24
+ const check = async (name, fn) => {
25
+ await fn();
26
+ report.checks.push({ name, ok: true });
27
+ };
28
+
29
+ await check('trigger-state round trip', async () => {
30
+ await store.putTrigger({
31
+ protocolVersion: '0.1.0',
32
+ schemaVersion: 'trigger.v0.1',
33
+ triggerId: 'store-conf-trigger',
34
+ version: '1',
35
+ conditionOnly: true,
36
+ clauses: [{
37
+ id: 'event',
38
+ event: 'store.conformance.event',
39
+ arguments: {},
40
+ where: [],
41
+ }],
42
+ expression: { kind: 'anyOf', refs: ['event'] },
43
+ temporal: [],
44
+ withinMs: 60000,
45
+ lifecycle: {
46
+ oneShot: false,
47
+ cooldownMs: 0,
48
+ completeOnGoal: false,
49
+ },
50
+ });
51
+ await store.setTriggerState(
52
+ 'store-conf-trigger',
53
+ '1',
54
+ 'paused',
55
+ { type: 'system', principal_id: 'conformance' },
56
+ );
57
+ assert.equal(
58
+ (await store.getTriggerState('store-conf-trigger', '1')).status,
59
+ 'paused',
60
+ );
61
+ });
62
+
63
+ await check('wake claim exclusion', async () => {
64
+ await store.ensureWakeDelivery({
65
+ wakeId: 'store-conf-wake',
66
+ sourceType: 'event',
67
+ sourceId: 'source-1',
68
+ runtime: 'conformance',
69
+ now: '2026-09-30T00:00:00.000Z',
70
+ });
71
+ const [a, b] = await Promise.all([
72
+ store.claimWakeDelivery('store-conf-wake', {
73
+ workerId: 'a',
74
+ now: '2026-09-30T00:00:00.000Z',
75
+ leaseMs: 30000,
76
+ }),
77
+ store.claimWakeDelivery('store-conf-wake', {
78
+ workerId: 'b',
79
+ now: '2026-09-30T00:00:00.000Z',
80
+ leaseMs: 30000,
81
+ }),
82
+ ]);
83
+ assert.equal([a, b].filter(Boolean).length, 1);
84
+ });
85
+
86
+ if (
87
+ typeof store.claimPartitionLease === 'function' &&
88
+ typeof store.releasePartitionLease === 'function'
89
+ ) {
90
+ await check('partition lease exclusion', async () => {
91
+ const [a, b] = await Promise.all([
92
+ store.claimPartitionLease('partition-1', {
93
+ workerId: 'a',
94
+ now: '2026-09-30T00:00:00.000Z',
95
+ leaseMs: 30000,
96
+ }),
97
+ store.claimPartitionLease('partition-1', {
98
+ workerId: 'b',
99
+ now: '2026-09-30T00:00:00.000Z',
100
+ leaseMs: 30000,
101
+ }),
102
+ ]);
103
+ assert.equal([a, b].filter(Boolean).length, 1);
104
+ const owner = a ? 'a' : 'b';
105
+ await store.releasePartitionLease('partition-1', {
106
+ workerId: owner,
107
+ });
108
+ });
109
+ }
110
+
111
+ if (typeof store.compactMutableState === 'function') {
112
+ await check('mutable-state compaction', async () => {
113
+ const result = await store.compactMutableState();
114
+ assert.equal(result?.ok, true);
115
+ });
116
+ }
117
+
118
+ const shared = validateSharedStoreCapabilities(store);
119
+ if (expectShared) {
120
+ assert.equal(shared.ok, true, shared.errors.join('; '));
121
+ }
122
+ report.sharedReady = shared.ok;
123
+ report.sharedErrors = shared.errors;
124
+
125
+ if (typeof store.close === 'function') await store.close();
126
+ return report;
127
+ }
@@ -19,14 +19,14 @@ export class TemporalDeadlineScheduler {
19
19
 
20
20
  try {
21
21
  const now = this.now();
22
- const due = this.store.listDueTemporalDeadlines(now.toISOString());
22
+ const due = await this.store.listDueTemporalDeadlines(now.toISOString());
23
23
  const outcomes = [];
24
24
 
25
25
  for (const deadline of due) {
26
- const latest = this.store.getTemporalDeadline(deadline.deadlineId);
26
+ const latest = await this.store.getTemporalDeadline(deadline.deadlineId);
27
27
  if (!latest || latest.status !== 'pending') continue;
28
28
 
29
- const match = this.store.listTriggerMatches(deadline.triggerId)
29
+ const match = (await this.store.listTriggerMatches(deadline.triggerId))
30
30
  .filter((record) => record.matchId === deadline.matchId)
31
31
  .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0];
32
32