@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
|
@@ -39,13 +39,17 @@ const SandboxConfigSchema = z.object({
|
|
|
39
39
|
/**
|
|
40
40
|
* Schema for sandbox execution context
|
|
41
41
|
*/
|
|
42
|
-
const SandboxContextSchema = z
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
const SandboxContextSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
event: z.any(),
|
|
45
|
+
store: z.any(),
|
|
46
|
+
delta: z.any(),
|
|
47
|
+
metadata: z.record(z.any()).optional(),
|
|
48
|
+
allowedFunctions: z.array(z.string()).default(['emitEvent', 'log', 'assert']),
|
|
49
|
+
})
|
|
50
|
+
.refine(obj => 'event' in obj && 'store' in obj && 'delta' in obj, {
|
|
51
|
+
message: 'event, store, and delta fields are required',
|
|
52
|
+
});
|
|
49
53
|
|
|
50
54
|
/**
|
|
51
55
|
* Schema for sandbox execution result
|
|
@@ -175,7 +179,7 @@ export class EffectSandbox {
|
|
|
175
179
|
...result,
|
|
176
180
|
executionId,
|
|
177
181
|
duration,
|
|
178
|
-
success:
|
|
182
|
+
success: result?.success !== false,
|
|
179
183
|
};
|
|
180
184
|
} catch (error) {
|
|
181
185
|
const duration = Date.now() - startTime;
|
|
@@ -478,7 +482,13 @@ export class EffectSandbox {
|
|
|
478
482
|
|
|
479
483
|
const terminationPromises = Array.from(this.workers.values()).map(worker => {
|
|
480
484
|
return new Promise(resolve => {
|
|
481
|
-
worker.terminate
|
|
485
|
+
if (worker?.terminate && typeof worker.terminate === 'function') {
|
|
486
|
+
try {
|
|
487
|
+
worker.terminate();
|
|
488
|
+
} catch (_e) {
|
|
489
|
+
// Ignore termination errors
|
|
490
|
+
}
|
|
491
|
+
}
|
|
482
492
|
resolve();
|
|
483
493
|
});
|
|
484
494
|
});
|
|
@@ -186,6 +186,10 @@ export async function loadShaclFile(uri, expectedHash, basePath = process.cwd())
|
|
|
186
186
|
* @param {string} [options.basePath] - Base path for file resolution
|
|
187
187
|
* @param {boolean} [options.enableCache] - Enable file content caching
|
|
188
188
|
* @param {number} [options.cacheMaxAge] - Cache max age in milliseconds
|
|
189
|
+
* @param {number} [options.cacheSize] - Maximum cache size
|
|
190
|
+
* @param {boolean} [options.allowAbsolutePaths] - Allow absolute paths (default: true)
|
|
191
|
+
* @param {boolean} [options.enablePathValidation] - Enforce strict path validation
|
|
192
|
+
* @param {Array<string>} [options.allowedExtensions] - Whitelist of allowed extensions
|
|
189
193
|
* @returns {Object} File resolver instance
|
|
190
194
|
*/
|
|
191
195
|
export function createFileResolver(options = {}) {
|
|
@@ -193,11 +197,152 @@ export function createFileResolver(options = {}) {
|
|
|
193
197
|
basePath = process.cwd(),
|
|
194
198
|
enableCache = true,
|
|
195
199
|
cacheMaxAge = 300000, // 5 minutes
|
|
200
|
+
cacheSize = 1000,
|
|
201
|
+
allowAbsolutePaths = true,
|
|
202
|
+
enablePathValidation = false,
|
|
203
|
+
allowedExtensions = null,
|
|
196
204
|
} = options;
|
|
197
205
|
|
|
198
206
|
const cache = new Map();
|
|
207
|
+
const pathResolveCache = new Map(); // Cache for resolve() results
|
|
208
|
+
let cacheHits = 0;
|
|
209
|
+
let cacheMisses = 0;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Ensure cache doesn't exceed size limit by removing oldest entries
|
|
213
|
+
*/
|
|
214
|
+
function enforceMaxCacheSize() {
|
|
215
|
+
if (cache.size > cacheSize) {
|
|
216
|
+
const keysToDelete = Array.from(cache.keys()).slice(0, cache.size - cacheSize);
|
|
217
|
+
keysToDelete.forEach(key => cache.delete(key));
|
|
218
|
+
}
|
|
219
|
+
if (pathResolveCache.size > cacheSize) {
|
|
220
|
+
const keysToDelete = Array.from(pathResolveCache.keys()).slice(
|
|
221
|
+
0,
|
|
222
|
+
pathResolveCache.size - cacheSize
|
|
223
|
+
);
|
|
224
|
+
keysToDelete.forEach(key => pathResolveCache.delete(key));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
199
227
|
|
|
200
228
|
return {
|
|
229
|
+
/**
|
|
230
|
+
* Sanitize a file path to remove dangerous patterns.
|
|
231
|
+
* @param {string} filePath - The file path to sanitize
|
|
232
|
+
* @returns {string} Sanitized file path
|
|
233
|
+
*/
|
|
234
|
+
sanitizePath(filePath) {
|
|
235
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
236
|
+
return '';
|
|
237
|
+
}
|
|
238
|
+
// Remove parent directory traversal attempts
|
|
239
|
+
return filePath
|
|
240
|
+
.split(/[/\\]+/)
|
|
241
|
+
.filter(part => part && part !== '..')
|
|
242
|
+
.join('/');
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Validate hash format.
|
|
247
|
+
* @param {string} hash - The hash string to validate
|
|
248
|
+
* @param {string} algorithm - Hash algorithm ('sha256', 'sha512', etc.)
|
|
249
|
+
* @returns {boolean} True if hash is valid
|
|
250
|
+
*/
|
|
251
|
+
isValidHash(hash, algorithm = 'sha256') {
|
|
252
|
+
if (!hash || typeof hash !== 'string') {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const expectedLength = algorithm === 'sha512' ? 128 : algorithm === 'sha256' ? 64 : 0;
|
|
257
|
+
if (expectedLength === 0 || hash.length !== expectedLength) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check if all characters are valid hex (0-9, a-f, A-F)
|
|
262
|
+
return /^[0-9a-fA-F]+$/.test(hash);
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Resolve a file path relative to basePath.
|
|
267
|
+
* @param {string} filePath - The file path to resolve
|
|
268
|
+
* @returns {string} Absolute file path
|
|
269
|
+
* @throws {Error} If path is invalid or not allowed
|
|
270
|
+
*/
|
|
271
|
+
resolve(filePath) {
|
|
272
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
273
|
+
throw new TypeError('resolve: path must be a non-empty string');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check cache first
|
|
277
|
+
if (enableCache && pathResolveCache.has(filePath)) {
|
|
278
|
+
cacheHits++;
|
|
279
|
+
return pathResolveCache.get(filePath);
|
|
280
|
+
}
|
|
281
|
+
cacheMisses++;
|
|
282
|
+
|
|
283
|
+
// Pass through HTTP/HTTPS URIs unchanged
|
|
284
|
+
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
|
|
285
|
+
if (enableCache) {
|
|
286
|
+
pathResolveCache.set(filePath, filePath);
|
|
287
|
+
enforceMaxCacheSize();
|
|
288
|
+
}
|
|
289
|
+
return filePath;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Pass through file:// URIs with special handling
|
|
293
|
+
if (filePath.startsWith('file://')) {
|
|
294
|
+
if (enableCache) {
|
|
295
|
+
pathResolveCache.set(filePath, filePath);
|
|
296
|
+
enforceMaxCacheSize();
|
|
297
|
+
}
|
|
298
|
+
return filePath;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Reject unknown URI schemes
|
|
302
|
+
if (filePath.includes('://')) {
|
|
303
|
+
throw new Error(`resolve: unsupported URI scheme in ${filePath}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Path traversal validation if enabled
|
|
307
|
+
if (enablePathValidation && filePath.includes('..')) {
|
|
308
|
+
throw new Error(`resolve: path traversal detected: ${filePath}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check for absolute paths - only reject if explicitly disabled
|
|
312
|
+
const isAbsolute = filePath.startsWith('/') || /^[a-zA-Z]:/.test(filePath);
|
|
313
|
+
if (!allowAbsolutePaths && isAbsolute) {
|
|
314
|
+
throw new Error(`resolve: absolute paths are not allowed: ${filePath}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// If absolute path and allowed, return as-is
|
|
318
|
+
if (isAbsolute) {
|
|
319
|
+
const resolved = _resolve(filePath);
|
|
320
|
+
if (enableCache) {
|
|
321
|
+
pathResolveCache.set(filePath, resolved);
|
|
322
|
+
enforceMaxCacheSize();
|
|
323
|
+
}
|
|
324
|
+
return resolved;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Check file extensions if whitelist provided
|
|
328
|
+
if (allowedExtensions && Array.isArray(allowedExtensions)) {
|
|
329
|
+
const ext = filePath.slice(filePath.lastIndexOf('.'));
|
|
330
|
+
if (ext && !allowedExtensions.includes(ext)) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`resolve: file extension '${ext}' is not allowed. Allowed: ${allowedExtensions.join(', ')}`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Resolve relative paths against basePath
|
|
338
|
+
const resolved = _resolve(_join(basePath, filePath));
|
|
339
|
+
if (enableCache) {
|
|
340
|
+
pathResolveCache.set(filePath, resolved);
|
|
341
|
+
enforceMaxCacheSize();
|
|
342
|
+
}
|
|
343
|
+
return resolved;
|
|
344
|
+
},
|
|
345
|
+
|
|
201
346
|
/**
|
|
202
347
|
* Load a file with hash verification.
|
|
203
348
|
* @param {string} uri - The file URI
|
|
@@ -222,6 +367,7 @@ export function createFileResolver(options = {}) {
|
|
|
222
367
|
data,
|
|
223
368
|
timestamp: Date.now(),
|
|
224
369
|
});
|
|
370
|
+
enforceMaxCacheSize();
|
|
225
371
|
}
|
|
226
372
|
|
|
227
373
|
return data;
|
|
@@ -251,6 +397,7 @@ export function createFileResolver(options = {}) {
|
|
|
251
397
|
data,
|
|
252
398
|
timestamp: Date.now(),
|
|
253
399
|
});
|
|
400
|
+
enforceMaxCacheSize();
|
|
254
401
|
}
|
|
255
402
|
|
|
256
403
|
return data;
|
|
@@ -280,6 +427,7 @@ export function createFileResolver(options = {}) {
|
|
|
280
427
|
data,
|
|
281
428
|
timestamp: Date.now(),
|
|
282
429
|
});
|
|
430
|
+
enforceMaxCacheSize();
|
|
283
431
|
}
|
|
284
432
|
|
|
285
433
|
return data;
|
|
@@ -290,6 +438,7 @@ export function createFileResolver(options = {}) {
|
|
|
290
438
|
*/
|
|
291
439
|
clearCache() {
|
|
292
440
|
cache.clear();
|
|
441
|
+
pathResolveCache.clear();
|
|
293
442
|
},
|
|
294
443
|
|
|
295
444
|
/**
|
|
@@ -377,10 +526,15 @@ export function createFileResolver(options = {}) {
|
|
|
377
526
|
}
|
|
378
527
|
|
|
379
528
|
return {
|
|
380
|
-
|
|
529
|
+
size: pathResolveCache.size,
|
|
530
|
+
hits: cacheHits,
|
|
531
|
+
misses: cacheMisses,
|
|
532
|
+
totalEntries: cache.size + pathResolveCache.size,
|
|
381
533
|
validEntries,
|
|
382
534
|
expiredEntries,
|
|
383
535
|
cacheMaxAge,
|
|
536
|
+
cacheSize,
|
|
537
|
+
pathCacheSize: pathResolveCache.size,
|
|
384
538
|
};
|
|
385
539
|
},
|
|
386
540
|
};
|
|
@@ -100,10 +100,17 @@ export function compileHookChain(hooks) {
|
|
|
100
100
|
|
|
101
101
|
// Generate inline transformation steps
|
|
102
102
|
const transformSteps = hooks
|
|
103
|
-
.map((h, i) =>
|
|
103
|
+
.map((h, i) => {
|
|
104
|
+
if (hasTransformation(h)) {
|
|
105
|
+
return `quad = hooks[${i}].transform(quad);
|
|
106
|
+
if (quad === null || quad === undefined) return { valid: false, quad };`;
|
|
107
|
+
}
|
|
108
|
+
return '';
|
|
109
|
+
})
|
|
104
110
|
.filter(Boolean)
|
|
105
111
|
.join('\n ');
|
|
106
112
|
|
|
113
|
+
|
|
107
114
|
// Compile the chain function
|
|
108
115
|
const fnBody = `
|
|
109
116
|
${validationSteps}
|
|
@@ -147,6 +154,9 @@ function createInterpretedChain(hooks) {
|
|
|
147
154
|
|
|
148
155
|
if (hasTransformation(hook)) {
|
|
149
156
|
currentQuad = hook.transform(currentQuad);
|
|
157
|
+
if (currentQuad === null || currentQuad === undefined) {
|
|
158
|
+
return { valid: false, quad: currentQuad, failedHook: hook.name };
|
|
159
|
+
}
|
|
150
160
|
}
|
|
151
161
|
}
|
|
152
162
|
|
|
@@ -65,6 +65,18 @@ export const ChainResultSchema = z.object({
|
|
|
65
65
|
* }
|
|
66
66
|
*/
|
|
67
67
|
export function executeHook(hook, quad, options = {}) {
|
|
68
|
+
// Validate input quad
|
|
69
|
+
if (!quad || typeof quad !== 'object') {
|
|
70
|
+
throw new TypeError(
|
|
71
|
+
`Invalid quad provided to executeHook: expected object, got ${typeof quad}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate quad has required properties
|
|
76
|
+
if (!quad.subject || !quad.predicate || !quad.object) {
|
|
77
|
+
throw new TypeError('Quad must have subject, predicate, and object properties');
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
// Fast path: skip Zod if hook was created via defineHook (_validated flag)
|
|
69
81
|
const validatedHook = hook._validated ? hook : HookSchema.parse(hook);
|
|
70
82
|
|
|
@@ -75,71 +87,61 @@ export function executeHook(hook, quad, options = {}) {
|
|
|
75
87
|
hookName: validatedHook.name,
|
|
76
88
|
};
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
// Execute validation if present - let errors propagate
|
|
91
|
+
if (hasValidation(validatedHook)) {
|
|
92
|
+
const validationResult = validatedHook.validate(quad);
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
// POKA-YOKE: Non-boolean validation return guard (RPN 280 → 28)
|
|
95
|
+
if (typeof validationResult !== 'boolean') {
|
|
96
|
+
console.warn(
|
|
97
|
+
`[POKA-YOKE] Hook "${validatedHook.name}": validate() returned ${typeof validationResult}, expected boolean. Coercing to boolean.`
|
|
98
|
+
);
|
|
99
|
+
result.warning = `Non-boolean validation return (${typeof validationResult}) coerced to boolean`;
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
if (!validationResult) {
|
|
103
|
+
result.valid = false;
|
|
104
|
+
result.error = `Validation failed for hook: ${validatedHook.name}`;
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
101
108
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
`Hook "${validatedHook.name}": transform() must return a Quad object, got ${typeof transformed}`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
109
|
+
// Execute transformation if present - let errors propagate
|
|
110
|
+
if (hasTransformation(validatedHook)) {
|
|
111
|
+
const transformed = validatedHook.transform(quad);
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
// POKA-YOKE: Transform return type validation (RPN 280 → 28)
|
|
114
|
+
if (transformed === null || transformed === undefined) {
|
|
115
|
+
result.valid = false;
|
|
116
|
+
result.quad = transformed;
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
result.warning = 'Pooled quad returned - consider cloning';
|
|
122
|
-
}
|
|
120
|
+
if (typeof transformed !== 'object') {
|
|
121
|
+
throw new TypeError(
|
|
122
|
+
`Hook "${validatedHook.name}": transform() must return a Quad object, got ${typeof transformed}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
// POKA-YOKE: Check for required Quad properties
|
|
127
|
+
if (!transformed.subject || !transformed.predicate || !transformed.object) {
|
|
128
|
+
throw new TypeError(
|
|
129
|
+
`Hook "${validatedHook.name}": transform() returned object missing subject/predicate/object`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// POKA-YOKE: Pooled quad leak detection (warn if returning pooled quad)
|
|
134
|
+
if (transformed._pooled && options.warnPooledQuads !== false) {
|
|
135
|
+
console.warn(
|
|
136
|
+
`[POKA-YOKE] Hook "${validatedHook.name}": returned pooled quad. Clone before storing to prevent memory issues.`
|
|
137
|
+
);
|
|
138
|
+
result.warning = 'Pooled quad returned - consider cloning';
|
|
125
139
|
}
|
|
126
140
|
|
|
127
|
-
|
|
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;
|
|
141
|
+
result.quad = transformed;
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
/**
|
|
@@ -158,7 +160,17 @@ export function executeHook(hook, quad, options = {}) {
|
|
|
158
160
|
* }
|
|
159
161
|
*/
|
|
160
162
|
export function executeHookChain(hooks, quad) {
|
|
161
|
-
//
|
|
163
|
+
// Validate input
|
|
164
|
+
if (!Array.isArray(hooks)) {
|
|
165
|
+
throw new TypeError('hooks must be an array');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check for null/undefined hooks
|
|
169
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
170
|
+
if (hooks[i] === null || hooks[i] === undefined) {
|
|
171
|
+
throw new Error(`Hook at index ${i} is ${hooks[i]}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
162
174
|
|
|
163
175
|
/** @type {HookResult[]} */
|
|
164
176
|
const results = [];
|
|
@@ -176,7 +188,7 @@ export function executeHookChain(hooks, quad) {
|
|
|
176
188
|
break;
|
|
177
189
|
}
|
|
178
190
|
|
|
179
|
-
if (result.quad) {
|
|
191
|
+
if (result.quad !== undefined) {
|
|
180
192
|
currentQuad = result.quad;
|
|
181
193
|
}
|
|
182
194
|
}
|
|
@@ -187,24 +199,44 @@ export function executeHookChain(hooks, quad) {
|
|
|
187
199
|
quad: currentQuad,
|
|
188
200
|
results,
|
|
189
201
|
error: chainError,
|
|
202
|
+
failedHook: !chainValid ? results[results.length - 1]?.hookName : undefined,
|
|
190
203
|
};
|
|
191
204
|
}
|
|
192
205
|
|
|
193
206
|
/**
|
|
194
207
|
* Execute hooks for a specific trigger type.
|
|
208
|
+
* Accepts either a registry or an array of hooks.
|
|
195
209
|
*
|
|
196
|
-
* @param {Hook[]}
|
|
210
|
+
* @param {HookRegistry|Hook[]} hooksOrRegistry - Hooks array or registry
|
|
197
211
|
* @param {import('./define-hook.mjs').HookTrigger} trigger - Trigger type to execute
|
|
198
212
|
* @param {Quad} quad - Quad to process
|
|
199
|
-
* @returns {
|
|
213
|
+
* @returns {HookResult[]} - Array of hook execution results
|
|
200
214
|
*
|
|
201
215
|
* @example
|
|
202
|
-
* const
|
|
216
|
+
* const results = executeHooksByTrigger(registry, 'before-add', quad);
|
|
203
217
|
*/
|
|
204
|
-
export function executeHooksByTrigger(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
218
|
+
export function executeHooksByTrigger(hooksOrRegistry, trigger, quad) {
|
|
219
|
+
let hooks;
|
|
220
|
+
|
|
221
|
+
// Handle registry object
|
|
222
|
+
if (hooksOrRegistry && typeof hooksOrRegistry === 'object' && hooksOrRegistry.hooks instanceof Map) {
|
|
223
|
+
// Extract hooks for the specific trigger from registry
|
|
224
|
+
const triggerSet = hooksOrRegistry.triggerIndex.get(trigger);
|
|
225
|
+
if (!triggerSet) {
|
|
226
|
+
hooks = [];
|
|
227
|
+
} else {
|
|
228
|
+
hooks = Array.from(triggerSet).map(name => hooksOrRegistry.hooks.get(name));
|
|
229
|
+
}
|
|
230
|
+
} else if (Array.isArray(hooksOrRegistry)) {
|
|
231
|
+
// Direct hooks array
|
|
232
|
+
hooks = hooksOrRegistry.filter(h => h.trigger === trigger);
|
|
233
|
+
} else {
|
|
234
|
+
// Invalid input
|
|
235
|
+
hooks = [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Execute and return the full chain result
|
|
239
|
+
return executeHookChain(hooks, quad);
|
|
208
240
|
}
|
|
209
241
|
|
|
210
242
|
/**
|
|
@@ -267,15 +299,13 @@ export function validateOnly(hooks, quad) {
|
|
|
267
299
|
* @param {Quad[]} quads - Array of quads to process
|
|
268
300
|
* @param {Object} [options] - Batch options
|
|
269
301
|
* @param {boolean} [options.stopOnError=false] - Stop on first error
|
|
270
|
-
* @returns {
|
|
302
|
+
* @returns {ChainResult[]} Array of execution results
|
|
271
303
|
*/
|
|
272
304
|
export function executeBatch(hooks, quads, options = {}) {
|
|
273
305
|
const { stopOnError = false } = options;
|
|
274
306
|
|
|
275
307
|
/** @type {ChainResult[]} */
|
|
276
308
|
const results = [];
|
|
277
|
-
let validCount = 0;
|
|
278
|
-
let invalidCount = 0;
|
|
279
309
|
|
|
280
310
|
// Zod-free hot path - hooks already validated by defineHook
|
|
281
311
|
for (let i = 0; i < quads.length; i++) {
|
|
@@ -314,12 +344,7 @@ export function executeBatch(hooks, quads, options = {}) {
|
|
|
314
344
|
|
|
315
345
|
results.push({ valid: isValid, quad: currentQuad, error, results: [] });
|
|
316
346
|
|
|
317
|
-
if (isValid)
|
|
318
|
-
validCount++;
|
|
319
|
-
} else {
|
|
320
|
-
invalidCount++;
|
|
321
|
-
if (stopOnError) break;
|
|
322
|
-
}
|
|
347
|
+
if (!isValid && stopOnError) break;
|
|
323
348
|
}
|
|
324
349
|
|
|
325
350
|
return results;
|