@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,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;
|