@unrdf/hooks 26.4.2 → 26.4.4
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/LICENSE +24 -0
- package/README.md +562 -53
- package/examples/atomvm-fibo-hooks-demo.mjs +323 -0
- package/examples/delta-monitoring-example.mjs +213 -0
- package/examples/fibo-jtbd-governance.mjs +388 -0
- package/examples/hook-chains/node_modules/.bin/jiti +21 -0
- package/examples/hook-chains/node_modules/.bin/msw +21 -0
- package/examples/hook-chains/node_modules/.bin/terser +21 -0
- package/examples/hook-chains/node_modules/.bin/tsc +21 -0
- package/examples/hook-chains/node_modules/.bin/tsserver +21 -0
- package/examples/hook-chains/node_modules/.bin/tsx +21 -0
- package/examples/hook-chains/node_modules/.bin/vite +21 -0
- package/examples/hook-chains/node_modules/.bin/vitest +2 -2
- package/examples/hook-chains/node_modules/.bin/yaml +21 -0
- package/examples/hook-chains/package.json +2 -2
- package/examples/hook-chains/unrdf-hooks-example-chains-5.0.0.tgz +0 -0
- package/examples/hooks-marketplace.mjs +261 -0
- package/examples/n3-reasoning-example.mjs +279 -0
- package/examples/policy-hooks/node_modules/.bin/jiti +21 -0
- package/examples/policy-hooks/node_modules/.bin/msw +21 -0
- package/examples/policy-hooks/node_modules/.bin/terser +21 -0
- package/examples/policy-hooks/node_modules/.bin/tsc +21 -0
- package/examples/policy-hooks/node_modules/.bin/tsserver +21 -0
- package/examples/policy-hooks/node_modules/.bin/tsx +21 -0
- package/examples/policy-hooks/node_modules/.bin/vite +21 -0
- package/examples/policy-hooks/node_modules/.bin/vitest +2 -2
- package/examples/policy-hooks/node_modules/.bin/yaml +21 -0
- package/examples/policy-hooks/package.json +2 -2
- package/examples/policy-hooks/unrdf-hooks-example-policy-5.0.0.tgz +0 -0
- package/examples/shacl-repair-example.mjs +191 -0
- package/examples/window-condition-example.mjs +285 -0
- package/package.json +6 -3
- package/src/atomvm.mjs +9 -0
- package/src/define.mjs +114 -0
- package/src/executor.mjs +23 -0
- package/src/hooks/atomvm-bridge.mjs +332 -0
- package/src/hooks/builtin-hooks.mjs +13 -7
- package/src/hooks/condition-evaluator.mjs +684 -77
- package/src/hooks/define-hook.mjs +23 -21
- package/src/hooks/effect-executor.mjs +630 -0
- package/src/hooks/effect-sandbox.mjs +19 -9
- package/src/hooks/file-resolver.mjs +155 -1
- package/src/hooks/hook-chain-compiler.mjs +11 -1
- package/src/hooks/hook-executor.mjs +98 -73
- package/src/hooks/knowledge-hook-engine.mjs +133 -7
- package/src/hooks/ontology-learner.mjs +190 -0
- package/src/hooks/policy-pack.mjs +7 -1
- package/src/hooks/query-optimizer.mjs +1 -5
- package/src/hooks/query.mjs +3 -3
- package/src/hooks/schemas.mjs +55 -5
- package/src/hooks/security/error-sanitizer.mjs +46 -24
- package/src/hooks/self-play-autonomics.mjs +423 -0
- package/src/hooks/telemetry.mjs +32 -9
- package/src/hooks/validate.mjs +100 -33
- package/src/index.mjs +2 -0
- package/src/lib/admit-hook.mjs +615 -0
- package/src/policy-compiler.mjs +23 -13
- package/dist/index.d.mts +0 -1738
- package/dist/index.d.ts +0 -1738
- package/dist/index.mjs +0 -1738
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Knowledge Hooks Self-Play Autonomics
|
|
3
|
+
* @module @unrdf/hooks/self-play-autonomics
|
|
4
|
+
* @description Autonomous feedback loops where hook conditions trigger effects which mutate the RDF store
|
|
5
|
+
*
|
|
6
|
+
* Enables closed-loop knowledge engineering:
|
|
7
|
+
* 1. Evaluate conditions (SPARQL ASK/SELECT, SHACL, delta, threshold, ...)
|
|
8
|
+
* 2. Execute satisfied effects (SPARQL CONSTRUCT mutations)
|
|
9
|
+
* 3. Store changes produce receipt (BLAKE3 hash, linked chain)
|
|
10
|
+
* 4. Re-evaluate conditions until goal or no-op
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomUUID } from 'crypto';
|
|
14
|
+
import { evaluateCondition } from './condition-evaluator.mjs';
|
|
15
|
+
import { KnowledgeHookEngine } from './knowledge-hook-engine.mjs';
|
|
16
|
+
import { ask, select, construct } from './query.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a self-play toolRegistry from knowledge hooks
|
|
20
|
+
* Returns structured ChainResult objects instead of CLI strings
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} store - RDF store (N3 or Oxigraph)
|
|
23
|
+
* @param {Array} hooks - Array of KnowledgeHook definitions
|
|
24
|
+
* @returns {Object} toolRegistry { toolName: { handler, schema } }
|
|
25
|
+
*/
|
|
26
|
+
export function buildHooksToolRegistry(store, _hooks = []) {
|
|
27
|
+
const engine = new KnowledgeHookEngine(store);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
hooks_evaluate_conditions: {
|
|
31
|
+
handler: async ({ store: evalStore, hooks: evalHooks }) => {
|
|
32
|
+
// Evaluate all conditions and collect results
|
|
33
|
+
const conditionResults = [];
|
|
34
|
+
const satisfied = [];
|
|
35
|
+
|
|
36
|
+
for (const hook of evalHooks) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await evaluateCondition(hook.condition, evalStore);
|
|
39
|
+
conditionResults.push({
|
|
40
|
+
hookName: hook.name,
|
|
41
|
+
condition: hook.condition,
|
|
42
|
+
satisfied: result,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (result) {
|
|
46
|
+
satisfied.push(hook);
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
conditionResults.push({
|
|
50
|
+
hookName: hook.name,
|
|
51
|
+
condition: hook.condition,
|
|
52
|
+
satisfied: false,
|
|
53
|
+
error: err.message,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const successRate = evalHooks.length > 0 ? satisfied.length / evalHooks.length : 0;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
conditionResults,
|
|
62
|
+
satisfied,
|
|
63
|
+
successRate,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
hooks_execute_effects: {
|
|
69
|
+
handler: async ({ store: _execStore, hooks: execHooks, delta: _delta }) => {
|
|
70
|
+
// Execute hooks with receipt chaining
|
|
71
|
+
const context = {
|
|
72
|
+
nodeId: 'self-play-autonomics',
|
|
73
|
+
t_ns: BigInt(Date.now() * 1000000),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const result = await engine.execute(context, execHooks);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
executionResults: execHooks.map(h => ({
|
|
81
|
+
hookName: h.name,
|
|
82
|
+
status: 'executed',
|
|
83
|
+
})),
|
|
84
|
+
receipt: {
|
|
85
|
+
receiptHash: result.receipt.receiptHash,
|
|
86
|
+
input_hash: result.receipt.input_hash,
|
|
87
|
+
output_hash: result.receipt.output_hash,
|
|
88
|
+
previousReceiptHash: result.receipt.previousReceiptHash || null,
|
|
89
|
+
hooksExecuted: result.successful + result.failed,
|
|
90
|
+
successful: result.successful,
|
|
91
|
+
failed: result.failed,
|
|
92
|
+
delta: result.receipt.delta,
|
|
93
|
+
timestamp: result.receipt.timestamp,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return {
|
|
98
|
+
executionResults: execHooks.map(h => ({
|
|
99
|
+
hookName: h.name,
|
|
100
|
+
status: 'failed',
|
|
101
|
+
error: err.message,
|
|
102
|
+
})),
|
|
103
|
+
receipt: null,
|
|
104
|
+
error: err.message,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
hooks_query: {
|
|
111
|
+
handler: async ({ store: queryStore, query: sparqlQuery, kind = 'sparql-ask' }) => {
|
|
112
|
+
// Execute SPARQL query based on kind
|
|
113
|
+
try {
|
|
114
|
+
let result;
|
|
115
|
+
|
|
116
|
+
switch (kind.toLowerCase()) {
|
|
117
|
+
case 'sparql-ask':
|
|
118
|
+
case 'ask':
|
|
119
|
+
result = await ask(queryStore, sparqlQuery);
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case 'sparql-select':
|
|
123
|
+
case 'select':
|
|
124
|
+
result = await select(queryStore, sparqlQuery);
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case 'sparql-construct':
|
|
128
|
+
case 'construct':
|
|
129
|
+
result = await construct(queryStore, sparqlQuery);
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
default:
|
|
133
|
+
// Default to ASK
|
|
134
|
+
result = await ask(queryStore, sparqlQuery);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
result,
|
|
139
|
+
kind,
|
|
140
|
+
success: true,
|
|
141
|
+
};
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return {
|
|
144
|
+
result: null,
|
|
145
|
+
kind,
|
|
146
|
+
success: false,
|
|
147
|
+
error: err.message,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create a decision policy that reads hook results and branches intelligently
|
|
157
|
+
*
|
|
158
|
+
* @param {Function} goalCondition - async (store, previousResult) => boolean
|
|
159
|
+
* @returns {Function} decisionFn(episode, previousResult) => { toolName, input } | null
|
|
160
|
+
*/
|
|
161
|
+
export function createHooksAwarePolicy(goalCondition = async () => false) {
|
|
162
|
+
return async (episode, previousResult) => {
|
|
163
|
+
const stepCount = episode.steps.length;
|
|
164
|
+
const { store, hooks } = episode.context;
|
|
165
|
+
|
|
166
|
+
// Step 0: always evaluate conditions first
|
|
167
|
+
if (stepCount === 0) {
|
|
168
|
+
return {
|
|
169
|
+
toolName: 'hooks_evaluate_conditions',
|
|
170
|
+
input: { store, hooks },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Step 1: if conditions satisfied, execute effects; else terminate
|
|
175
|
+
if (stepCount === 1) {
|
|
176
|
+
const { satisfied } = previousResult || {};
|
|
177
|
+
if (!satisfied || satisfied.length === 0) {
|
|
178
|
+
return null; // No conditions to satisfy
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
toolName: 'hooks_execute_effects',
|
|
183
|
+
input: { store, hooks, delta: { adds: 0, deletes: 0 } },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Step 2+: check goal condition
|
|
188
|
+
if (stepCount >= 2) {
|
|
189
|
+
if (await goalCondition(store, previousResult)) {
|
|
190
|
+
episode.recordFeedback(1.0, 'goal condition satisfied');
|
|
191
|
+
return null; // Success
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Re-evaluate to see if more conditions fire
|
|
195
|
+
return {
|
|
196
|
+
toolName: 'hooks_evaluate_conditions',
|
|
197
|
+
input: { store, hooks },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Compute feedback signal based on hook execution outcome
|
|
207
|
+
*
|
|
208
|
+
* @param {Object} executionResult - from hooks_execute_effects
|
|
209
|
+
* @param {Object} previousResult - from hooks_evaluate_conditions
|
|
210
|
+
* @returns {number} feedback signal (-1 to 1)
|
|
211
|
+
*/
|
|
212
|
+
export function computeHooksFeedback(executionResult, _previousResult) {
|
|
213
|
+
if (!executionResult) return -0.5; // Execution failed
|
|
214
|
+
|
|
215
|
+
const { receipt } = executionResult;
|
|
216
|
+
if (!receipt) return 0; // No-op
|
|
217
|
+
|
|
218
|
+
const { hooksExecuted, successful, failed } = receipt;
|
|
219
|
+
|
|
220
|
+
if (failed > 0) return -0.3; // Some failures
|
|
221
|
+
if (successful === 0) return 0; // No hooks ran (no-op)
|
|
222
|
+
if (hooksExecuted > 0) return 0.1 + 0.1 * Math.min(successful / hooksExecuted, 1); // Pro-rata success
|
|
223
|
+
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Receipt chain node
|
|
229
|
+
*/
|
|
230
|
+
class ReceiptChainNode {
|
|
231
|
+
constructor(receipt, previousNode = null) {
|
|
232
|
+
this.receiptHash = receipt.receiptHash;
|
|
233
|
+
this.input_hash = receipt.input_hash;
|
|
234
|
+
this.output_hash = receipt.output_hash;
|
|
235
|
+
this.previousReceiptHash = previousNode?.receiptHash || receipt.previousReceiptHash || null;
|
|
236
|
+
this.timestamp = receipt.timestamp || new Date().toISOString();
|
|
237
|
+
this.delta = receipt.delta;
|
|
238
|
+
this.hooksExecuted = receipt.hooksExecuted;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
toJSON() {
|
|
242
|
+
return {
|
|
243
|
+
receiptHash: this.receiptHash,
|
|
244
|
+
input_hash: this.input_hash,
|
|
245
|
+
output_hash: this.output_hash,
|
|
246
|
+
previousReceiptHash: this.previousReceiptHash,
|
|
247
|
+
timestamp: this.timestamp,
|
|
248
|
+
delta: this.delta,
|
|
249
|
+
hooksExecuted: this.hooksExecuted,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Run full autonomous hooks loop across multiple episodes
|
|
256
|
+
*
|
|
257
|
+
* @param {Object} store - RDF store (shared across episodes)
|
|
258
|
+
* @param {Array} hookDefinitions - KnowledgeHook array
|
|
259
|
+
* @param {Object} options - { goalCondition, episodeCount, maxStepsPerEpisode, onEpisodeEnd }
|
|
260
|
+
* @returns {Promise<Object>} { episodes, finalStore, receiptChain, stats }
|
|
261
|
+
*/
|
|
262
|
+
export async function runHooksAutonomics(store, hookDefinitions = [], options = {}) {
|
|
263
|
+
const {
|
|
264
|
+
goalCondition = async () => false,
|
|
265
|
+
episodeCount = 3,
|
|
266
|
+
maxStepsPerEpisode = 10,
|
|
267
|
+
onEpisodeEnd = () => {},
|
|
268
|
+
} = options;
|
|
269
|
+
|
|
270
|
+
// Build tool registry
|
|
271
|
+
const toolRegistry = buildHooksToolRegistry(store, hookDefinitions);
|
|
272
|
+
|
|
273
|
+
// Create decision policy
|
|
274
|
+
const decisionFn = createHooksAwarePolicy(goalCondition);
|
|
275
|
+
|
|
276
|
+
// Track receipt chain
|
|
277
|
+
const receiptChain = [];
|
|
278
|
+
let previousReceiptNode = null;
|
|
279
|
+
|
|
280
|
+
const episodes = [];
|
|
281
|
+
|
|
282
|
+
for (let e = 0; e < episodeCount; e++) {
|
|
283
|
+
const episode = {
|
|
284
|
+
episodeId: randomUUID(),
|
|
285
|
+
stepCount: 0,
|
|
286
|
+
stepResults: [],
|
|
287
|
+
feedback: [],
|
|
288
|
+
terminated: false,
|
|
289
|
+
terminationReason: null,
|
|
290
|
+
timestamp: new Date().toISOString(),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const context = {
|
|
294
|
+
store,
|
|
295
|
+
hooks: hookDefinitions,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
let previousResult = null;
|
|
299
|
+
|
|
300
|
+
for (let step = 0; step < maxStepsPerEpisode; step++) {
|
|
301
|
+
if (episode.terminated) break;
|
|
302
|
+
|
|
303
|
+
// Decide next tool
|
|
304
|
+
const decision = await decisionFn(
|
|
305
|
+
{
|
|
306
|
+
steps: episode.stepResults,
|
|
307
|
+
context,
|
|
308
|
+
recordFeedback: (s, r) => episode.feedback.push({ signal: s, reason: r }),
|
|
309
|
+
},
|
|
310
|
+
previousResult
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (!decision) {
|
|
314
|
+
episode.terminated = true;
|
|
315
|
+
episode.terminationReason = 'no more decisions';
|
|
316
|
+
episode.feedback.push({ signal: 0, reason: 'terminated: no more decisions' });
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const { toolName, input } = decision;
|
|
321
|
+
|
|
322
|
+
// Execute tool
|
|
323
|
+
const stepStartTime = Date.now();
|
|
324
|
+
try {
|
|
325
|
+
const tool = toolRegistry[toolName];
|
|
326
|
+
if (!tool) {
|
|
327
|
+
episode.terminated = true;
|
|
328
|
+
episode.terminationReason = `unknown tool: ${toolName}`;
|
|
329
|
+
episode.feedback.push({ signal: -1, reason: `unknown tool: ${toolName}` });
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const result = await tool.handler(input);
|
|
334
|
+
const duration = Date.now() - stepStartTime;
|
|
335
|
+
|
|
336
|
+
episode.stepResults.push({
|
|
337
|
+
stepId: randomUUID(),
|
|
338
|
+
toolName,
|
|
339
|
+
input,
|
|
340
|
+
output: result,
|
|
341
|
+
duration,
|
|
342
|
+
timestamp: new Date().toISOString(),
|
|
343
|
+
success: true,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
previousResult = result;
|
|
347
|
+
|
|
348
|
+
// Record receipt if present
|
|
349
|
+
if (result?.receipt) {
|
|
350
|
+
const receiptNode = new ReceiptChainNode(result.receipt, previousReceiptNode);
|
|
351
|
+
receiptChain.push(receiptNode.toJSON());
|
|
352
|
+
previousReceiptNode = receiptNode;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Compute feedback
|
|
356
|
+
const feedback = computeHooksFeedback(result, previousResult);
|
|
357
|
+
episode.feedback.push({
|
|
358
|
+
signal: feedback,
|
|
359
|
+
reason: `${toolName} succeeded`,
|
|
360
|
+
});
|
|
361
|
+
} catch (err) {
|
|
362
|
+
const duration = Date.now() - stepStartTime;
|
|
363
|
+
|
|
364
|
+
episode.stepResults.push({
|
|
365
|
+
stepId: randomUUID(),
|
|
366
|
+
toolName,
|
|
367
|
+
input,
|
|
368
|
+
output: null,
|
|
369
|
+
duration,
|
|
370
|
+
timestamp: new Date().toISOString(),
|
|
371
|
+
success: false,
|
|
372
|
+
error: err.message,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
episode.terminated = true;
|
|
376
|
+
episode.terminationReason = `tool failed: ${toolName}`;
|
|
377
|
+
episode.feedback.push({ signal: -0.5, reason: `${toolName} failed: ${err.message}` });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
episode.stepCount++;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!episode.terminated && episode.stepCount >= maxStepsPerEpisode) {
|
|
384
|
+
episode.terminated = true;
|
|
385
|
+
episode.terminationReason = 'max steps reached';
|
|
386
|
+
episode.feedback.push({ signal: 0, reason: 'max steps reached' });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Calculate metrics
|
|
390
|
+
const totalFeedback = episode.feedback.reduce((sum, f) => sum + f.signal, 0);
|
|
391
|
+
const avgFeedback = episode.feedback.length > 0 ? totalFeedback / episode.feedback.length : 0;
|
|
392
|
+
|
|
393
|
+
episode.metrics = {
|
|
394
|
+
stepCount: episode.stepCount,
|
|
395
|
+
feedbackCount: episode.feedback.length,
|
|
396
|
+
totalFeedback,
|
|
397
|
+
avgFeedback,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
episodes.push(episode);
|
|
401
|
+
await onEpisodeEnd(episode);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Calculate stats
|
|
405
|
+
const totalFeedback = episodes.reduce((sum, ep) => sum + ep.metrics.totalFeedback, 0);
|
|
406
|
+
const successCount = episodes.filter(ep => ep.metrics.totalFeedback > 0).length;
|
|
407
|
+
|
|
408
|
+
const stats = {
|
|
409
|
+
totalEpisodes: episodes.length,
|
|
410
|
+
successCount,
|
|
411
|
+
successRate: episodes.length > 0 ? successCount / episodes.length : 0,
|
|
412
|
+
totalFeedback,
|
|
413
|
+
avgFeedback: episodes.length > 0 ? totalFeedback / episodes.length : 0,
|
|
414
|
+
receiptChainLength: receiptChain.length,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
episodes,
|
|
419
|
+
finalStore: store,
|
|
420
|
+
receiptChain,
|
|
421
|
+
stats,
|
|
422
|
+
};
|
|
423
|
+
}
|
package/src/hooks/telemetry.mjs
CHANGED
|
@@ -48,10 +48,7 @@ export class BatchedTelemetry {
|
|
|
48
48
|
|
|
49
49
|
try {
|
|
50
50
|
return this.tracer.startSpan(name, {
|
|
51
|
-
attributes
|
|
52
|
-
'hook.transaction': true,
|
|
53
|
-
...attributes,
|
|
54
|
-
},
|
|
51
|
+
attributes,
|
|
55
52
|
});
|
|
56
53
|
} catch {
|
|
57
54
|
return null;
|
|
@@ -68,7 +65,7 @@ export class BatchedTelemetry {
|
|
|
68
65
|
* @param {string} key - Attribute key
|
|
69
66
|
* @param {*} value - Attribute value
|
|
70
67
|
*/
|
|
71
|
-
|
|
68
|
+
addPendingAttribute(span, key, value) {
|
|
72
69
|
if (!this.enabled || !span) {
|
|
73
70
|
return;
|
|
74
71
|
}
|
|
@@ -78,7 +75,7 @@ export class BatchedTelemetry {
|
|
|
78
75
|
|
|
79
76
|
// Schedule batch flush if not already scheduled
|
|
80
77
|
if (!this.flushTimeout) {
|
|
81
|
-
this.flushTimeout = setTimeout(() => this.
|
|
78
|
+
this.flushTimeout = setTimeout(() => this.flushPendingAttributes(), this.flushInterval);
|
|
82
79
|
}
|
|
83
80
|
}
|
|
84
81
|
|
|
@@ -88,7 +85,7 @@ export class BatchedTelemetry {
|
|
|
88
85
|
* This reduces the number of span attribute mutations
|
|
89
86
|
* by batching them together rather than setting each individually.
|
|
90
87
|
*/
|
|
91
|
-
|
|
88
|
+
flushPendingAttributes() {
|
|
92
89
|
try {
|
|
93
90
|
for (const { span, key, value } of this.pendingAttributes) {
|
|
94
91
|
try {
|
|
@@ -137,7 +134,7 @@ export class BatchedTelemetry {
|
|
|
137
134
|
try {
|
|
138
135
|
// Flush any pending attributes first
|
|
139
136
|
if (this.pendingAttributes.length > 0) {
|
|
140
|
-
this.
|
|
137
|
+
this.flushPendingAttributes();
|
|
141
138
|
}
|
|
142
139
|
|
|
143
140
|
// End span with status
|
|
@@ -148,12 +145,24 @@ export class BatchedTelemetry {
|
|
|
148
145
|
}
|
|
149
146
|
}
|
|
150
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Cleanup telemetry (flush and clear state)
|
|
150
|
+
*/
|
|
151
|
+
cleanup() {
|
|
152
|
+
if (this.flushTimeout) {
|
|
153
|
+
clearTimeout(this.flushTimeout);
|
|
154
|
+
this.flushTimeout = null;
|
|
155
|
+
}
|
|
156
|
+
this.flushPendingAttributes();
|
|
157
|
+
this.pendingAttributes = [];
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
/**
|
|
152
161
|
* Disable telemetry (for production or testing)
|
|
153
162
|
*/
|
|
154
163
|
disable() {
|
|
155
164
|
this.enabled = false;
|
|
156
|
-
this.
|
|
165
|
+
this.cleanup();
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
/**
|
|
@@ -162,6 +171,20 @@ export class BatchedTelemetry {
|
|
|
162
171
|
enable() {
|
|
163
172
|
this.enabled = true;
|
|
164
173
|
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Alias for addPendingAttribute (backward compatibility)
|
|
177
|
+
*/
|
|
178
|
+
setAttribute(span, key, value) {
|
|
179
|
+
return this.addPendingAttribute(span, key, value);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Alias for flushPendingAttributes (backward compatibility)
|
|
184
|
+
*/
|
|
185
|
+
flush() {
|
|
186
|
+
return this.flushPendingAttributes();
|
|
187
|
+
}
|
|
165
188
|
}
|
|
166
189
|
|
|
167
190
|
export default BatchedTelemetry;
|
package/src/hooks/validate.mjs
CHANGED
|
@@ -3,56 +3,104 @@
|
|
|
3
3
|
* @module hooks/validate
|
|
4
4
|
*
|
|
5
5
|
* @description
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Full SHACL validation using rdf-validate-shacl against Oxigraph-backed RDF stores.
|
|
7
|
+
* Supports minCount, maxCount, datatype, and other SHACL Core constraints.
|
|
8
|
+
* Caches parsed shape validators for performance.
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
import SHACLValidator from 'rdf-validate-shacl';
|
|
12
|
+
import oxigraph from 'oxigraph';
|
|
13
|
+
|
|
14
|
+
const SHACL_NS = 'http://www.w3.org/ns/shacl#';
|
|
15
|
+
|
|
16
|
+
// Severity URI to short string mapping
|
|
17
|
+
const SEVERITY_MAP = {
|
|
18
|
+
[`${SHACL_NS}Violation`]: 'violation',
|
|
19
|
+
[`${SHACL_NS}Warning`]: 'warning',
|
|
20
|
+
[`${SHACL_NS}Info`]: 'info',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Cache parsed SHACLValidator instances keyed by shapes string
|
|
24
|
+
const validatorCache = new Map();
|
|
25
|
+
const CACHE_MAX_SIZE = 50;
|
|
26
|
+
|
|
10
27
|
/**
|
|
11
|
-
*
|
|
28
|
+
* Get or create a cached SHACLValidator for the given shapes Turtle string.
|
|
12
29
|
*
|
|
13
|
-
* @param {
|
|
30
|
+
* @param {string} shapesString - SHACL shapes as Turtle
|
|
31
|
+
* @returns {SHACLValidator} Cached validator instance
|
|
32
|
+
*/
|
|
33
|
+
function getValidator(shapesString) {
|
|
34
|
+
let validator = validatorCache.get(shapesString);
|
|
35
|
+
if (validator) {
|
|
36
|
+
return validator;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const shapesStore = new oxigraph.Store();
|
|
40
|
+
shapesStore.load(shapesString, { format: 'text/turtle' });
|
|
41
|
+
const shapesQuads = shapesStore.match(null, null, null, null);
|
|
42
|
+
|
|
43
|
+
validator = new SHACLValidator(shapesQuads);
|
|
44
|
+
|
|
45
|
+
// Evict oldest entry if cache is full
|
|
46
|
+
if (validatorCache.size >= CACHE_MAX_SIZE) {
|
|
47
|
+
const firstKey = validatorCache.keys().next().value;
|
|
48
|
+
validatorCache.delete(firstKey);
|
|
49
|
+
}
|
|
50
|
+
validatorCache.set(shapesString, validator);
|
|
51
|
+
|
|
52
|
+
return validator;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate RDF data against SHACL shapes.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} dataStore - RDF store containing data to validate (OxigraphStore or raw oxigraph.Store)
|
|
14
59
|
* @param {string} shapesString - SHACL shapes as Turtle string
|
|
15
60
|
* @param {object} options - Validation options
|
|
16
61
|
* @param {boolean} options.strict - Enable strict validation mode
|
|
17
62
|
* @param {boolean} options.includeDetails - Include validation details in report
|
|
18
|
-
* @returns {object} SHACL validation report
|
|
63
|
+
* @returns {Promise<object>} SHACL validation report
|
|
19
64
|
*/
|
|
20
|
-
export function validateShacl(dataStore, shapesString, options = {}) {
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
// Simplified SHACL validation
|
|
24
|
-
// In production, use a full SHACL validator like rdf-validate-shacl
|
|
25
|
-
|
|
26
|
-
const report = {
|
|
27
|
-
conforms: true,
|
|
28
|
-
results: [],
|
|
29
|
-
timestamp: new Date().toISOString(),
|
|
30
|
-
};
|
|
65
|
+
export async function validateShacl(dataStore, shapesString, options = {}) {
|
|
66
|
+
const { includeDetails = true } = options;
|
|
31
67
|
|
|
32
68
|
try {
|
|
33
|
-
// Basic validation: check if data store is valid
|
|
34
69
|
if (!dataStore || typeof dataStore.size !== 'number') {
|
|
35
70
|
throw new Error('Invalid data store');
|
|
36
71
|
}
|
|
37
|
-
|
|
38
|
-
// Check if shapes string is valid
|
|
39
72
|
if (!shapesString || typeof shapesString !== 'string') {
|
|
40
73
|
throw new Error('Invalid shapes string');
|
|
41
74
|
}
|
|
42
75
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
const validator = getValidator(shapesString);
|
|
77
|
+
|
|
78
|
+
// Use the raw oxigraph.Store for validation (unwrap OxigraphStore wrapper if needed)
|
|
79
|
+
const rawStore = dataStore.store || dataStore;
|
|
80
|
+
|
|
81
|
+
const shaclReport = await validator.validate(rawStore);
|
|
48
82
|
|
|
49
|
-
|
|
50
|
-
|
|
83
|
+
const results = (shaclReport.results || []).map(result => ({
|
|
84
|
+
severity: SEVERITY_MAP[result.severity?.value] || 'violation',
|
|
85
|
+
message: result.message?.map(m => m.value).join('; ') || null,
|
|
86
|
+
focusNode: result.focusNode?.value || null,
|
|
87
|
+
resultPath: result.path?.value || null,
|
|
88
|
+
resultMessage: result.message?.map(m => m.value).join('; ') || null,
|
|
89
|
+
value: result.value?.value || null,
|
|
90
|
+
sourceConstraintComponent: result.sourceConstraintComponent?.value || null,
|
|
91
|
+
sourceShape: result.sourceShape?.value || null,
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
const report = {
|
|
95
|
+
conforms: shaclReport.conforms,
|
|
96
|
+
results,
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
};
|
|
51
99
|
|
|
52
100
|
if (includeDetails) {
|
|
53
101
|
report.details = {
|
|
54
|
-
shapesCount:
|
|
55
|
-
constraintsChecked:
|
|
102
|
+
shapesCount: countShapes(shapesString),
|
|
103
|
+
constraintsChecked: results.length,
|
|
56
104
|
validationTime: 0,
|
|
57
105
|
};
|
|
58
106
|
}
|
|
@@ -65,7 +113,9 @@ export function validateShacl(dataStore, shapesString, options = {}) {
|
|
|
65
113
|
{
|
|
66
114
|
severity: 'violation',
|
|
67
115
|
message: `SHACL validation error: ${error.message}`,
|
|
68
|
-
|
|
116
|
+
focusNode: null,
|
|
117
|
+
resultPath: null,
|
|
118
|
+
resultMessage: `SHACL validation error: ${error.message}`,
|
|
69
119
|
value: null,
|
|
70
120
|
},
|
|
71
121
|
],
|
|
@@ -75,6 +125,17 @@ export function validateShacl(dataStore, shapesString, options = {}) {
|
|
|
75
125
|
}
|
|
76
126
|
}
|
|
77
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Count the number of sh:NodeShape declarations in a shapes string.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} shapesString - Turtle shapes string
|
|
132
|
+
* @returns {number} Approximate number of shapes
|
|
133
|
+
*/
|
|
134
|
+
function countShapes(shapesString) {
|
|
135
|
+
const matches = shapesString.match(/sh:NodeShape/g);
|
|
136
|
+
return matches ? matches.length : 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
78
139
|
/**
|
|
79
140
|
* Validate a single node against SHACL shapes
|
|
80
141
|
*
|
|
@@ -82,11 +143,10 @@ export function validateShacl(dataStore, shapesString, options = {}) {
|
|
|
82
143
|
* @param {object} node - Node to validate
|
|
83
144
|
* @param {string} shapesString - SHACL shapes
|
|
84
145
|
* @param {object} options - Validation options
|
|
85
|
-
* @returns {object} Validation report for the node
|
|
146
|
+
* @returns {Promise<object>} Validation report for the node
|
|
86
147
|
*/
|
|
87
|
-
export function validateNode(store, node, shapesString, options = {}) {
|
|
88
|
-
|
|
89
|
-
const report = validateShacl(store, shapesString, options);
|
|
148
|
+
export async function validateNode(store, node, shapesString, options = {}) {
|
|
149
|
+
const report = await validateShacl(store, shapesString, options);
|
|
90
150
|
|
|
91
151
|
return {
|
|
92
152
|
...report,
|
|
@@ -131,3 +191,10 @@ export function getWarnings(report) {
|
|
|
131
191
|
|
|
132
192
|
return report.results.filter(result => result.severity === 'warning');
|
|
133
193
|
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Clear the validator cache. Useful in tests or when shapes change frequently.
|
|
197
|
+
*/
|
|
198
|
+
export function clearValidatorCache() {
|
|
199
|
+
validatorCache.clear();
|
|
200
|
+
}
|
package/src/index.mjs
CHANGED