@unrdf/kgc-runtime 26.4.2

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 (70) hide show
  1. package/IMPLEMENTATION_SUMMARY.json +150 -0
  2. package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
  3. package/README.md +98 -0
  4. package/TRANSACTION_IMPLEMENTATION.json +119 -0
  5. package/capability-map.md +93 -0
  6. package/docs/api-stability.md +269 -0
  7. package/docs/extensions/plugin-development.md +382 -0
  8. package/package.json +40 -0
  9. package/plugins/registry.json +35 -0
  10. package/src/admission-gate.mjs +414 -0
  11. package/src/api-version.mjs +373 -0
  12. package/src/atomic-admission.mjs +310 -0
  13. package/src/bounds.mjs +289 -0
  14. package/src/bulkhead-manager.mjs +280 -0
  15. package/src/capsule.mjs +524 -0
  16. package/src/crdt.mjs +361 -0
  17. package/src/enhanced-bounds.mjs +614 -0
  18. package/src/executor.mjs +73 -0
  19. package/src/freeze-restore.mjs +521 -0
  20. package/src/index.mjs +62 -0
  21. package/src/materialized-views.mjs +371 -0
  22. package/src/merge.mjs +472 -0
  23. package/src/plugin-isolation.mjs +392 -0
  24. package/src/plugin-manager.mjs +441 -0
  25. package/src/projections-api.mjs +336 -0
  26. package/src/projections-cli.mjs +238 -0
  27. package/src/projections-docs.mjs +300 -0
  28. package/src/projections-ide.mjs +278 -0
  29. package/src/receipt.mjs +340 -0
  30. package/src/rollback.mjs +258 -0
  31. package/src/saga-orchestrator.mjs +355 -0
  32. package/src/schemas.mjs +1330 -0
  33. package/src/storage-optimization.mjs +359 -0
  34. package/src/tool-registry.mjs +272 -0
  35. package/src/transaction.mjs +466 -0
  36. package/src/validators.mjs +485 -0
  37. package/src/work-item.mjs +449 -0
  38. package/templates/plugin-template/README.md +58 -0
  39. package/templates/plugin-template/index.mjs +162 -0
  40. package/templates/plugin-template/plugin.json +19 -0
  41. package/test/admission-gate.test.mjs +583 -0
  42. package/test/api-version.test.mjs +74 -0
  43. package/test/atomic-admission.test.mjs +155 -0
  44. package/test/bounds.test.mjs +341 -0
  45. package/test/bulkhead-manager.test.mjs +236 -0
  46. package/test/capsule.test.mjs +625 -0
  47. package/test/crdt.test.mjs +215 -0
  48. package/test/enhanced-bounds.test.mjs +487 -0
  49. package/test/freeze-restore.test.mjs +472 -0
  50. package/test/materialized-views.test.mjs +243 -0
  51. package/test/merge.test.mjs +665 -0
  52. package/test/plugin-isolation.test.mjs +109 -0
  53. package/test/plugin-manager.test.mjs +208 -0
  54. package/test/projections-api.test.mjs +293 -0
  55. package/test/projections-cli.test.mjs +204 -0
  56. package/test/projections-docs.test.mjs +173 -0
  57. package/test/projections-ide.test.mjs +230 -0
  58. package/test/receipt.test.mjs +295 -0
  59. package/test/rollback.test.mjs +132 -0
  60. package/test/saga-orchestrator.test.mjs +279 -0
  61. package/test/schemas.test.mjs +716 -0
  62. package/test/storage-optimization.test.mjs +503 -0
  63. package/test/tool-registry.test.mjs +341 -0
  64. package/test/transaction.test.mjs +189 -0
  65. package/test/validators.test.mjs +463 -0
  66. package/test/work-item.test.mjs +548 -0
  67. package/test/work-item.test.mjs.bak +548 -0
  68. package/var/kgc/test-atomic-log.json +519 -0
  69. package/var/kgc/test-cascading-log.json +145 -0
  70. package/vitest.config.mjs +18 -0
@@ -0,0 +1,441 @@
1
+ /**
2
+ * @file Plugin Manager - Lifecycle management for KGC plugins
3
+ * @module @unrdf/kgc-runtime/plugin-manager
4
+ * @description Manages plugin lifecycle with state machine: REGISTERED → LOADED → EXECUTING → UNLOADED
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import { PluginManifestSchema, PluginStateSchema } from './schemas.mjs';
9
+
10
+ /**
11
+ * Plugin lifecycle states
12
+ */
13
+ export const PLUGIN_STATES = {
14
+ REGISTERED: 'registered',
15
+ LOADED: 'loaded',
16
+ EXECUTING: 'executing',
17
+ UNLOADED: 'unloaded',
18
+ FAILED: 'failed',
19
+ };
20
+
21
+ /**
22
+ * Valid state transitions for plugin lifecycle
23
+ */
24
+ const VALID_TRANSITIONS = {
25
+ [PLUGIN_STATES.REGISTERED]: [PLUGIN_STATES.LOADED, PLUGIN_STATES.FAILED],
26
+ [PLUGIN_STATES.LOADED]: [PLUGIN_STATES.EXECUTING, PLUGIN_STATES.UNLOADED, PLUGIN_STATES.FAILED],
27
+ [PLUGIN_STATES.EXECUTING]: [PLUGIN_STATES.LOADED, PLUGIN_STATES.UNLOADED, PLUGIN_STATES.FAILED],
28
+ [PLUGIN_STATES.UNLOADED]: [PLUGIN_STATES.LOADED],
29
+ [PLUGIN_STATES.FAILED]: [PLUGIN_STATES.UNLOADED],
30
+ };
31
+
32
+ /**
33
+ * Plugin Manager - Manages plugin lifecycle with deterministic state machine
34
+ *
35
+ * @example
36
+ * import { PluginManager } from '@unrdf/kgc-runtime/plugin-manager';
37
+ * const manager = new PluginManager();
38
+ * await manager.registerPlugin({ name: 'my-plugin', version: '1.0.0', ... });
39
+ * await manager.loadPlugin('my-plugin');
40
+ * await manager.activatePlugin('my-plugin');
41
+ */
42
+ export class PluginManager {
43
+ /**
44
+ * Create new plugin manager
45
+ * @param {Object} options - Configuration options
46
+ * @param {Object} options.isolation - Isolation settings
47
+ * @param {boolean} options.parallelLoading - Enable parallel loading (default: true)
48
+ */
49
+ constructor(options = {}) {
50
+ /** @type {Map<string, Object>} */
51
+ this.plugins = new Map();
52
+
53
+ /** @type {Map<string, string>} Plugin state map */
54
+ this.states = new Map();
55
+
56
+ /** @type {Array<Object>} Audit log for all operations */
57
+ this.auditLog = [];
58
+
59
+ /** @type {number} State transition counter */
60
+ this.transitionCount = 0;
61
+
62
+ this.options = {
63
+ parallelLoading: options.parallelLoading ?? true,
64
+ isolation: options.isolation || {},
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Register a plugin
70
+ *
71
+ * @param {Object} manifest - Plugin manifest
72
+ * @returns {Promise<string>} Plugin ID
73
+ * @throws {Error} If manifest is invalid
74
+ *
75
+ * @example
76
+ * const id = await manager.registerPlugin({
77
+ * name: 'custom-receipt',
78
+ * version: '1.0.0',
79
+ * description: 'Custom receipt types',
80
+ * entryPoint: './plugin.mjs',
81
+ * capabilities: ['custom-receipt'],
82
+ * api_version: '5.0.1'
83
+ * });
84
+ */
85
+ async registerPlugin(manifest) {
86
+ // Validate manifest
87
+ const validated = PluginManifestSchema.parse(manifest);
88
+
89
+ const pluginId = `${validated.name}@${validated.version}`;
90
+
91
+ // Check if already registered
92
+ if (this.plugins.has(pluginId)) {
93
+ throw new Error(`Plugin already registered: ${pluginId}`);
94
+ }
95
+
96
+ // Store plugin
97
+ this.plugins.set(pluginId, {
98
+ ...validated,
99
+ id: pluginId,
100
+ registeredAt: Date.now(),
101
+ loadedAt: null,
102
+ instance: null,
103
+ });
104
+
105
+ // Set initial state
106
+ await this._transitionState(pluginId, PLUGIN_STATES.REGISTERED);
107
+
108
+ // Log registration
109
+ await this._auditLog('register', pluginId, { manifest: validated });
110
+
111
+ return pluginId;
112
+ }
113
+
114
+ /**
115
+ * Load a plugin (initialize but don't activate)
116
+ *
117
+ * @param {string} pluginId - Plugin ID (name@version)
118
+ * @returns {Promise<void>}
119
+ * @throws {Error} If plugin not found or load fails
120
+ *
121
+ * @example
122
+ * await manager.loadPlugin('custom-receipt@1.0.0');
123
+ */
124
+ async loadPlugin(pluginId) {
125
+ const plugin = this.plugins.get(pluginId);
126
+
127
+ if (!plugin) {
128
+ throw new Error(`Plugin not found: ${pluginId}`);
129
+ }
130
+
131
+ // Verify state allows loading
132
+ const currentState = this.states.get(pluginId);
133
+ if (!this._canTransition(currentState, PLUGIN_STATES.LOADED)) {
134
+ throw new Error(`Cannot load plugin in state: ${currentState}`);
135
+ }
136
+
137
+ try {
138
+ // Transition to loaded state
139
+ await this._transitionState(pluginId, PLUGIN_STATES.LOADED);
140
+
141
+ // Update metadata
142
+ plugin.loadedAt = Date.now();
143
+
144
+ // Log load
145
+ await this._auditLog('load', pluginId, { success: true });
146
+ } catch (error) {
147
+ await this._transitionState(pluginId, PLUGIN_STATES.FAILED);
148
+ await this._auditLog('load', pluginId, { success: false, error: error.message });
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Activate a plugin (start execution)
155
+ *
156
+ * @param {string} pluginId - Plugin ID
157
+ * @returns {Promise<void>}
158
+ * @throws {Error} If plugin not loaded or activation fails
159
+ *
160
+ * @example
161
+ * await manager.activatePlugin('custom-receipt@1.0.0');
162
+ */
163
+ async activatePlugin(pluginId) {
164
+ const plugin = this.plugins.get(pluginId);
165
+
166
+ if (!plugin) {
167
+ throw new Error(`Plugin not found: ${pluginId}`);
168
+ }
169
+
170
+ const currentState = this.states.get(pluginId);
171
+ if (!this._canTransition(currentState, PLUGIN_STATES.EXECUTING)) {
172
+ throw new Error(`Cannot activate plugin in state: ${currentState}`);
173
+ }
174
+
175
+ try {
176
+ // Transition to executing state
177
+ await this._transitionState(pluginId, PLUGIN_STATES.EXECUTING);
178
+
179
+ // Log activation
180
+ await this._auditLog('activate', pluginId, { success: true });
181
+ } catch (error) {
182
+ await this._transitionState(pluginId, PLUGIN_STATES.FAILED);
183
+ await this._auditLog('activate', pluginId, { success: false, error: error.message });
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Deactivate a plugin (stop execution but keep loaded)
190
+ *
191
+ * @param {string} pluginId - Plugin ID
192
+ * @returns {Promise<void>}
193
+ *
194
+ * @example
195
+ * await manager.deactivatePlugin('custom-receipt@1.0.0');
196
+ */
197
+ async deactivatePlugin(pluginId) {
198
+ const plugin = this.plugins.get(pluginId);
199
+
200
+ if (!plugin) {
201
+ throw new Error(`Plugin not found: ${pluginId}`);
202
+ }
203
+
204
+ const currentState = this.states.get(pluginId);
205
+ if (currentState !== PLUGIN_STATES.EXECUTING) {
206
+ throw new Error(`Plugin not executing: ${currentState}`);
207
+ }
208
+
209
+ // Transition back to loaded
210
+ await this._transitionState(pluginId, PLUGIN_STATES.LOADED);
211
+
212
+ // Log deactivation
213
+ await this._auditLog('deactivate', pluginId, { success: true });
214
+ }
215
+
216
+ /**
217
+ * Unload a plugin (cleanup and remove from memory)
218
+ *
219
+ * @param {string} pluginId - Plugin ID
220
+ * @returns {Promise<void>}
221
+ *
222
+ * @example
223
+ * await manager.unloadPlugin('custom-receipt@1.0.0');
224
+ */
225
+ async unloadPlugin(pluginId) {
226
+ const plugin = this.plugins.get(pluginId);
227
+
228
+ if (!plugin) {
229
+ throw new Error(`Plugin not found: ${pluginId}`);
230
+ }
231
+
232
+ const currentState = this.states.get(pluginId);
233
+ if (!this._canTransition(currentState, PLUGIN_STATES.UNLOADED)) {
234
+ throw new Error(`Cannot unload plugin in state: ${currentState}`);
235
+ }
236
+
237
+ // Transition to unloaded
238
+ await this._transitionState(pluginId, PLUGIN_STATES.UNLOADED);
239
+
240
+ // Cleanup
241
+ plugin.instance = null;
242
+ plugin.loadedAt = null;
243
+
244
+ // Log unload
245
+ await this._auditLog('unload', pluginId, { success: true });
246
+ }
247
+
248
+ /**
249
+ * Uninstall a plugin (complete removal)
250
+ *
251
+ * @param {string} pluginId - Plugin ID
252
+ * @returns {Promise<void>}
253
+ *
254
+ * @example
255
+ * await manager.uninstallPlugin('custom-receipt@1.0.0');
256
+ */
257
+ async uninstallPlugin(pluginId) {
258
+ const plugin = this.plugins.get(pluginId);
259
+
260
+ if (!plugin) {
261
+ throw new Error(`Plugin not found: ${pluginId}`);
262
+ }
263
+
264
+ // Must be unloaded first
265
+ const currentState = this.states.get(pluginId);
266
+ if (currentState !== PLUGIN_STATES.UNLOADED && currentState !== PLUGIN_STATES.REGISTERED) {
267
+ throw new Error(`Must unload plugin before uninstall: ${currentState}`);
268
+ }
269
+
270
+ // Remove from registry
271
+ this.plugins.delete(pluginId);
272
+ this.states.delete(pluginId);
273
+
274
+ // Log uninstall
275
+ await this._auditLog('uninstall', pluginId, { success: true });
276
+ }
277
+
278
+ /**
279
+ * Get plugin state
280
+ *
281
+ * @param {string} pluginId - Plugin ID
282
+ * @returns {string|null} Current state or null if not found
283
+ */
284
+ getPluginState(pluginId) {
285
+ return this.states.get(pluginId) || null;
286
+ }
287
+
288
+ /**
289
+ * Get plugin manifest
290
+ *
291
+ * @param {string} pluginId - Plugin ID
292
+ * @returns {Object|null} Plugin manifest or null if not found
293
+ */
294
+ getPlugin(pluginId) {
295
+ const plugin = this.plugins.get(pluginId);
296
+ if (!plugin) return null;
297
+
298
+ return {
299
+ ...plugin,
300
+ state: this.states.get(pluginId),
301
+ };
302
+ }
303
+
304
+ /**
305
+ * List all plugins
306
+ *
307
+ * @returns {Array<Object>} Array of plugin manifests with states
308
+ */
309
+ listPlugins() {
310
+ return Array.from(this.plugins.entries()).map(([id, plugin]) => ({
311
+ ...plugin,
312
+ state: this.states.get(id),
313
+ }));
314
+ }
315
+
316
+ /**
317
+ * List plugins by state
318
+ *
319
+ * @param {string} state - Plugin state
320
+ * @returns {Array<Object>} Plugins in specified state
321
+ */
322
+ listPluginsByState(state) {
323
+ return this.listPlugins().filter(p => p.state === state);
324
+ }
325
+
326
+ /**
327
+ * Get audit log
328
+ *
329
+ * @param {Object} filters - Optional filters
330
+ * @param {string} filters.pluginId - Filter by plugin ID
331
+ * @param {string} filters.action - Filter by action
332
+ * @returns {Array<Object>} Filtered audit log entries
333
+ */
334
+ getAuditLog(filters = {}) {
335
+ let log = this.auditLog;
336
+
337
+ if (filters.pluginId) {
338
+ log = log.filter(entry => entry.pluginId === filters.pluginId);
339
+ }
340
+
341
+ if (filters.action) {
342
+ log = log.filter(entry => entry.action === filters.action);
343
+ }
344
+
345
+ return log;
346
+ }
347
+
348
+ /**
349
+ * Get transition count
350
+ *
351
+ * @returns {number} Total number of state transitions
352
+ */
353
+ getTransitionCount() {
354
+ return this.transitionCount;
355
+ }
356
+
357
+ /**
358
+ * Load multiple plugins in parallel
359
+ *
360
+ * @param {string[]} pluginIds - Array of plugin IDs
361
+ * @returns {Promise<Object>} Results { success: [], failed: [] }
362
+ */
363
+ async loadPluginsParallel(pluginIds) {
364
+ if (!this.options.parallelLoading) {
365
+ throw new Error('Parallel loading not enabled');
366
+ }
367
+
368
+ const results = await Promise.allSettled(
369
+ pluginIds.map(id => this.loadPlugin(id))
370
+ );
371
+
372
+ const success = [];
373
+ const failed = [];
374
+
375
+ results.forEach((result, index) => {
376
+ if (result.status === 'fulfilled') {
377
+ success.push(pluginIds[index]);
378
+ } else {
379
+ failed.push({
380
+ pluginId: pluginIds[index],
381
+ error: result.reason?.message || 'Unknown error',
382
+ });
383
+ }
384
+ });
385
+
386
+ return { success, failed };
387
+ }
388
+
389
+ // ===== Private Methods =====
390
+
391
+ /**
392
+ * Transition plugin to new state
393
+ * @private
394
+ */
395
+ async _transitionState(pluginId, newState) {
396
+ const currentState = this.states.get(pluginId);
397
+
398
+ // Validate transition
399
+ if (currentState && !this._canTransition(currentState, newState)) {
400
+ throw new Error(`Invalid transition: ${currentState} -> ${newState}`);
401
+ }
402
+
403
+ // Update state
404
+ this.states.set(pluginId, newState);
405
+ this.transitionCount++;
406
+ }
407
+
408
+ /**
409
+ * Check if state transition is valid
410
+ * @private
411
+ */
412
+ _canTransition(fromState, toState) {
413
+ if (!fromState) return true; // Initial registration
414
+ return VALID_TRANSITIONS[fromState]?.includes(toState) || false;
415
+ }
416
+
417
+ /**
418
+ * Add entry to audit log
419
+ * @private
420
+ */
421
+ async _auditLog(action, pluginId, details) {
422
+ const entry = {
423
+ timestamp: Date.now(),
424
+ action,
425
+ pluginId,
426
+ details,
427
+ };
428
+
429
+ this.auditLog.push(entry);
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Create a new plugin manager instance
435
+ *
436
+ * @param {Object} options - Configuration options
437
+ * @returns {PluginManager} New plugin manager
438
+ */
439
+ export function createPluginManager(options = {}) {
440
+ return new PluginManager(options);
441
+ }