@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
@@ -39,13 +39,17 @@ const SandboxConfigSchema = z.object({
39
39
  /**
40
40
  * Schema for sandbox execution context
41
41
  */
42
- const SandboxContextSchema = z.object({
43
- event: z.any(),
44
- store: z.any(),
45
- delta: z.any(),
46
- metadata: z.record(z.any()).optional(),
47
- allowedFunctions: z.array(z.string()).default(['emitEvent', 'log', 'assert']),
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: true,
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
- totalEntries: cache.size,
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) => (hasTransformation(h) ? `quad = hooks[${i}].transform(quad);` : ''))
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
- 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
+ // Execute validation if present - let errors propagate
91
+ if (hasValidation(validatedHook)) {
92
+ const validationResult = validatedHook.validate(quad);
90
93
 
91
- if (!validationResult) {
92
- result.valid = false;
93
- result.error = `Validation failed for hook: ${validatedHook.name}`;
94
- return result;
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
- // Execute transformation if present
99
- if (hasTransformation(validatedHook)) {
100
- const transformed = validatedHook.transform(quad);
102
+ if (!validationResult) {
103
+ result.valid = false;
104
+ result.error = `Validation failed for hook: ${validatedHook.name}`;
105
+ return result;
106
+ }
107
+ }
101
108
 
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
- }
109
+ // Execute transformation if present - let errors propagate
110
+ if (hasTransformation(validatedHook)) {
111
+ const transformed = validatedHook.transform(quad);
108
112
 
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
- }
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
- // 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
- }
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
- result.quad = transformed;
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
- 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;
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
- // Fast path: trust pre-validated hooks (skip Zod array parse)
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[]} hooks - All registered hooks
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 {ChainResult} - Execution result
213
+ * @returns {HookResult[]} - Array of hook execution results
200
214
  *
201
215
  * @example
202
- * const result = executeHooksByTrigger(allHooks, 'before-add', quad);
216
+ * const results = executeHooksByTrigger(registry, 'before-add', quad);
203
217
  */
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);
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 {{ results: ChainResult[], validCount: number, invalidCount: number }}
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;