@unrdf/hooks 5.0.1
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 +21 -0
- package/README.md +86 -0
- package/package.json +70 -0
- package/src/hooks/builtin-hooks.mjs +296 -0
- package/src/hooks/condition-cache.mjs +109 -0
- package/src/hooks/condition-evaluator.mjs +722 -0
- package/src/hooks/define-hook.mjs +211 -0
- package/src/hooks/effect-sandbox-worker.mjs +170 -0
- package/src/hooks/effect-sandbox.mjs +517 -0
- package/src/hooks/file-resolver.mjs +387 -0
- package/src/hooks/hook-chain-compiler.mjs +236 -0
- package/src/hooks/hook-executor-batching.mjs +277 -0
- package/src/hooks/hook-executor.mjs +465 -0
- package/src/hooks/hook-management.mjs +202 -0
- package/src/hooks/hook-scheduler.mjs +413 -0
- package/src/hooks/knowledge-hook-engine.mjs +358 -0
- package/src/hooks/knowledge-hook-manager.mjs +269 -0
- package/src/hooks/observability.mjs +531 -0
- package/src/hooks/policy-pack.mjs +572 -0
- package/src/hooks/quad-pool.mjs +249 -0
- package/src/hooks/quality-metrics.mjs +544 -0
- package/src/hooks/security/error-sanitizer.mjs +257 -0
- package/src/hooks/security/path-validator.mjs +194 -0
- package/src/hooks/security/sandbox-restrictions.mjs +331 -0
- package/src/hooks/telemetry.mjs +167 -0
- package/src/index.mjs +101 -0
- package/src/security/sandbox/browser-executor.mjs +220 -0
- package/src/security/sandbox/detector.mjs +342 -0
- package/src/security/sandbox/isolated-vm-executor.mjs +373 -0
- package/src/security/sandbox/vm2-executor.mjs +217 -0
- package/src/security/sandbox/worker-executor-runtime.mjs +74 -0
- package/src/security/sandbox/worker-executor.mjs +212 -0
- package/src/security/sandbox-adapter.mjs +141 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Hook execution utilities for UNRDF Knowledge Hooks.
|
|
3
|
+
* @module hooks/hook-executor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { HookSchema, hasValidation, hasTransformation } from './define-hook.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import('./define-hook.mjs').Hook} Hook
|
|
11
|
+
* @typedef {import('n3').Quad} Quad
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hook execution result.
|
|
16
|
+
* @typedef {Object} HookResult
|
|
17
|
+
* @property {boolean} valid - Whether validation passed
|
|
18
|
+
* @property {Quad} [quad] - Transformed quad (if transformation applied)
|
|
19
|
+
* @property {string} [error] - Error message if validation failed
|
|
20
|
+
* @property {string} hookName - Name of hook that executed
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hook chain execution result.
|
|
25
|
+
* @typedef {Object} ChainResult
|
|
26
|
+
* @property {boolean} valid - Whether all validations passed
|
|
27
|
+
* @property {Quad} quad - Final transformed quad
|
|
28
|
+
* @property {HookResult[]} results - Individual hook results
|
|
29
|
+
* @property {string} [error] - Error message if any validation failed
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/* ========================================================================= */
|
|
33
|
+
/* Zod Schemas */
|
|
34
|
+
/* ========================================================================= */
|
|
35
|
+
|
|
36
|
+
export const HookResultSchema = z.object({
|
|
37
|
+
valid: z.boolean(),
|
|
38
|
+
quad: z.any().optional(),
|
|
39
|
+
error: z.string().optional(),
|
|
40
|
+
hookName: z.string(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const ChainResultSchema = z.object({
|
|
44
|
+
valid: z.boolean(),
|
|
45
|
+
quad: z.any(),
|
|
46
|
+
results: z.array(HookResultSchema),
|
|
47
|
+
error: z.string().optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/* ========================================================================= */
|
|
51
|
+
/* Public API */
|
|
52
|
+
/* ========================================================================= */
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Execute a single hook on a quad.
|
|
56
|
+
*
|
|
57
|
+
* @param {Hook} hook - Hook to execute
|
|
58
|
+
* @param {Quad} quad - Quad to process
|
|
59
|
+
* @returns {HookResult} - Execution result
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const result = executeHook(iriValidator, quad);
|
|
63
|
+
* if (!result.valid) {
|
|
64
|
+
* console.error(result.error);
|
|
65
|
+
* }
|
|
66
|
+
*/
|
|
67
|
+
export function executeHook(hook, quad, options = {}) {
|
|
68
|
+
// Fast path: skip Zod if hook was created via defineHook (_validated flag)
|
|
69
|
+
const validatedHook = hook._validated ? hook : HookSchema.parse(hook);
|
|
70
|
+
|
|
71
|
+
/** @type {HookResult} */
|
|
72
|
+
const result = {
|
|
73
|
+
valid: true,
|
|
74
|
+
quad: quad,
|
|
75
|
+
hookName: validatedHook.name,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Execute validation if present
|
|
80
|
+
if (hasValidation(validatedHook)) {
|
|
81
|
+
const validationResult = validatedHook.validate(quad);
|
|
82
|
+
|
|
83
|
+
// POKA-YOKE: Non-boolean validation return guard (RPN 280 → 28)
|
|
84
|
+
if (typeof validationResult !== 'boolean') {
|
|
85
|
+
console.warn(
|
|
86
|
+
`[POKA-YOKE] Hook "${validatedHook.name}": validate() returned ${typeof validationResult}, expected boolean. Coercing to boolean.`
|
|
87
|
+
);
|
|
88
|
+
result.warning = `Non-boolean validation return (${typeof validationResult}) coerced to boolean`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!validationResult) {
|
|
92
|
+
result.valid = false;
|
|
93
|
+
result.error = `Validation failed for hook: ${validatedHook.name}`;
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Execute transformation if present
|
|
99
|
+
if (hasTransformation(validatedHook)) {
|
|
100
|
+
const transformed = validatedHook.transform(quad);
|
|
101
|
+
|
|
102
|
+
// POKA-YOKE: Transform return type validation (RPN 280 → 28)
|
|
103
|
+
if (!transformed || typeof transformed !== 'object') {
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
`Hook "${validatedHook.name}": transform() must return a Quad object, got ${typeof transformed}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// POKA-YOKE: Check for required Quad properties
|
|
110
|
+
if (!transformed.subject || !transformed.predicate || !transformed.object) {
|
|
111
|
+
throw new TypeError(
|
|
112
|
+
`Hook "${validatedHook.name}": transform() returned object missing subject/predicate/object`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// POKA-YOKE: Pooled quad leak detection (warn if returning pooled quad)
|
|
117
|
+
if (transformed._pooled && options.warnPooledQuads !== false) {
|
|
118
|
+
console.warn(
|
|
119
|
+
`[POKA-YOKE] Hook "${validatedHook.name}": returned pooled quad. Clone before storing to prevent memory issues.`
|
|
120
|
+
);
|
|
121
|
+
result.warning = 'Pooled quad returned - consider cloning';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
result.quad = transformed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
result.valid = false;
|
|
130
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
131
|
+
|
|
132
|
+
// POKA-YOKE: Stack trace preservation (RPN 504 → 50)
|
|
133
|
+
result.errorDetails = {
|
|
134
|
+
hookName: validatedHook.name,
|
|
135
|
+
hookTrigger: validatedHook.trigger,
|
|
136
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
137
|
+
originalError: error instanceof Error ? error : undefined,
|
|
138
|
+
rawError: !(error instanceof Error) ? error : undefined,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Execute multiple hooks in sequence on a quad.
|
|
147
|
+
* Stops at first validation failure.
|
|
148
|
+
* Transformations are chained (output of one becomes input to next).
|
|
149
|
+
*
|
|
150
|
+
* @param {Hook[]} hooks - Array of hooks to execute
|
|
151
|
+
* @param {Quad} quad - Initial quad to process
|
|
152
|
+
* @returns {ChainResult} - Chain execution result
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* const result = executeHookChain([validator, transformer], quad);
|
|
156
|
+
* if (result.valid) {
|
|
157
|
+
* store.add(result.quad);
|
|
158
|
+
* }
|
|
159
|
+
*/
|
|
160
|
+
export function executeHookChain(hooks, quad) {
|
|
161
|
+
// Fast path: trust pre-validated hooks (skip Zod array parse)
|
|
162
|
+
|
|
163
|
+
/** @type {HookResult[]} */
|
|
164
|
+
const results = [];
|
|
165
|
+
let currentQuad = quad;
|
|
166
|
+
let chainValid = true;
|
|
167
|
+
let chainError = undefined;
|
|
168
|
+
|
|
169
|
+
for (const hook of hooks) {
|
|
170
|
+
const result = executeHook(hook, currentQuad);
|
|
171
|
+
results.push(result);
|
|
172
|
+
|
|
173
|
+
if (!result.valid) {
|
|
174
|
+
chainValid = false;
|
|
175
|
+
chainError = result.error;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (result.quad) {
|
|
180
|
+
currentQuad = result.quad;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Fast path: return plain object (skip ChainResultSchema.parse)
|
|
185
|
+
return {
|
|
186
|
+
valid: chainValid,
|
|
187
|
+
quad: currentQuad,
|
|
188
|
+
results,
|
|
189
|
+
error: chainError,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Execute hooks for a specific trigger type.
|
|
195
|
+
*
|
|
196
|
+
* @param {Hook[]} hooks - All registered hooks
|
|
197
|
+
* @param {import('./define-hook.mjs').HookTrigger} trigger - Trigger type to execute
|
|
198
|
+
* @param {Quad} quad - Quad to process
|
|
199
|
+
* @returns {ChainResult} - Execution result
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* const result = executeHooksByTrigger(allHooks, 'before-add', quad);
|
|
203
|
+
*/
|
|
204
|
+
export function executeHooksByTrigger(hooks, trigger, quad) {
|
|
205
|
+
// Fast path: trust pre-validated hooks (skip Zod array parse)
|
|
206
|
+
const matchingHooks = hooks.filter(h => h.trigger === trigger);
|
|
207
|
+
return executeHookChain(matchingHooks, quad);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if hooks would pass for a quad (dry-run validation).
|
|
212
|
+
*
|
|
213
|
+
* @param {Hook[]} hooks - Hooks to check
|
|
214
|
+
* @param {Quad} quad - Quad to validate
|
|
215
|
+
* @returns {boolean} - True if all validations would pass
|
|
216
|
+
*/
|
|
217
|
+
export function wouldPassHooks(hooks, quad) {
|
|
218
|
+
const result = executeHookChain(hooks, quad);
|
|
219
|
+
return result.valid;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* ========================================================================= */
|
|
223
|
+
/* Batch API (High-Performance Bulk Operations) */
|
|
224
|
+
/* Sub-1μs per operation via Zod-free hot path */
|
|
225
|
+
/* ========================================================================= */
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Execute validation only (skip transforms) for faster validation-only checks.
|
|
229
|
+
* Zod-free hot path for sub-1μs execution.
|
|
230
|
+
*
|
|
231
|
+
* @param {Hook[]} hooks - Hooks to execute (must be pre-validated via defineHook)
|
|
232
|
+
* @param {Quad} quad - Quad to validate
|
|
233
|
+
* @returns {HookResult} - Validation result
|
|
234
|
+
*/
|
|
235
|
+
export function validateOnly(hooks, quad) {
|
|
236
|
+
// Skip Zod in hot path - trust pre-validated hooks
|
|
237
|
+
for (const hook of hooks) {
|
|
238
|
+
if (hasValidation(hook)) {
|
|
239
|
+
try {
|
|
240
|
+
if (!hook.validate(quad)) {
|
|
241
|
+
return {
|
|
242
|
+
valid: false,
|
|
243
|
+
quad,
|
|
244
|
+
error: `Validation failed for hook: ${hook.name}`,
|
|
245
|
+
hookName: hook.name,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return {
|
|
250
|
+
valid: false,
|
|
251
|
+
quad,
|
|
252
|
+
error: error instanceof Error ? error.message : String(error),
|
|
253
|
+
hookName: hook.name,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { valid: true, quad, hookName: 'validateOnly' };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Execute hooks in batch for multiple quads.
|
|
264
|
+
* Optimized for bulk operations - Zod-free hot path.
|
|
265
|
+
*
|
|
266
|
+
* @param {Hook[]} hooks - Hooks to execute (must be pre-validated via defineHook)
|
|
267
|
+
* @param {Quad[]} quads - Array of quads to process
|
|
268
|
+
* @param {Object} [options] - Batch options
|
|
269
|
+
* @param {boolean} [options.stopOnError=false] - Stop on first error
|
|
270
|
+
* @returns {{ results: ChainResult[], validCount: number, invalidCount: number }}
|
|
271
|
+
*/
|
|
272
|
+
export function executeBatch(hooks, quads, options = {}) {
|
|
273
|
+
const { stopOnError = false } = options;
|
|
274
|
+
|
|
275
|
+
/** @type {ChainResult[]} */
|
|
276
|
+
const results = [];
|
|
277
|
+
let validCount = 0;
|
|
278
|
+
let invalidCount = 0;
|
|
279
|
+
|
|
280
|
+
// Zod-free hot path - hooks already validated by defineHook
|
|
281
|
+
for (let i = 0; i < quads.length; i++) {
|
|
282
|
+
const quad = quads[i];
|
|
283
|
+
let currentQuad = quad;
|
|
284
|
+
let isValid = true;
|
|
285
|
+
let error;
|
|
286
|
+
|
|
287
|
+
for (const hook of hooks) {
|
|
288
|
+
// Validation check
|
|
289
|
+
if (hasValidation(hook)) {
|
|
290
|
+
try {
|
|
291
|
+
if (!hook.validate(currentQuad)) {
|
|
292
|
+
isValid = false;
|
|
293
|
+
error = `Validation failed: ${hook.name}`;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
isValid = false;
|
|
298
|
+
error = e instanceof Error ? e.message : String(e);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Transform if valid
|
|
304
|
+
if (isValid && hasTransformation(hook)) {
|
|
305
|
+
try {
|
|
306
|
+
currentQuad = hook.transform(currentQuad);
|
|
307
|
+
} catch (e) {
|
|
308
|
+
isValid = false;
|
|
309
|
+
error = e instanceof Error ? e.message : String(e);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
results.push({ valid: isValid, quad: currentQuad, error, results: [] });
|
|
316
|
+
|
|
317
|
+
if (isValid) {
|
|
318
|
+
validCount++;
|
|
319
|
+
} else {
|
|
320
|
+
invalidCount++;
|
|
321
|
+
if (stopOnError) break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { results, validCount, invalidCount };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Validate batch of quads, returning bitmap of valid quads.
|
|
330
|
+
* Hyper-speed: Zod-free hot path, returns Uint8Array directly.
|
|
331
|
+
*
|
|
332
|
+
* @param {Hook[]} hooks - Hooks to execute (must be pre-validated via defineHook)
|
|
333
|
+
* @param {Quad[]} quads - Array of quads to validate
|
|
334
|
+
* @returns {Uint8Array} - Bitmap where 1 = valid, 0 = invalid
|
|
335
|
+
*/
|
|
336
|
+
export function validateBatch(hooks, quads) {
|
|
337
|
+
// Filter validation hooks once (no Zod)
|
|
338
|
+
const validationHooks = hooks.filter(hasValidation);
|
|
339
|
+
|
|
340
|
+
// Use Uint8Array for compact boolean storage - returned directly
|
|
341
|
+
const bitmap = new Uint8Array(quads.length);
|
|
342
|
+
|
|
343
|
+
for (let i = 0; i < quads.length; i++) {
|
|
344
|
+
const quad = quads[i];
|
|
345
|
+
let isValid = true;
|
|
346
|
+
|
|
347
|
+
for (const hook of validationHooks) {
|
|
348
|
+
try {
|
|
349
|
+
if (!hook.validate(quad)) {
|
|
350
|
+
isValid = false;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
isValid = false;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
bitmap[i] = isValid ? 1 : 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return bitmap;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Transform batch of quads.
|
|
367
|
+
* Applies transformation hooks to all quads - Zod-free hot path.
|
|
368
|
+
*
|
|
369
|
+
* @param {Hook[]} hooks - Hooks to execute (must be pre-validated via defineHook)
|
|
370
|
+
* @param {Quad[]} quads - Array of quads to transform
|
|
371
|
+
* @param {Object} [options] - Transform options
|
|
372
|
+
* @param {boolean} [options.validateFirst=true] - Validate before transform
|
|
373
|
+
* @returns {{ transformed: Quad[], errors: Array<{index: number, error: string}> }}
|
|
374
|
+
*/
|
|
375
|
+
export function transformBatch(hooks, quads, options = {}) {
|
|
376
|
+
const { validateFirst = true } = options;
|
|
377
|
+
|
|
378
|
+
/** @type {Quad[]} */
|
|
379
|
+
const transformed = [];
|
|
380
|
+
/** @type {Array<{index: number, error: string}>} */
|
|
381
|
+
const errors = [];
|
|
382
|
+
|
|
383
|
+
for (let i = 0; i < quads.length; i++) {
|
|
384
|
+
let currentQuad = quads[i];
|
|
385
|
+
let hasError = false;
|
|
386
|
+
|
|
387
|
+
for (const hook of hooks) {
|
|
388
|
+
try {
|
|
389
|
+
// Validate first if required
|
|
390
|
+
if (validateFirst && hasValidation(hook)) {
|
|
391
|
+
if (!hook.validate(currentQuad)) {
|
|
392
|
+
errors.push({ index: i, error: `Validation failed: ${hook.name}` });
|
|
393
|
+
hasError = true;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Apply transformation
|
|
399
|
+
if (hasTransformation(hook)) {
|
|
400
|
+
currentQuad = hook.transform(currentQuad);
|
|
401
|
+
}
|
|
402
|
+
} catch (error) {
|
|
403
|
+
errors.push({
|
|
404
|
+
index: i,
|
|
405
|
+
error: error instanceof Error ? error.message : String(error),
|
|
406
|
+
});
|
|
407
|
+
hasError = true;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!hasError) {
|
|
413
|
+
transformed.push(currentQuad);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { transformed, errors };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* ========================================================================= */
|
|
421
|
+
/* Cache Management */
|
|
422
|
+
/* ========================================================================= */
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Hook execution cache for pre-validated hooks.
|
|
426
|
+
* @type {WeakMap<object, boolean>}
|
|
427
|
+
*/
|
|
428
|
+
const hookValidationCache = new WeakMap();
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Clear all hook caches (validation and compiled chains).
|
|
432
|
+
* Call this when hooks are modified or for testing.
|
|
433
|
+
*/
|
|
434
|
+
export function clearHookCaches() {
|
|
435
|
+
// WeakMap auto-clears, but we can signal intent
|
|
436
|
+
// The compiled chain cache is in hook-chain-compiler.mjs
|
|
437
|
+
// This is a no-op for WeakMap but provides consistent API
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Pre-warm hook cache by pre-validating hooks.
|
|
442
|
+
* Call this at startup to avoid first-execution overhead.
|
|
443
|
+
*
|
|
444
|
+
* @param {Hook[]} hooks - Hooks to pre-warm
|
|
445
|
+
* @returns {{ prewarmed: number, errors: string[] }}
|
|
446
|
+
*/
|
|
447
|
+
export function prewarmHookCache(hooks) {
|
|
448
|
+
const errors = [];
|
|
449
|
+
let prewarmed = 0;
|
|
450
|
+
|
|
451
|
+
for (const hook of hooks) {
|
|
452
|
+
try {
|
|
453
|
+
// Validate hook structure
|
|
454
|
+
HookSchema.parse(hook);
|
|
455
|
+
hookValidationCache.set(hook, true);
|
|
456
|
+
prewarmed++;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
errors.push(
|
|
459
|
+
`Hook "${hook?.name || 'unknown'}": ${error instanceof Error ? error.message : String(error)}`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return { prewarmed, errors };
|
|
465
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Hook registry and management utilities for UNRDF Knowledge Hooks.
|
|
3
|
+
* @module hooks/hook-management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { HookSchema } from './define-hook.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import('./define-hook.mjs').Hook} Hook
|
|
11
|
+
* @typedef {import('./define-hook.mjs').HookTrigger} HookTrigger
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hook registry for managing registered hooks.
|
|
16
|
+
* @typedef {Object} HookRegistry
|
|
17
|
+
* @property {Map<string, Hook>} hooks - Map of hook name to hook
|
|
18
|
+
* @property {Map<HookTrigger, Set<string>>} triggerIndex - Index of trigger to hook names
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/* ========================================================================= */
|
|
22
|
+
/* Zod Schemas */
|
|
23
|
+
/* ========================================================================= */
|
|
24
|
+
|
|
25
|
+
export const HookRegistrySchema = z.object({
|
|
26
|
+
hooks: z.instanceof(Map),
|
|
27
|
+
triggerIndex: z.instanceof(Map),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/* ========================================================================= */
|
|
31
|
+
/* Public API */
|
|
32
|
+
/* ========================================================================= */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a new hook registry.
|
|
36
|
+
*
|
|
37
|
+
* @returns {HookRegistry} - New empty registry
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const registry = createHookRegistry();
|
|
41
|
+
* registerHook(registry, myHook);
|
|
42
|
+
*/
|
|
43
|
+
export function createHookRegistry() {
|
|
44
|
+
return {
|
|
45
|
+
hooks: new Map(),
|
|
46
|
+
triggerIndex: new Map(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register a hook in the registry.
|
|
52
|
+
*
|
|
53
|
+
* @param {HookRegistry} registry - Hook registry
|
|
54
|
+
* @param {Hook} hook - Hook to register
|
|
55
|
+
* @throws {Error} - If hook with same name already exists
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* registerHook(registry, defineHook({
|
|
59
|
+
* name: 'validate-iri',
|
|
60
|
+
* trigger: 'before-add',
|
|
61
|
+
* validate: (quad) => quad.subject.termType === 'NamedNode'
|
|
62
|
+
* }));
|
|
63
|
+
*/
|
|
64
|
+
export function registerHook(registry, hook) {
|
|
65
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
66
|
+
const validatedHook = HookSchema.parse(hook);
|
|
67
|
+
|
|
68
|
+
if (validatedRegistry.hooks.has(validatedHook.name)) {
|
|
69
|
+
throw new Error(`Hook already registered: ${validatedHook.name}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
validatedRegistry.hooks.set(validatedHook.name, validatedHook);
|
|
73
|
+
|
|
74
|
+
if (!validatedRegistry.triggerIndex.has(validatedHook.trigger)) {
|
|
75
|
+
validatedRegistry.triggerIndex.set(validatedHook.trigger, new Set());
|
|
76
|
+
}
|
|
77
|
+
validatedRegistry.triggerIndex.get(validatedHook.trigger).add(validatedHook.name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Unregister a hook from the registry.
|
|
82
|
+
*
|
|
83
|
+
* @param {HookRegistry} registry - Hook registry
|
|
84
|
+
* @param {string} name - Hook name to remove
|
|
85
|
+
* @returns {boolean} - True if hook was removed, false if not found
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* unregisterHook(registry, 'validate-iri');
|
|
89
|
+
*/
|
|
90
|
+
export function unregisterHook(registry, name) {
|
|
91
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
92
|
+
|
|
93
|
+
const hook = validatedRegistry.hooks.get(name);
|
|
94
|
+
if (!hook) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
validatedRegistry.hooks.delete(name);
|
|
99
|
+
|
|
100
|
+
const triggerSet = validatedRegistry.triggerIndex.get(hook.trigger);
|
|
101
|
+
if (triggerSet) {
|
|
102
|
+
triggerSet.delete(name);
|
|
103
|
+
if (triggerSet.size === 0) {
|
|
104
|
+
validatedRegistry.triggerIndex.delete(hook.trigger);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get a hook by name.
|
|
113
|
+
*
|
|
114
|
+
* @param {HookRegistry} registry - Hook registry
|
|
115
|
+
* @param {string} name - Hook name
|
|
116
|
+
* @returns {Hook | undefined} - Hook if found, undefined otherwise
|
|
117
|
+
*/
|
|
118
|
+
export function getHook(registry, name) {
|
|
119
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
120
|
+
return validatedRegistry.hooks.get(name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* List all registered hooks.
|
|
125
|
+
*
|
|
126
|
+
* @param {HookRegistry} registry - Hook registry
|
|
127
|
+
* @returns {Hook[]} - Array of all registered hooks
|
|
128
|
+
*/
|
|
129
|
+
export function listHooks(registry) {
|
|
130
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
131
|
+
return Array.from(validatedRegistry.hooks.values());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get hooks by trigger type.
|
|
136
|
+
*
|
|
137
|
+
* @param {HookRegistry} registry - Hook registry
|
|
138
|
+
* @param {HookTrigger} trigger - Trigger type
|
|
139
|
+
* @returns {Hook[]} - Array of hooks for this trigger
|
|
140
|
+
*/
|
|
141
|
+
export function getHooksByTrigger(registry, trigger) {
|
|
142
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
143
|
+
|
|
144
|
+
const hookNames = validatedRegistry.triggerIndex.get(trigger);
|
|
145
|
+
if (!hookNames) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hooks = [];
|
|
150
|
+
for (const name of hookNames) {
|
|
151
|
+
const hook = validatedRegistry.hooks.get(name);
|
|
152
|
+
if (hook) {
|
|
153
|
+
hooks.push(hook);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return hooks;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a hook is registered.
|
|
161
|
+
*
|
|
162
|
+
* @param {HookRegistry} registry - Hook registry
|
|
163
|
+
* @param {string} name - Hook name
|
|
164
|
+
* @returns {boolean} - True if hook is registered
|
|
165
|
+
*/
|
|
166
|
+
export function hasHook(registry, name) {
|
|
167
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
168
|
+
return validatedRegistry.hooks.has(name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Clear all hooks from registry.
|
|
173
|
+
*
|
|
174
|
+
* @param {HookRegistry} registry - Hook registry
|
|
175
|
+
*/
|
|
176
|
+
export function clearHooks(registry) {
|
|
177
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
178
|
+
validatedRegistry.hooks.clear();
|
|
179
|
+
validatedRegistry.triggerIndex.clear();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get registry statistics.
|
|
184
|
+
*
|
|
185
|
+
* @param {HookRegistry} registry - Hook registry
|
|
186
|
+
* @returns {Object} - Registry statistics
|
|
187
|
+
* @property {number} totalHooks - Total number of registered hooks
|
|
188
|
+
* @property {Record<string, number>} byTrigger - Count of hooks by trigger type
|
|
189
|
+
*/
|
|
190
|
+
export function getRegistryStats(registry) {
|
|
191
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
192
|
+
|
|
193
|
+
const byTrigger = {};
|
|
194
|
+
for (const [trigger, names] of validatedRegistry.triggerIndex) {
|
|
195
|
+
byTrigger[trigger] = names.size;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
totalHooks: validatedRegistry.hooks.size,
|
|
200
|
+
byTrigger,
|
|
201
|
+
};
|
|
202
|
+
}
|