@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.
- package/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- 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
|
+
}
|