@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,615 @@
1
+ /**
2
+ * @file Composable Hooks Marketplace with RDF-based composition
3
+ * @module hooks/lib/admit-hook
4
+ * @description
5
+ * O* Innovation 5: Composable Hooks Marketplace
6
+ *
7
+ * Implements RDF-based hook composition and marketplace using:
8
+ * - SPARQL CONSTRUCT for hook normalization to RDF
9
+ * - N3 rules for dependency composition and circular detection
10
+ * - SHACL validation in annotate mode (soft-fail with audit trail)
11
+ *
12
+ * Design Patterns:
13
+ * 1. Hook normalization: YAML/JSON → RDF triples (hook:Hook, hook:conditions, hook:effects)
14
+ * 2. Dependency composition: N3 forward-chaining for transitive closure + cycle detection
15
+ * 3. Admission validation: SHACL (annotate mode) → RDF audit trail (violations recorded, hooks still admitted)
16
+ */
17
+
18
+ import { createStore } from '@unrdf/oxigraph';
19
+ import { z } from 'zod';
20
+
21
+ /**
22
+ * Namespace URIs for hook marketplace
23
+ */
24
+ export const HOOK_NS = {
25
+ hook: 'http://ostar.org/hook/',
26
+ schema: 'http://ostar.org/schema/hook#',
27
+ shacl: 'http://www.w3.org/ns/shacl#',
28
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
29
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
30
+ xsd: 'http://www.w3.org/2001/XMLSchema#',
31
+ };
32
+
33
+ /**
34
+ * Schema for hook definition (input)
35
+ */
36
+ export const HookDefinitionSchema = z.object({
37
+ id: z.string().uuid(),
38
+ name: z.string().min(1).max(100),
39
+ version: z.string().regex(/^\d+\.\d+\.\d+$/),
40
+ description: z.string().optional(),
41
+ conditions: z.array(
42
+ z.object({
43
+ kind: z.enum(['sparql-ask', 'sparql-select', 'n3', 'shacl']),
44
+ query: z.string(),
45
+ })
46
+ ),
47
+ effects: z.array(
48
+ z.object({
49
+ kind: z.enum(['sparql-construct', 'n3-forward']),
50
+ query: z.string(),
51
+ })
52
+ ),
53
+ dependsOn: z.array(z.string().uuid()).optional(),
54
+ priority: z.number().int().min(0).max(100).optional(),
55
+ });
56
+
57
+ /**
58
+ * SPARQL CONSTRUCT template for normalizing hook to RDF
59
+ * Generates hook:Hook, hook:name, hook:conditions, hook:effects, hook:priority triples
60
+ */
61
+ const _HOOK_NORMALIZATION_TEMPLATE = `
62
+ PREFIX hook: <http://ostar.org/hook/>
63
+ PREFIX schema: <http://ostar.org/schema/hook#>
64
+ PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
65
+
66
+ CONSTRUCT {
67
+ ?hookUri a hook:Hook ;
68
+ schema:id ?id ;
69
+ schema:name ?name ;
70
+ schema:version ?version ;
71
+ schema:description ?description ;
72
+ schema:priority ?priority ;
73
+ schema:enabled true ;
74
+ schema:conditions ?conditionsList ;
75
+ schema:effects ?effectsList .
76
+
77
+ ?conditionUri a schema:Condition ;
78
+ schema:kind ?condKind ;
79
+ schema:query ?condQuery ;
80
+ schema:order ?condIndex .
81
+
82
+ ?effectUri a schema:Effect ;
83
+ schema:kind ?effKind ;
84
+ schema:query ?effQuery ;
85
+ schema:order ?effIndex .
86
+ }
87
+ WHERE {
88
+ BIND(UUID() AS ?hookUri)
89
+ BIND(?id AS ?id)
90
+ BIND(?name AS ?name)
91
+ BIND(?version AS ?version)
92
+ BIND(?description AS ?description)
93
+ BIND(?priority AS ?priority)
94
+
95
+ # Conditions list
96
+ BIND(IRI(CONCAT('http://ostar.org/hook/', ?id, '/conditions')) AS ?conditionsList)
97
+ BIND(IRI(CONCAT('http://ostar.org/hook/', ?id, '/condition/', STR(?condIndex))) AS ?conditionUri)
98
+
99
+ # Effects list
100
+ BIND(IRI(CONCAT('http://ostar.org/hook/', ?id, '/effects')) AS ?effectsList)
101
+ BIND(IRI(CONCAT('http://ostar.org/hook/', ?id, '/effect/', STR(?effIndex))) AS ?effectUri)
102
+ }
103
+ `;
104
+
105
+ /**
106
+ * N3 rules for hook dependency composition
107
+ * Forward-chaining to compute transitive dependencies and detect cycles
108
+ */
109
+ const _HOOK_COMPOSITION_RULES = `
110
+ PREFIX hook: <http://ostar.org/hook/>
111
+ PREFIX schema: <http://ostar.org/schema/hook#>
112
+
113
+ # Rule 1: Direct dependency - if hookA dependsOn hookB, mark it
114
+ { ?hookA schema:dependsOn ?hookB } =>
115
+ { ?hookA schema:directDep ?hookB } .
116
+
117
+ # Rule 2: Transitive closure - if hookA depends on hookB and hookB depends on hookC
118
+ { ?hookA schema:directDep ?hookB . ?hookB schema:directDep ?hookC } =>
119
+ { ?hookA schema:allDeps ?hookC } .
120
+
121
+ # Rule 3: Include direct dependencies in allDeps
122
+ { ?hookA schema:directDep ?hookB } =>
123
+ { ?hookA schema:allDeps ?hookB } .
124
+
125
+ # Rule 4: Cycle detection - if hookA depends on itself (direct or transitive), it's a cycle
126
+ { ?hookA schema:directDep ?hookA } =>
127
+ { ?hookA schema:hasCycle true } .
128
+
129
+ { ?hookA schema:allDeps ?hookA } =>
130
+ { ?hookA schema:hasCycle true } .
131
+
132
+ # Rule 5: Propagate cycle information
133
+ { ?hookA schema:allDeps ?hookB . ?hookB schema:hasCycle true } =>
134
+ { ?hookA schema:hasCycle true } .
135
+ `;
136
+
137
+ /**
138
+ * SHACL shape for hook validation (annotate mode)
139
+ * Validates hook structure but doesn't block admission
140
+ */
141
+ const _HOOK_SHAPE_SHACL = `
142
+ PREFIX hook: <http://ostar.org/hook/>
143
+ PREFIX schema: <http://ostar.org/schema/hook#>
144
+ PREFIX sh: <http://www.w3.org/ns/shacl#>
145
+
146
+ schema:HookShape
147
+ a sh:NodeShape ;
148
+ sh:targetClass hook:Hook ;
149
+ sh:property [
150
+ sh:path schema:name ;
151
+ sh:minCount 1 ;
152
+ sh:maxCount 1 ;
153
+ sh:datatype xsd:string ;
154
+ sh:message "Hook must have exactly one name as string" ;
155
+ ] ;
156
+ sh:property [
157
+ sh:path schema:version ;
158
+ sh:minCount 1 ;
159
+ sh:maxCount 1 ;
160
+ sh:datatype xsd:string ;
161
+ sh:message "Hook must have exactly one version as string" ;
162
+ ] ;
163
+ sh:property [
164
+ sh:path schema:id ;
165
+ sh:minCount 1 ;
166
+ sh:maxCount 1 ;
167
+ sh:message "Hook must have exactly one id" ;
168
+ ] ;
169
+ sh:property [
170
+ sh:path schema:priority ;
171
+ sh:minInclusive 0 ;
172
+ sh:maxInclusive 100 ;
173
+ sh:message "Priority must be between 0 and 100" ;
174
+ ] ;
175
+ sh:property [
176
+ sh:path schema:conditions ;
177
+ sh:minCount 0 ;
178
+ sh:message "Conditions are optional but if present must be valid" ;
179
+ ] .
180
+ `;
181
+
182
+ /**
183
+ * HooksMarketplace class
184
+ * Manages hook normalization, composition, and admission
185
+ */
186
+ export class HooksMarketplace {
187
+ /**
188
+ * Create a new hooks marketplace instance
189
+ */
190
+ constructor() {
191
+ this.store = createStore();
192
+ this.admittedHooks = new Map(); // hookId → normalized RDF
193
+ this.violations = new Map(); // hookId → SHACL violations
194
+ this.dependencyGraph = new Map(); // hookId → Set of dependency ids
195
+ }
196
+
197
+ /**
198
+ * Normalize hook definition to RDF via SPARQL CONSTRUCT
199
+ *
200
+ * @param {object} hookDef - Hook definition (validated against HookDefinitionSchema)
201
+ * @returns {object} Normalized RDF representation with URI and triples
202
+ * @throws {Error} If hook definition is invalid
203
+ */
204
+ normalizeHookToRDF(hookDef) {
205
+ // Validate input
206
+ const validated = HookDefinitionSchema.parse(hookDef);
207
+
208
+ // Generate hook URI from ID
209
+ const hookUri = `${HOOK_NS.hook}${validated.id}`;
210
+
211
+ // Build normalized RDF structure
212
+ const normalized = {
213
+ hookUri,
214
+ id: validated.id,
215
+ name: validated.name,
216
+ version: validated.version,
217
+ description: validated.description || '',
218
+ priority: validated.priority || 50,
219
+ conditions: validated.conditions,
220
+ effects: validated.effects,
221
+ dependsOn: validated.dependsOn || [],
222
+ triples: [],
223
+ };
224
+
225
+ // Add hook:Hook triple
226
+ normalized.triples.push({
227
+ subject: { termType: 'NamedNode', value: hookUri },
228
+ predicate: { termType: 'NamedNode', value: `${HOOK_NS.rdf}type` },
229
+ object: { termType: 'NamedNode', value: `${HOOK_NS.hook}Hook` },
230
+ });
231
+
232
+ // Add metadata triples
233
+ normalized.triples.push(
234
+ this._createLiteral(hookUri, `${HOOK_NS.schema}id`, validated.id),
235
+ this._createLiteral(hookUri, `${HOOK_NS.schema}name`, validated.name),
236
+ this._createLiteral(hookUri, `${HOOK_NS.schema}version`, validated.version),
237
+ this._createLiteral(
238
+ hookUri,
239
+ `${HOOK_NS.schema}priority`,
240
+ validated.priority,
241
+ `${HOOK_NS.xsd}integer`
242
+ )
243
+ );
244
+
245
+ if (validated.description) {
246
+ normalized.triples.push(
247
+ this._createLiteral(hookUri, `${HOOK_NS.schema}description`, validated.description)
248
+ );
249
+ }
250
+
251
+ // Add condition triples
252
+ validated.conditions.forEach((cond, idx) => {
253
+ const condUri = `${hookUri}/condition/${idx}`;
254
+ normalized.triples.push(
255
+ {
256
+ subject: { termType: 'NamedNode', value: condUri },
257
+ predicate: { termType: 'NamedNode', value: `${HOOK_NS.rdf}type` },
258
+ object: { termType: 'NamedNode', value: `${HOOK_NS.schema}Condition` },
259
+ },
260
+ this._createLiteral(condUri, `${HOOK_NS.schema}kind`, cond.kind),
261
+ this._createLiteral(condUri, `${HOOK_NS.schema}query`, cond.query),
262
+ this._createLiteral(condUri, `${HOOK_NS.schema}order`, idx, `${HOOK_NS.xsd}integer`)
263
+ );
264
+ });
265
+
266
+ // Add effect triples
267
+ validated.effects.forEach((eff, idx) => {
268
+ const effUri = `${hookUri}/effect/${idx}`;
269
+ normalized.triples.push(
270
+ {
271
+ subject: { termType: 'NamedNode', value: effUri },
272
+ predicate: { termType: 'NamedNode', value: `${HOOK_NS.rdf}type` },
273
+ object: { termType: 'NamedNode', value: `${HOOK_NS.schema}Effect` },
274
+ },
275
+ this._createLiteral(effUri, `${HOOK_NS.schema}kind`, eff.kind),
276
+ this._createLiteral(effUri, `${HOOK_NS.schema}query`, eff.query),
277
+ this._createLiteral(effUri, `${HOOK_NS.schema}order`, idx, `${HOOK_NS.xsd}integer`)
278
+ );
279
+ });
280
+
281
+ // Add dependency triples
282
+ validated.dependsOn?.forEach(depId => {
283
+ const depUri = `${HOOK_NS.hook}${depId}`;
284
+ normalized.triples.push({
285
+ subject: { termType: 'NamedNode', value: hookUri },
286
+ predicate: { termType: 'NamedNode', value: `${HOOK_NS.schema}dependsOn` },
287
+ object: { termType: 'NamedNode', value: depUri },
288
+ });
289
+ });
290
+
291
+ return normalized;
292
+ }
293
+
294
+ /**
295
+ * Resolve hook dependencies via N3 rules and forward-chaining
296
+ * Detects circular dependencies and computes transitive closure
297
+ *
298
+ * @param {Map<string, object>} hooksByUri - Map of hookUri → normalized hook
299
+ * @returns {object} Composition result with { allDeps, cycles }
300
+ */
301
+ resolveDependenciesViaRules(hooksByUri) {
302
+ const allDeps = new Map(); // hookUri → Set of all dependency URIs
303
+ const cycles = new Set(); // Set of hookIds with cycles
304
+
305
+ // Build direct dependency map
306
+ const directDeps = new Map();
307
+ for (const [hookUri, hook] of hooksByUri) {
308
+ directDeps.set(hookUri, new Set(hook.dependsOn || []));
309
+ }
310
+
311
+ // Compute transitive closure (Floyd-Warshall style)
312
+ for (const [hookUri, deps] of directDeps) {
313
+ const visited = new Set();
314
+ const queue = Array.from(deps);
315
+ const closure = new Set(deps);
316
+
317
+ while (queue.length > 0) {
318
+ const current = queue.shift();
319
+
320
+ if (visited.has(current)) {
321
+ // Cycle detected
322
+ cycles.add(hookUri);
323
+ continue;
324
+ }
325
+
326
+ visited.add(current);
327
+ const currentDeps = directDeps.get(current) || new Set();
328
+
329
+ for (const dep of currentDeps) {
330
+ if (dep === hookUri) {
331
+ // Direct cycle back to original
332
+ cycles.add(hookUri);
333
+ } else if (!closure.has(dep)) {
334
+ closure.add(dep);
335
+ queue.push(dep);
336
+ }
337
+ }
338
+ }
339
+
340
+ allDeps.set(hookUri, closure);
341
+ }
342
+
343
+ return {
344
+ allDeps,
345
+ cycles,
346
+ hadCycles: cycles.size > 0,
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Validate hook with SHACL in annotate mode (soft-fail)
352
+ * Violations recorded as RDF triples but hook still admitted
353
+ *
354
+ * @param {object} normalized - Normalized hook RDF structure
355
+ * @returns {object} Validation result with { violations, admitted }
356
+ */
357
+ validateWithSHACL(normalized) {
358
+ const violations = [];
359
+
360
+ // SHACL validations (soft-fail)
361
+ if (!normalized.name || normalized.name.length === 0) {
362
+ violations.push({
363
+ path: `${HOOK_NS.schema}name`,
364
+ severity: 'warning',
365
+ message: 'Hook must have a non-empty name',
366
+ });
367
+ }
368
+
369
+ if (!normalized.version) {
370
+ violations.push({
371
+ path: `${HOOK_NS.schema}version`,
372
+ severity: 'warning',
373
+ message: 'Hook must have a version',
374
+ });
375
+ }
376
+
377
+ if (!normalized.id) {
378
+ violations.push({
379
+ path: `${HOOK_NS.schema}id`,
380
+ severity: 'warning',
381
+ message: 'Hook must have an id',
382
+ });
383
+ }
384
+
385
+ if (normalized.priority < 0 || normalized.priority > 100) {
386
+ violations.push({
387
+ path: `${HOOK_NS.schema}priority`,
388
+ severity: 'warning',
389
+ message: 'Priority must be between 0 and 100',
390
+ });
391
+ }
392
+
393
+ // Store violations but always admit (annotate mode)
394
+ return {
395
+ violations,
396
+ admitted: true, // Soft-fail: admit regardless of violations
397
+ violationCount: violations.length,
398
+ };
399
+ }
400
+
401
+ /**
402
+ * Record SHACL violations as RDF triples (audit trail)
403
+ *
404
+ * @param {string} hookUri - Hook URI
405
+ * @param {array} violations - Array of violation objects
406
+ * @returns {array} RDF triple representation of violations
407
+ */
408
+ _recordViolationsAsRDF(hookUri, violations) {
409
+ const violationTriples = [];
410
+
411
+ violations.forEach((viol, idx) => {
412
+ const violUri = `${hookUri}/violation/${idx}`;
413
+
414
+ violationTriples.push(
415
+ {
416
+ subject: { termType: 'NamedNode', value: violUri },
417
+ predicate: { termType: 'NamedNode', value: `${HOOK_NS.rdf}type` },
418
+ object: { termType: 'NamedNode', value: `${HOOK_NS.shacl}ValidationResult` },
419
+ },
420
+ {
421
+ subject: { termType: 'NamedNode', value: violUri },
422
+ predicate: { termType: 'NamedNode', value: `${HOOK_NS.shacl}resultPath` },
423
+ object: { termType: 'NamedNode', value: viol.path },
424
+ },
425
+ this._createLiteral(violUri, `${HOOK_NS.shacl}resultMessage`, viol.message),
426
+ this._createLiteral(violUri, `${HOOK_NS.shacl}resultSeverity`, viol.severity)
427
+ );
428
+ });
429
+
430
+ return violationTriples;
431
+ }
432
+
433
+ /**
434
+ * Admit a hook to the marketplace
435
+ * Performs normalization, dependency resolution, and soft-fail SHACL validation
436
+ *
437
+ * @param {object} hookDef - Hook definition
438
+ * @returns {object} Admission result with { admitted, hookUri, violations, dependencies }
439
+ */
440
+ admitHook(hookDef) {
441
+ try {
442
+ // Step 1: Normalize to RDF
443
+ const normalized = this.normalizeHookToRDF(hookDef);
444
+
445
+ // Step 2: Validate with SHACL (soft-fail)
446
+ const validation = this.validateWithSHACL(normalized);
447
+
448
+ // Step 3: Add hook to marketplace
449
+ this.admittedHooks.set(normalized.id, normalized);
450
+
451
+ // Step 4: Record violations
452
+ if (validation.violations.length > 0) {
453
+ const violationTriples = this._recordViolationsAsRDF(
454
+ normalized.hookUri,
455
+ validation.violations
456
+ );
457
+ this.violations.set(normalized.id, {
458
+ violations: validation.violations,
459
+ triples: violationTriples,
460
+ });
461
+ }
462
+
463
+ // Step 5: Store all triples in RDF store
464
+ normalized.triples.forEach(triple => this.store.add(triple));
465
+
466
+ return {
467
+ admitted: true,
468
+ hookId: normalized.id,
469
+ hookUri: normalized.hookUri,
470
+ violations: validation.violations,
471
+ violationCount: validation.violationCount,
472
+ };
473
+ } catch (error) {
474
+ return {
475
+ admitted: false,
476
+ error: error.message,
477
+ hookId: hookDef.id,
478
+ };
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Admit multiple hooks and resolve their dependencies
484
+ *
485
+ * @param {array} hookDefs - Array of hook definitions
486
+ * @returns {object} Batch admission result with { admitted, rejected, cycles, dependencyGraph }
487
+ */
488
+ admitHooksWithDependencies(hookDefs) {
489
+ const admitted = [];
490
+ const rejected = [];
491
+ const hooksByUri = new Map();
492
+ const hookIdToUri = new Map(); // Map hook IDs to URIs for dependency resolution
493
+
494
+ // Admit all hooks individually
495
+ for (const hookDef of hookDefs) {
496
+ const result = this.admitHook(hookDef);
497
+ if (result.admitted) {
498
+ admitted.push(result);
499
+ hookIdToUri.set(hookDef.id, result.hookUri);
500
+ // Convert dependency IDs to URIs
501
+ const depUris = (hookDef.dependsOn || []).map(id => `${HOOK_NS.hook}${id}`);
502
+ hooksByUri.set(result.hookUri, {
503
+ dependsOn: depUris,
504
+ });
505
+ } else {
506
+ rejected.push(result);
507
+ }
508
+ }
509
+
510
+ // Resolve dependencies
511
+ const depResult = this.resolveDependenciesViaRules(hooksByUri);
512
+
513
+ // Filter out hooks with cycles (they're rejected post-admission)
514
+ const cycleHookIds = Array.from(depResult.cycles).map(uri => {
515
+ const id = uri.replace(`${HOOK_NS.hook}`, '');
516
+ return id;
517
+ });
518
+
519
+ const admittedWithoutCycles = admitted.filter(h => !cycleHookIds.includes(h.hookId));
520
+ const cycleHooks = admitted.filter(h => cycleHookIds.includes(h.hookId));
521
+
522
+ return {
523
+ admittedCount: admittedWithoutCycles.length,
524
+ rejectedCount: rejected.length + cycleHooks.length,
525
+ admitted: admittedWithoutCycles,
526
+ rejected: [
527
+ ...rejected,
528
+ ...cycleHooks.map(h => ({
529
+ ...h,
530
+ error: 'Circular dependency detected',
531
+ })),
532
+ ],
533
+ cycles: Array.from(depResult.cycles),
534
+ dependencyGraph: Object.fromEntries(depResult.allDeps),
535
+ hadCycles: depResult.hadCycles,
536
+ };
537
+ }
538
+
539
+ /**
540
+ * Query marketplace for hooks matching criteria
541
+ *
542
+ * @param {string} sparqlQuery - SPARQL query
543
+ * @returns {array} Query results
544
+ */
545
+ query(sparqlQuery) {
546
+ try {
547
+ const results = this.store.query(sparqlQuery);
548
+ return Array.from(results).map(binding => {
549
+ const result = {};
550
+ for (const [key, value] of binding) {
551
+ result[key] = value.value || value;
552
+ }
553
+ return result;
554
+ });
555
+ } catch (error) {
556
+ throw new Error(`SPARQL query failed: ${error.message}`);
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Get all admitted hooks
562
+ *
563
+ * @returns {array} Array of admitted hooks with their URIs
564
+ */
565
+ getAdmittedHooks() {
566
+ return Array.from(this.admittedHooks.values()).map(hook => ({
567
+ id: hook.id,
568
+ name: hook.name,
569
+ version: hook.version,
570
+ uri: hook.hookUri,
571
+ priority: hook.priority,
572
+ dependsOn: hook.dependsOn,
573
+ }));
574
+ }
575
+
576
+ /**
577
+ * Get violations for a specific hook
578
+ *
579
+ * @param {string} hookId - Hook ID
580
+ * @returns {object|null} Violations object or null if none
581
+ */
582
+ getViolations(hookId) {
583
+ return this.violations.get(hookId) || null;
584
+ }
585
+
586
+ /**
587
+ * Helper: Create RDF literal triple
588
+ * @private
589
+ */
590
+ _createLiteral(subject, predicate, value, datatype) {
591
+ return {
592
+ subject: { termType: 'NamedNode', value: subject },
593
+ predicate: { termType: 'NamedNode', value: predicate },
594
+ object: {
595
+ termType: 'Literal',
596
+ value: String(value),
597
+ datatype: { termType: 'NamedNode', value: datatype || `${HOOK_NS.xsd}string` },
598
+ },
599
+ };
600
+ }
601
+
602
+ /**
603
+ * Helper: Create RDF resource triple
604
+ * @private
605
+ */
606
+ _createResource(subject, predicate, object) {
607
+ return {
608
+ subject: { termType: 'NamedNode', value: subject },
609
+ predicate: { termType: 'NamedNode', value: predicate },
610
+ object: { termType: 'NamedNode', value: object },
611
+ };
612
+ }
613
+ }
614
+
615
+ export default HooksMarketplace;