@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,449 @@
1
+ /**
2
+ * KGC Runtime - Async Work Item System
3
+ * Provides work item management with deterministic scheduling and receipt logging
4
+ */
5
+
6
+ import { createStore, dataFactory } from '@unrdf/oxigraph';
7
+ import { z } from 'zod';
8
+
9
+ /**
10
+ * Work item state enumeration
11
+ */
12
+ export const WORK_ITEM_STATES = {
13
+ QUEUED: 'queued',
14
+ RUNNING: 'running',
15
+ SUCCEEDED: 'succeeded',
16
+ FAILED: 'failed',
17
+ DENIED: 'denied',
18
+ };
19
+
20
+ /**
21
+ * Terminal states (cannot transition from these)
22
+ */
23
+ const TERMINAL_STATES = new Set([
24
+ WORK_ITEM_STATES.SUCCEEDED,
25
+ WORK_ITEM_STATES.FAILED,
26
+ WORK_ITEM_STATES.DENIED,
27
+ ]);
28
+
29
+ /**
30
+ * Valid state transitions
31
+ */
32
+ const VALID_TRANSITIONS = {
33
+ [WORK_ITEM_STATES.QUEUED]: [
34
+ WORK_ITEM_STATES.RUNNING,
35
+ WORK_ITEM_STATES.DENIED,
36
+ ],
37
+ [WORK_ITEM_STATES.RUNNING]: [
38
+ WORK_ITEM_STATES.SUCCEEDED,
39
+ WORK_ITEM_STATES.FAILED,
40
+ ],
41
+ [WORK_ITEM_STATES.SUCCEEDED]: [],
42
+ [WORK_ITEM_STATES.FAILED]: [],
43
+ [WORK_ITEM_STATES.DENIED]: [],
44
+ };
45
+
46
+ /**
47
+ * Zod schema for work item bounds
48
+ */
49
+ const BoundsSchema = z.object({
50
+ timeout: z.number().optional(),
51
+ maxRetries: z.number().optional(),
52
+ priority: z.number().optional(),
53
+ metadata: z.any().optional(),
54
+ }).optional();
55
+
56
+ /**
57
+ * Generate nanosecond timestamp
58
+ * @returns {bigint} Current time in nanoseconds
59
+ */
60
+ function nowNs() {
61
+ const hrTime = process.hrtime.bigint();
62
+ return hrTime;
63
+ }
64
+
65
+ /**
66
+ * Generate unique work item ID
67
+ * @returns {string} Unique ID
68
+ */
69
+ function generateWorkItemId() {
70
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
71
+ return `work-item-${crypto.randomUUID()}`;
72
+ }
73
+ try {
74
+ const crypto = require('crypto');
75
+ return `work-item-${crypto.randomUUID()}`;
76
+ } catch {
77
+ return `work-item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * WorkItem Executor - Manages async work items with deterministic scheduling
83
+ *
84
+ * @example
85
+ * import { WorkItemExecutor } from './work-item.mjs';
86
+ * const executor = new WorkItemExecutor();
87
+ * const id = await executor.enqueueWorkItem('Process data');
88
+ * const item = await executor.pollWorkItem(id);
89
+ * console.assert(item.status === 'queued');
90
+ */
91
+ export class WorkItemExecutor {
92
+ /**
93
+ * Create new work item executor
94
+ * @param {Object} options - Configuration options
95
+ */
96
+ constructor(options = {}) {
97
+ this.store = createStore();
98
+ this.workItems = new Map(); // In-memory cache for fast access
99
+ this.stateTransitionCount = 0;
100
+
101
+ // RDF predicates for work items
102
+ this.predicates = {
103
+ GOAL: 'http://kgc.io/goal',
104
+ STATUS: 'http://kgc.io/status',
105
+ CREATED_NS: 'http://kgc.io/created_ns',
106
+ STARTED_NS: 'http://kgc.io/started_ns',
107
+ FINISHED_NS: 'http://kgc.io/finished_ns',
108
+ RECEIPT_LOG: 'http://kgc.io/receipt_log',
109
+ BOUNDS: 'http://kgc.io/bounds',
110
+ PRIORITY: 'http://kgc.io/priority',
111
+ };
112
+
113
+ // Named graph for work items
114
+ this.workItemsGraph = 'http://kgc.io/var/kgc/work-items';
115
+ }
116
+
117
+ /**
118
+ * Enqueue new work item
119
+ *
120
+ * @param {string} goal - Work item goal/description
121
+ * @param {Object} [bounds] - Execution bounds (timeout, retries, priority, etc.)
122
+ * @returns {Promise<string>} Work item ID
123
+ *
124
+ * @example
125
+ * const id = await executor.enqueueWorkItem('Calculate sum', { priority: 5 });
126
+ */
127
+ async enqueueWorkItem(goal, bounds = {}) {
128
+ // Validate bounds
129
+ BoundsSchema.parse(bounds);
130
+
131
+ const workItemId = generateWorkItemId();
132
+ const created_ns = nowNs();
133
+
134
+ const workItem = {
135
+ id: workItemId,
136
+ goal,
137
+ status: WORK_ITEM_STATES.QUEUED,
138
+ receipt_log: [],
139
+ created_ns: created_ns.toString(),
140
+ started_ns: null,
141
+ finished_ns: null,
142
+ bounds,
143
+ priority: bounds.priority || 0,
144
+ };
145
+
146
+ // Store in memory
147
+ this.workItems.set(workItemId, workItem);
148
+
149
+ // Store in RDF triple store
150
+ await this._persistWorkItem(workItem);
151
+
152
+ // Count initial state
153
+ this.stateTransitionCount++;
154
+
155
+ return workItemId;
156
+ }
157
+
158
+ /**
159
+ * Poll work item status
160
+ *
161
+ * @param {string} workItemId - Work item ID
162
+ * @returns {Promise<Object|null>} Work item or null if not found
163
+ *
164
+ * @example
165
+ * const item = await executor.pollWorkItem(id);
166
+ * console.log(item.status); // 'queued'
167
+ */
168
+ async pollWorkItem(workItemId) {
169
+ const workItem = this.workItems.get(workItemId);
170
+
171
+ if (!workItem) {
172
+ return null;
173
+ }
174
+
175
+ // Return deep copy to prevent external modifications
176
+ return JSON.parse(JSON.stringify(workItem));
177
+ }
178
+
179
+ /**
180
+ * Transition work item to new state
181
+ *
182
+ * @param {string} workItemId - Work item ID
183
+ * @param {string} newState - Target state
184
+ * @throws {Error} If transition is invalid
185
+ *
186
+ * @example
187
+ * await executor.transitionWorkItem(id, WORK_ITEM_STATES.RUNNING);
188
+ */
189
+ async transitionWorkItem(workItemId, newState) {
190
+ const workItem = this.workItems.get(workItemId);
191
+
192
+ if (!workItem) {
193
+ throw new Error(`Work item not found: ${workItemId}`);
194
+ }
195
+
196
+ const currentState = workItem.status;
197
+
198
+ // Check if current state is terminal
199
+ if (TERMINAL_STATES.has(currentState)) {
200
+ throw new Error(
201
+ `Cannot transition from terminal state: ${currentState}`
202
+ );
203
+ }
204
+
205
+ // Validate state transition
206
+ if (!VALID_TRANSITIONS[currentState]?.includes(newState)) {
207
+ throw new Error(
208
+ `Invalid state transition: ${currentState} -> ${newState}`
209
+ );
210
+ }
211
+
212
+ // Update state
213
+ workItem.status = newState;
214
+
215
+ // Update timestamps
216
+ if (newState === WORK_ITEM_STATES.RUNNING && !workItem.started_ns) {
217
+ workItem.started_ns = nowNs().toString();
218
+ }
219
+
220
+ if (TERMINAL_STATES.has(newState) && !workItem.finished_ns) {
221
+ workItem.finished_ns = nowNs().toString();
222
+ }
223
+
224
+ // Persist changes
225
+ await this._persistWorkItem(workItem);
226
+
227
+ // Increment transition counter
228
+ this.stateTransitionCount++;
229
+ }
230
+
231
+ /**
232
+ * Finalize work item with result and final receipt
233
+ *
234
+ * @param {string} workItemId - Work item ID
235
+ * @param {Object} result - Execution result
236
+ * @param {Object} receipt - Final receipt
237
+ * @returns {Promise<Object>} Updated work item
238
+ *
239
+ * @example
240
+ * const item = await executor.finalizeWorkItem(id, { value: 42 }, { done: true });
241
+ */
242
+ async finalizeWorkItem(workItemId, result, receipt) {
243
+ const workItem = this.workItems.get(workItemId);
244
+
245
+ if (!workItem) {
246
+ throw new Error(`Work item not found: ${workItemId}`);
247
+ }
248
+
249
+ // Add final receipt
250
+ await this.addReceipt(workItemId, receipt);
251
+
252
+ // Transition to succeeded (if currently running)
253
+ if (workItem.status === WORK_ITEM_STATES.RUNNING) {
254
+ await this.transitionWorkItem(workItemId, WORK_ITEM_STATES.SUCCEEDED);
255
+ }
256
+
257
+ // Store result
258
+ workItem.result = result;
259
+ await this._persistWorkItem(workItem);
260
+
261
+ return this.pollWorkItem(workItemId);
262
+ }
263
+
264
+ /**
265
+ * Add receipt to work item's append-only log
266
+ *
267
+ * @param {string} workItemId - Work item ID
268
+ * @param {Object} receipt - Receipt data
269
+ *
270
+ * @example
271
+ * await executor.addReceipt(id, { step: 'validation', status: 'ok' });
272
+ */
273
+ async addReceipt(workItemId, receipt) {
274
+ const workItem = this.workItems.get(workItemId);
275
+
276
+ if (!workItem) {
277
+ throw new Error(`Work item not found: ${workItemId}`);
278
+ }
279
+
280
+ // Deep copy receipt to ensure immutability
281
+ const receiptCopy = JSON.parse(JSON.stringify(receipt));
282
+
283
+ // Append to log
284
+ workItem.receipt_log.push(receiptCopy);
285
+
286
+ // Persist changes
287
+ await this._persistWorkItem(workItem);
288
+ }
289
+
290
+ /**
291
+ * Get deterministic execution order
292
+ * Returns pending/queued items ordered by priority (desc) then creation time (asc)
293
+ *
294
+ * @returns {string[]} Ordered array of work item IDs
295
+ *
296
+ * @example
297
+ * const order = executor.getExecutionOrder();
298
+ * console.log(order); // ['work-item-1', 'work-item-2', ...]
299
+ */
300
+ getExecutionOrder() {
301
+ const eligibleItems = Array.from(this.workItems.values())
302
+ .filter(item => item.status === WORK_ITEM_STATES.QUEUED);
303
+
304
+ // Sort by priority (descending) then by creation time (ascending)
305
+ eligibleItems.sort((a, b) => {
306
+ // Higher priority first
307
+ if (a.priority !== b.priority) {
308
+ return b.priority - a.priority;
309
+ }
310
+ // Earlier creation time first (FIFO for same priority)
311
+ return BigInt(a.created_ns) < BigInt(b.created_ns) ? -1 : 1;
312
+ });
313
+
314
+ return eligibleItems.map(item => item.id);
315
+ }
316
+
317
+ /**
318
+ * Get total state transition count
319
+ *
320
+ * @returns {number} Number of state transitions
321
+ */
322
+ getStateTransitionCount() {
323
+ return this.stateTransitionCount;
324
+ }
325
+
326
+ /**
327
+ * Query work items using SPARQL
328
+ *
329
+ * @param {string} sparql - SPARQL query
330
+ * @returns {Promise<Array>} Query results
331
+ */
332
+ async queryWorkItems(sparql) {
333
+ return this.store.query(sparql);
334
+ }
335
+
336
+ // ===== Private Methods =====
337
+
338
+ /**
339
+ * Persist work item to RDF triple store
340
+ * @private
341
+ */
342
+ async _persistWorkItem(workItem) {
343
+ const subject = dataFactory.namedNode(
344
+ `http://kgc.io/var/kgc/work-items/${workItem.id}`
345
+ );
346
+ const graph = dataFactory.namedNode(this.workItemsGraph);
347
+
348
+ // Remove existing quads for this work item
349
+ const existingQuads = [];
350
+ for (const quad of this.store.match(subject, null, null, graph)) {
351
+ existingQuads.push(quad);
352
+ }
353
+ for (const quad of existingQuads) {
354
+ this.store.delete(quad);
355
+ }
356
+
357
+ // Add updated quads
358
+ const quads = [
359
+ dataFactory.quad(
360
+ subject,
361
+ dataFactory.namedNode(this.predicates.GOAL),
362
+ dataFactory.literal(workItem.goal),
363
+ graph
364
+ ),
365
+ dataFactory.quad(
366
+ subject,
367
+ dataFactory.namedNode(this.predicates.STATUS),
368
+ dataFactory.literal(workItem.status),
369
+ graph
370
+ ),
371
+ dataFactory.quad(
372
+ subject,
373
+ dataFactory.namedNode(this.predicates.CREATED_NS),
374
+ dataFactory.literal(
375
+ workItem.created_ns,
376
+ dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')
377
+ ),
378
+ graph
379
+ ),
380
+ ];
381
+
382
+ if (workItem.started_ns) {
383
+ quads.push(
384
+ dataFactory.quad(
385
+ subject,
386
+ dataFactory.namedNode(this.predicates.STARTED_NS),
387
+ dataFactory.literal(
388
+ workItem.started_ns,
389
+ dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')
390
+ ),
391
+ graph
392
+ )
393
+ );
394
+ }
395
+
396
+ if (workItem.finished_ns) {
397
+ quads.push(
398
+ dataFactory.quad(
399
+ subject,
400
+ dataFactory.namedNode(this.predicates.FINISHED_NS),
401
+ dataFactory.literal(
402
+ workItem.finished_ns,
403
+ dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')
404
+ ),
405
+ graph
406
+ )
407
+ );
408
+ }
409
+
410
+ if (workItem.receipt_log.length > 0) {
411
+ quads.push(
412
+ dataFactory.quad(
413
+ subject,
414
+ dataFactory.namedNode(this.predicates.RECEIPT_LOG),
415
+ dataFactory.literal(JSON.stringify(workItem.receipt_log)),
416
+ graph
417
+ )
418
+ );
419
+ }
420
+
421
+ if (workItem.bounds) {
422
+ quads.push(
423
+ dataFactory.quad(
424
+ subject,
425
+ dataFactory.namedNode(this.predicates.BOUNDS),
426
+ dataFactory.literal(JSON.stringify(workItem.bounds)),
427
+ graph
428
+ )
429
+ );
430
+ }
431
+
432
+ quads.push(
433
+ dataFactory.quad(
434
+ subject,
435
+ dataFactory.namedNode(this.predicates.PRIORITY),
436
+ dataFactory.literal(
437
+ workItem.priority.toString(),
438
+ dataFactory.namedNode('http://www.w3.org/2001/XMLSchema#integer')
439
+ ),
440
+ graph
441
+ )
442
+ );
443
+
444
+ // Add all quads to store
445
+ for (const quad of quads) {
446
+ this.store.add(quad);
447
+ }
448
+ }
449
+ }
@@ -0,0 +1,58 @@
1
+ # Example KGC Plugin Template
2
+
3
+ This is a complete template for creating KGC Runtime plugins.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ plugin-template/
9
+ ├── plugin.json # Plugin manifest
10
+ ├── index.mjs # Plugin implementation
11
+ ├── README.md # This file
12
+ └── test.mjs # Plugin tests
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ 1. **Copy Template**
18
+ ```bash
19
+ cp -r templates/plugin-template my-plugin
20
+ cd my-plugin
21
+ ```
22
+
23
+ 2. **Update Manifest** (`plugin.json`)
24
+ - Change `name` to your plugin name
25
+ - Update `version`, `author`, `description`
26
+ - Declare required `capabilities`
27
+
28
+ 3. **Implement Plugin** (`index.mjs`)
29
+ - Modify `initialize()` for setup
30
+ - Add your custom methods
31
+ - Update `cleanup()` for teardown
32
+
33
+ 4. **Test Plugin** (`test.mjs`)
34
+ - Write comprehensive tests
35
+ - Test all capabilities
36
+ - Verify error handling
37
+
38
+ 5. **Register and Use**
39
+ ```javascript
40
+ import { PluginManager } from '@unrdf/kgc-runtime/plugin-manager';
41
+
42
+ const manager = new PluginManager();
43
+ await manager.registerPlugin(manifest);
44
+ await manager.loadPlugin('my-plugin@1.0.0');
45
+ ```
46
+
47
+ ## Best Practices
48
+
49
+ 1. **Validate All Inputs** - Use Zod schemas
50
+ 2. **Handle Errors** - Return error receipts
51
+ 3. **Declare Minimum Capabilities** - Only what you need
52
+ 4. **Document Your API** - JSDoc comments
53
+ 5. **Write Tests** - Aim for 100% coverage
54
+
55
+ ## See Also
56
+
57
+ - [Plugin Development Guide](../../docs/extensions/plugin-development.md)
58
+ - [API Stability Policy](../../docs/api-stability.md)
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Example KGC Runtime Plugin
3
+ *
4
+ * This is a template for creating custom KGC plugins.
5
+ * Copy this template and modify to create your own plugin.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import { PluginReceiptSchema } from '@unrdf/kgc-runtime/schemas';
10
+
11
+ /**
12
+ * Input validation schema
13
+ */
14
+ const InputSchema = z.object({
15
+ operation: z.string().min(1).max(100),
16
+ data: z.record(z.any()).optional(),
17
+ });
18
+
19
+ /**
20
+ * Plugin entry point
21
+ *
22
+ * @param {Object} runtime - KGC Runtime API (whitelisted methods only)
23
+ * @returns {Object} Plugin interface
24
+ */
25
+ export default function examplePlugin(runtime) {
26
+ // Private state (encapsulated)
27
+ let initialized = false;
28
+ let callCount = 0;
29
+
30
+ return {
31
+ name: 'example-plugin',
32
+ version: '1.0.0',
33
+
34
+ /**
35
+ * Initialize plugin
36
+ * Called when plugin transitions to LOADED state
37
+ */
38
+ async initialize() {
39
+ console.log('[example-plugin] Initializing...');
40
+
41
+ // Perform any setup here
42
+ // - Validate runtime API
43
+ // - Initialize state
44
+ // - Connect to resources (within capability limits)
45
+
46
+ initialized = true;
47
+ console.log('[example-plugin] Initialized successfully');
48
+ },
49
+
50
+ /**
51
+ * Generate custom receipt
52
+ *
53
+ * @param {string} operation - Operation name
54
+ * @param {Object} inputs - Operation inputs
55
+ * @param {Object} outputs - Operation outputs
56
+ * @returns {Promise<Object>} Custom receipt
57
+ */
58
+ async generateCustomReceipt(operation, inputs = {}, outputs = {}) {
59
+ if (!initialized) {
60
+ throw new Error('Plugin not initialized');
61
+ }
62
+
63
+ // Validate inputs
64
+ InputSchema.parse({ operation, data: inputs });
65
+
66
+ // Use runtime API to generate base receipt
67
+ const baseReceipt = await runtime.generateReceipt(
68
+ operation,
69
+ inputs,
70
+ outputs
71
+ );
72
+
73
+ callCount++;
74
+
75
+ // Extend with custom metadata
76
+ const customReceipt = {
77
+ ...baseReceipt,
78
+ pluginMetadata: {
79
+ pluginName: 'example-plugin',
80
+ pluginVersion: '1.0.0',
81
+ receiptType: 'custom-example',
82
+ customFields: {
83
+ callCount,
84
+ timestamp: Date.now(),
85
+ processingNotes: 'Processed by example plugin',
86
+ },
87
+ },
88
+ };
89
+
90
+ // Validate against plugin receipt schema
91
+ return PluginReceiptSchema.parse(customReceipt);
92
+ },
93
+
94
+ /**
95
+ * Validate custom receipt
96
+ *
97
+ * @param {Object} receipt - Receipt to validate
98
+ * @returns {Promise<Object>} Validation result
99
+ */
100
+ async validateCustomReceipt(receipt) {
101
+ if (!initialized) {
102
+ throw new Error('Plugin not initialized');
103
+ }
104
+
105
+ try {
106
+ // Validate schema
107
+ const validated = PluginReceiptSchema.parse(receipt);
108
+
109
+ // Custom validation logic
110
+ if (validated.pluginMetadata?.pluginName !== 'example-plugin') {
111
+ return {
112
+ valid: false,
113
+ errors: ['Receipt not from example-plugin'],
114
+ };
115
+ }
116
+
117
+ // Use runtime validation
118
+ const baseValidation = await runtime.validateReceipt(receipt);
119
+
120
+ return {
121
+ valid: baseValidation.success,
122
+ errors: baseValidation.errors || [],
123
+ metadata: validated.pluginMetadata,
124
+ };
125
+ } catch (error) {
126
+ return {
127
+ valid: false,
128
+ errors: [error.message],
129
+ };
130
+ }
131
+ },
132
+
133
+ /**
134
+ * Get plugin statistics
135
+ *
136
+ * @returns {Object} Plugin stats
137
+ */
138
+ getStats() {
139
+ return {
140
+ initialized,
141
+ callCount,
142
+ version: '1.0.0',
143
+ };
144
+ },
145
+
146
+ /**
147
+ * Cleanup on unload
148
+ * Called when plugin transitions to UNLOADED state
149
+ */
150
+ async cleanup() {
151
+ console.log('[example-plugin] Cleaning up...');
152
+
153
+ // Perform cleanup:
154
+ // - Close connections
155
+ // - Release resources
156
+ // - Save state if needed
157
+
158
+ initialized = false;
159
+ console.log('[example-plugin] Cleanup complete');
160
+ },
161
+ };
162
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "example-plugin",
3
+ "version": "1.0.0",
4
+ "description": "Example KGC Runtime plugin template",
5
+ "entryPoint": "./index.mjs",
6
+ "capabilities": [
7
+ "receipt:generate",
8
+ "receipt:validate",
9
+ "custom:example"
10
+ ],
11
+ "api_version": "5.0.1",
12
+ "author": "Your Name",
13
+ "license": "MIT",
14
+ "repository": "https://github.com/your-org/example-plugin",
15
+ "metadata": {
16
+ "tags": ["example", "template"],
17
+ "category": "receipt"
18
+ }
19
+ }