@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.
Files changed (60) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +562 -53
  3. package/examples/atomvm-fibo-hooks-demo.mjs +323 -0
  4. package/examples/delta-monitoring-example.mjs +213 -0
  5. package/examples/fibo-jtbd-governance.mjs +388 -0
  6. package/examples/hook-chains/node_modules/.bin/jiti +21 -0
  7. package/examples/hook-chains/node_modules/.bin/msw +21 -0
  8. package/examples/hook-chains/node_modules/.bin/terser +21 -0
  9. package/examples/hook-chains/node_modules/.bin/tsc +21 -0
  10. package/examples/hook-chains/node_modules/.bin/tsserver +21 -0
  11. package/examples/hook-chains/node_modules/.bin/tsx +21 -0
  12. package/examples/hook-chains/node_modules/.bin/vite +21 -0
  13. package/examples/hook-chains/node_modules/.bin/vitest +2 -2
  14. package/examples/hook-chains/node_modules/.bin/yaml +21 -0
  15. package/examples/hook-chains/package.json +2 -2
  16. package/examples/hook-chains/unrdf-hooks-example-chains-5.0.0.tgz +0 -0
  17. package/examples/hooks-marketplace.mjs +261 -0
  18. package/examples/n3-reasoning-example.mjs +279 -0
  19. package/examples/policy-hooks/node_modules/.bin/jiti +21 -0
  20. package/examples/policy-hooks/node_modules/.bin/msw +21 -0
  21. package/examples/policy-hooks/node_modules/.bin/terser +21 -0
  22. package/examples/policy-hooks/node_modules/.bin/tsc +21 -0
  23. package/examples/policy-hooks/node_modules/.bin/tsserver +21 -0
  24. package/examples/policy-hooks/node_modules/.bin/tsx +21 -0
  25. package/examples/policy-hooks/node_modules/.bin/vite +21 -0
  26. package/examples/policy-hooks/node_modules/.bin/vitest +2 -2
  27. package/examples/policy-hooks/node_modules/.bin/yaml +21 -0
  28. package/examples/policy-hooks/package.json +2 -2
  29. package/examples/policy-hooks/unrdf-hooks-example-policy-5.0.0.tgz +0 -0
  30. package/examples/shacl-repair-example.mjs +191 -0
  31. package/examples/window-condition-example.mjs +285 -0
  32. package/package.json +6 -3
  33. package/src/atomvm.mjs +9 -0
  34. package/src/define.mjs +114 -0
  35. package/src/executor.mjs +23 -0
  36. package/src/hooks/atomvm-bridge.mjs +332 -0
  37. package/src/hooks/builtin-hooks.mjs +13 -7
  38. package/src/hooks/condition-evaluator.mjs +684 -77
  39. package/src/hooks/define-hook.mjs +23 -21
  40. package/src/hooks/effect-executor.mjs +630 -0
  41. package/src/hooks/effect-sandbox.mjs +19 -9
  42. package/src/hooks/file-resolver.mjs +155 -1
  43. package/src/hooks/hook-chain-compiler.mjs +11 -1
  44. package/src/hooks/hook-executor.mjs +98 -73
  45. package/src/hooks/knowledge-hook-engine.mjs +133 -7
  46. package/src/hooks/ontology-learner.mjs +190 -0
  47. package/src/hooks/policy-pack.mjs +7 -1
  48. package/src/hooks/query-optimizer.mjs +1 -5
  49. package/src/hooks/query.mjs +3 -3
  50. package/src/hooks/schemas.mjs +55 -5
  51. package/src/hooks/security/error-sanitizer.mjs +46 -24
  52. package/src/hooks/self-play-autonomics.mjs +423 -0
  53. package/src/hooks/telemetry.mjs +32 -9
  54. package/src/hooks/validate.mjs +100 -33
  55. package/src/index.mjs +2 -0
  56. package/src/lib/admit-hook.mjs +615 -0
  57. package/src/policy-compiler.mjs +23 -13
  58. package/dist/index.d.mts +0 -1738
  59. package/dist/index.d.ts +0 -1738
  60. 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
+ }
@@ -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
- setAttribute(span, key, value) {
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.flush(), this.flushInterval);
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
- flush() {
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.flush();
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.flush();
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;
@@ -3,56 +3,104 @@
3
3
  * @module hooks/validate
4
4
  *
5
5
  * @description
6
- * Provides SHACL validation against RDF stores.
7
- * This is a simplified implementation - full SHACL validation would require a dedicated library.
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
- * Validate RDF data against SHACL shapes
28
+ * Get or create a cached SHACLValidator for the given shapes Turtle string.
12
29
  *
13
- * @param {object} dataStore - RDF store containing data to validate
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 { strict = false, includeDetails = true } = options;
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
- // In a full implementation, this would:
44
- // 1. Parse SHACL shapes from shapesString
45
- // 2. Iterate through each shape
46
- // 3. Apply constraints to data
47
- // 4. Collect validation results
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
- // For now, return conforming result
50
- // This should be replaced with actual SHACL validation logic
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: 0,
55
- constraintsChecked: 0,
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
- path: null,
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
- // Simplified node validation
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
@@ -116,6 +116,8 @@ export {
116
116
  evaluateCondition,
117
117
  createConditionEvaluator,
118
118
  validateCondition,
119
+ serializeShaclReport,
120
+ SlidingWindow,
119
121
  } from './hooks/condition-evaluator.mjs';
120
122
 
121
123
  // File Resolver (Content-addressed file loading)