@unrdf/knowledge-engine 5.0.1 → 26.4.2

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 (71) hide show
  1. package/package.json +23 -17
  2. package/src/ai-enhanced-search.mjs +371 -0
  3. package/src/anomaly-detector.mjs +226 -0
  4. package/src/artifact-generator.mjs +252 -0
  5. package/src/browser.mjs +1 -1
  6. package/src/chatman/disruption-arithmetic.mjs +140 -0
  7. package/src/chatman/market-dynamics.mjs +140 -0
  8. package/src/chatman/organizational-dynamics.mjs +140 -0
  9. package/src/chatman/strategic-dynamics.mjs +140 -0
  10. package/src/chatman-config-loader.mjs +282 -0
  11. package/src/chatman-engine.mjs +435 -0
  12. package/src/chatman-operator.mjs +343 -0
  13. package/src/dark-field-detector.mjs +332 -0
  14. package/src/formation-theorems.mjs +345 -0
  15. package/src/index.mjs +20 -2
  16. package/src/knowledge-hook-manager.mjs +1 -1
  17. package/src/lockchain-writer-browser.mjs +2 -2
  18. package/src/observability.mjs +40 -4
  19. package/src/query-optimizer.mjs +1 -1
  20. package/src/resolution-layer.mjs +1 -1
  21. package/src/transaction.mjs +11 -9
  22. package/README.md +0 -84
  23. package/src/browser-shims.mjs +0 -343
  24. package/src/canonicalize.mjs +0 -414
  25. package/src/condition-cache.mjs +0 -109
  26. package/src/condition-evaluator.mjs +0 -722
  27. package/src/dark-matter-core.mjs +0 -742
  28. package/src/define-hook.mjs +0 -213
  29. package/src/effect-sandbox-browser.mjs +0 -283
  30. package/src/effect-sandbox-worker.mjs +0 -170
  31. package/src/effect-sandbox.mjs +0 -517
  32. package/src/engines/index.mjs +0 -11
  33. package/src/engines/rdf-engine.mjs +0 -299
  34. package/src/file-resolver.mjs +0 -387
  35. package/src/hook-executor-batching.mjs +0 -277
  36. package/src/hook-executor.mjs +0 -870
  37. package/src/hook-management.mjs +0 -150
  38. package/src/ken-parliment.mjs +0 -119
  39. package/src/ken.mjs +0 -149
  40. package/src/knowledge-engine/builtin-rules.mjs +0 -190
  41. package/src/knowledge-engine/inference-engine.mjs +0 -418
  42. package/src/knowledge-engine/knowledge-engine.mjs +0 -317
  43. package/src/knowledge-engine/pattern-dsl.mjs +0 -142
  44. package/src/knowledge-engine/pattern-matcher.mjs +0 -215
  45. package/src/knowledge-engine/rules.mjs +0 -184
  46. package/src/knowledge-engine.mjs +0 -319
  47. package/src/knowledge-hook-engine.mjs +0 -360
  48. package/src/knowledge-substrate-core.mjs +0 -927
  49. package/src/lite.mjs +0 -222
  50. package/src/lockchain-writer.mjs +0 -602
  51. package/src/monitoring/andon-signals.mjs +0 -775
  52. package/src/parse.mjs +0 -290
  53. package/src/performance-optimizer.mjs +0 -678
  54. package/src/policy-pack.mjs +0 -572
  55. package/src/query-cache.mjs +0 -116
  56. package/src/query.mjs +0 -306
  57. package/src/reason.mjs +0 -350
  58. package/src/schemas.mjs +0 -1063
  59. package/src/security/error-sanitizer.mjs +0 -257
  60. package/src/security/path-validator.mjs +0 -194
  61. package/src/security/sandbox-restrictions.mjs +0 -331
  62. package/src/security-validator.mjs +0 -389
  63. package/src/store-cache.mjs +0 -137
  64. package/src/telemetry.mjs +0 -167
  65. package/src/utils/adaptive-monitor.mjs +0 -746
  66. package/src/utils/circuit-breaker.mjs +0 -513
  67. package/src/utils/edge-case-handler.mjs +0 -503
  68. package/src/utils/memory-manager.mjs +0 -498
  69. package/src/utils/ring-buffer.mjs +0 -282
  70. package/src/validate.mjs +0 -319
  71. package/src/validators/index.mjs +0 -338
@@ -1,870 +0,0 @@
1
- /**
2
- * @file Production hook execution engine.
3
- * @module hook-executor
4
- *
5
- * @description
6
- * Production-ready hook execution engine that evaluates conditions,
7
- * executes hook lifecycles, and integrates with the knowledge engine.
8
- */
9
-
10
- import { createConditionEvaluator } from './condition-evaluator.mjs';
11
- import { createEffectSandbox } from './effect-sandbox.mjs';
12
- import { createErrorSanitizer } from './security/error-sanitizer.mjs';
13
- import { createSandboxRestrictions } from './security/sandbox-restrictions.mjs';
14
- import { createStore } from '@unrdf/oxigraph';
15
- import { trace, SpanStatusCode } from '@opentelemetry/api';
16
-
17
- const tracer = trace.getTracer('unrdf');
18
-
19
- /**
20
- * Execute a knowledge hook with full lifecycle management.
21
- * @param {Object} hook - The hook definition
22
- * @param {Object} event - The hook event
23
- * @param {Object} [options] - Execution options
24
- * @returns {Promise<Object>} Hook execution result
25
- *
26
- * @throws {Error} If hook execution fails
27
- */
28
- export async function executeHook(hook, event, options = {}) {
29
- if (!hook || typeof hook !== 'object') {
30
- throw new TypeError('executeHook: hook must be an object');
31
- }
32
- if (!event || typeof event !== 'object') {
33
- throw new TypeError('executeHook: event must be an object');
34
- }
35
-
36
- const {
37
- basePath = process.cwd(),
38
- strictMode = false,
39
- timeoutMs = 30000,
40
- enableConditionEvaluation = true,
41
- enableSandboxing = true,
42
- sandboxConfig = {},
43
- } = options;
44
-
45
- const startTime = Date.now();
46
- const executionId = `hook-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
47
-
48
- try {
49
- // Set up timeout
50
- const timeoutPromise = new Promise((_, reject) => {
51
- setTimeout(() => reject(new Error(`Hook execution timeout after ${timeoutMs}ms`)), timeoutMs);
52
- });
53
-
54
- const executionPromise = _executeHookLifecycle(hook, event, {
55
- basePath,
56
- strictMode,
57
- enableConditionEvaluation,
58
- enableSandboxing,
59
- sandboxConfig,
60
- executionId,
61
- });
62
-
63
- const result = await Promise.race([executionPromise, timeoutPromise]);
64
-
65
- return {
66
- ...result,
67
- executionId,
68
- durationMs: Date.now() - startTime,
69
- success: true,
70
- };
71
- } catch (error) {
72
- // Sanitize error message to prevent information disclosure
73
- const errorSanitizer = createErrorSanitizer();
74
- const sanitizedError = errorSanitizer.sanitize(error);
75
-
76
- return {
77
- executionId,
78
- durationMs: Date.now() - startTime,
79
- success: false,
80
- error: sanitizedError,
81
- cancelled: false,
82
- };
83
- }
84
- }
85
-
86
- /**
87
- * Execute the complete hook lifecycle.
88
- * @param {Object} hook - The hook definition
89
- * @param {Object} event - The hook event
90
- * @param {Object} options - Execution options
91
- * @returns {Promise<Object>} Lifecycle execution result
92
- */
93
- async function _executeHookLifecycle(hook, event, options) {
94
- const {
95
- _basePath,
96
- strictMode,
97
- _enableConditionEvaluation,
98
- enableSandboxing,
99
- _sandboxConfig,
100
- executionId,
101
- } = options;
102
-
103
- return tracer.startActiveSpan('hook.evaluate', async span => {
104
- try {
105
- span.setAttributes({
106
- 'hook.execution_id': executionId,
107
- 'hook.has_before': !!hook.before,
108
- 'hook.has_when': !!hook.when,
109
- 'hook.has_run': !!hook.run,
110
- 'hook.has_after': !!hook.after,
111
- 'hook.strict_mode': strictMode,
112
- 'hook.sandboxing_enabled': enableSandboxing,
113
- });
114
-
115
- let currentEvent = { ...event };
116
- let beforeResult = null;
117
- let runResult = null;
118
- let afterResult = null;
119
- let conditionResult = null;
120
- let cancelled = false;
121
- let cancelReason = null;
122
-
123
- const result = await _executeHookPhases(
124
- hook,
125
- currentEvent,
126
- beforeResult,
127
- runResult,
128
- afterResult,
129
- conditionResult,
130
- cancelled,
131
- cancelReason,
132
- options
133
- );
134
-
135
- span.setAttributes({
136
- 'hook.cancelled': result.cancelled || false,
137
- 'hook.success': result.success || false,
138
- 'hook.phase': result.phase || 'unknown',
139
- });
140
-
141
- span.setStatus({ code: SpanStatusCode.OK });
142
- return result;
143
- } catch (error) {
144
- span.recordException(error);
145
- span.setStatus({
146
- code: SpanStatusCode.ERROR,
147
- message: error.message,
148
- });
149
- throw error;
150
- } finally {
151
- span.end();
152
- }
153
- });
154
- }
155
-
156
- async function _executeHookPhases(
157
- hook,
158
- currentEvent,
159
- beforeResult,
160
- runResult,
161
- afterResult,
162
- conditionResult,
163
- cancelled,
164
- cancelReason,
165
- options
166
- ) {
167
- const {
168
- basePath,
169
- strictMode,
170
- enableConditionEvaluation,
171
- enableSandboxing,
172
- sandboxConfig,
173
- _executionId,
174
- } = options;
175
-
176
- // Phase 1: Before
177
- if (hook.before) {
178
- try {
179
- if (enableSandboxing) {
180
- const sandbox = createEffectSandbox(sandboxConfig);
181
- beforeResult = await sandbox.executeEffect(hook.before, {
182
- event: currentEvent,
183
- store: currentEvent.context?.graph,
184
- delta: currentEvent.payload?.delta,
185
- metadata: currentEvent.context?.metadata || {},
186
- });
187
- } else {
188
- beforeResult = await hook.before(currentEvent);
189
- }
190
-
191
- if (beforeResult && beforeResult.cancel) {
192
- cancelled = true;
193
- cancelReason = beforeResult.reason || 'Cancelled in before phase';
194
- return {
195
- beforeResult,
196
- cancelled: true,
197
- cancelReason,
198
- phase: 'before',
199
- success: false,
200
- };
201
- }
202
-
203
- // Merge before result into event payload
204
- if (beforeResult && typeof beforeResult === 'object' && !beforeResult.cancel) {
205
- currentEvent = {
206
- ...currentEvent,
207
- payload: { ...currentEvent.payload, ...beforeResult },
208
- };
209
- }
210
- } catch (error) {
211
- // Sanitize error message
212
- const errorSanitizer = createErrorSanitizer();
213
- const sanitizedError = errorSanitizer.sanitize(error);
214
-
215
- if (strictMode) {
216
- throw new Error(`Before phase failed: ${sanitizedError}`);
217
- }
218
- return {
219
- beforeResult: { error: sanitizedError },
220
- cancelled: true,
221
- cancelReason: `Before phase error: ${sanitizedError}`,
222
- phase: 'before',
223
- };
224
- }
225
- }
226
-
227
- // Phase 2: Condition Evaluation
228
- if (enableConditionEvaluation && hook.when) {
229
- try {
230
- const evaluator = createConditionEvaluator({ basePath, strictMode });
231
- conditionResult = await evaluator.evaluate(
232
- hook.when,
233
- currentEvent.context?.graph || createStore(),
234
- currentEvent.context?.env || {}
235
- );
236
-
237
- // Check if condition is satisfied
238
- const isSatisfied = await evaluator.isSatisfied(
239
- hook.when,
240
- currentEvent.context?.graph || createStore(),
241
- currentEvent.context?.env || {}
242
- );
243
-
244
- if (!isSatisfied) {
245
- return {
246
- beforeResult,
247
- conditionResult,
248
- cancelled: true,
249
- cancelReason: 'Condition not satisfied',
250
- phase: 'condition',
251
- };
252
- }
253
- } catch (error) {
254
- // Sanitize error message
255
- const errorSanitizer = createErrorSanitizer();
256
- const sanitizedError = errorSanitizer.sanitize(error);
257
-
258
- if (strictMode) {
259
- throw new Error(`Condition evaluation failed: ${sanitizedError}`);
260
- }
261
- return {
262
- beforeResult,
263
- conditionResult: { error: sanitizedError },
264
- cancelled: true,
265
- cancelReason: `Condition evaluation error: ${sanitizedError}`,
266
- phase: 'condition',
267
- success: false,
268
- };
269
- }
270
- }
271
-
272
- // Phase 3: Run
273
- if (hook.run) {
274
- try {
275
- if (enableSandboxing) {
276
- // Use security sandbox with restrictions
277
- const sandboxRestrictions = createSandboxRestrictions(sandboxConfig);
278
- runResult = await sandboxRestrictions.executeRestricted(hook.run, currentEvent);
279
- } else {
280
- runResult = await hook.run(currentEvent);
281
- }
282
- } catch (error) {
283
- // Sanitize error message
284
- const errorSanitizer = createErrorSanitizer();
285
- const sanitizedError = errorSanitizer.sanitize(error);
286
-
287
- if (strictMode) {
288
- throw new Error(`Run phase failed: ${sanitizedError}`);
289
- }
290
- return {
291
- beforeResult,
292
- conditionResult,
293
- runResult: { error: sanitizedError },
294
- cancelled: true,
295
- cancelReason: `Run phase error: ${sanitizedError}`,
296
- phase: 'run',
297
- success: false,
298
- };
299
- }
300
- }
301
-
302
- // Phase 4: After
303
- if (hook.after) {
304
- try {
305
- if (enableSandboxing) {
306
- const sandbox = createEffectSandbox(sandboxConfig);
307
- afterResult = await sandbox.executeEffect(hook.after, {
308
- event: {
309
- ...currentEvent,
310
- result: runResult,
311
- cancelled: false,
312
- },
313
- store: currentEvent.context?.graph,
314
- delta: currentEvent.payload?.delta,
315
- metadata: currentEvent.context?.metadata || {},
316
- });
317
- } else {
318
- afterResult = await hook.after({
319
- ...currentEvent,
320
- result: runResult,
321
- cancelled: false,
322
- });
323
- }
324
- } catch (error) {
325
- // Sanitize error message
326
- const errorSanitizer = createErrorSanitizer();
327
- const sanitizedError = errorSanitizer.sanitize(error);
328
-
329
- if (strictMode) {
330
- throw new Error(`After phase failed: ${sanitizedError}`);
331
- }
332
- // After phase errors don't cancel the hook, just log them
333
- afterResult = { error: sanitizedError };
334
- }
335
- }
336
-
337
- const finalResult = {
338
- beforeResult,
339
- conditionResult,
340
- runResult,
341
- afterResult,
342
- cancelled: false,
343
- phase: 'completed',
344
- success: !cancelled && (!runResult || !runResult.error),
345
- };
346
-
347
- // Record hook result span
348
- return tracer.startActiveSpan('hook.result', resultSpan => {
349
- try {
350
- resultSpan.setAttributes({
351
- 'hook.result.success': finalResult.success,
352
- 'hook.result.cancelled': finalResult.cancelled,
353
- 'hook.result.phase': finalResult.phase,
354
- 'hook.result.has_before': !!beforeResult,
355
- 'hook.result.has_condition': !!conditionResult,
356
- 'hook.result.has_run': !!runResult,
357
- 'hook.result.has_after': !!afterResult,
358
- });
359
-
360
- resultSpan.setStatus({ code: SpanStatusCode.OK });
361
- resultSpan.end();
362
- return finalResult;
363
- } catch (error) {
364
- resultSpan.recordException(error);
365
- resultSpan.setStatus({
366
- code: SpanStatusCode.ERROR,
367
- message: error.message,
368
- });
369
- resultSpan.end();
370
- throw error;
371
- }
372
- });
373
- }
374
-
375
- /**
376
- * Circuit breaker states
377
- * @enum {string}
378
- */
379
- const CircuitBreakerState = {
380
- CLOSED: 'closed',
381
- OPEN: 'open',
382
- HALF_OPEN: 'half-open',
383
- };
384
-
385
- /**
386
- * Create a hook executor with advanced features.
387
- * @param {Object} [options] - Executor options
388
- * @param {string} [options.basePath] - Base path for file resolution
389
- * @param {boolean} [options.strictMode] - Enable strict error handling
390
- * @param {number} [options.timeoutMs] - Default timeout in milliseconds
391
- * @param {number} [options.totalBudgetMs] - Total timeout budget for batch execution (prevents N hooks * 30s cascade)
392
- * @param {boolean} [options.enableConditionEvaluation] - Enable condition evaluation
393
- * @param {boolean} [options.enableMetrics] - Enable execution metrics
394
- * @param {number} [options.circuitBreakerThreshold] - Number of failures before circuit opens
395
- * @param {number} [options.circuitBreakerResetMs] - Time before circuit half-opens
396
- * @returns {Object} Hook executor instance
397
- */
398
- export function createHookExecutor(options = {}) {
399
- const {
400
- basePath = process.cwd(),
401
- strictMode = false,
402
- timeoutMs = 30000,
403
- totalBudgetMs = 60000, // Total budget for batch execution (prevents cascade)
404
- enableConditionEvaluation = true,
405
- enableMetrics = true,
406
- enableSandboxing = true,
407
- sandboxConfig = {},
408
- missingDependencyPolicy = 'warn', // 'error' | 'warn' | 'ignore'
409
- circuitBreakerThreshold = 5, // Open circuit after 5 consecutive failures
410
- circuitBreakerResetMs = 30000, // Try again after 30s
411
- } = options;
412
-
413
- const metrics = {
414
- totalExecutions: 0,
415
- successfulExecutions: 0,
416
- failedExecutions: 0,
417
- cancelledExecutions: 0,
418
- totalDuration: 0,
419
- averageDuration: 0,
420
- executionsByPhase: {
421
- before: 0,
422
- condition: 0,
423
- run: 0,
424
- after: 0,
425
- completed: 0,
426
- },
427
- };
428
-
429
- // Circuit breaker state for timeout cascade prevention
430
- const circuitBreaker = {
431
- state: CircuitBreakerState.CLOSED,
432
- failureCount: 0,
433
- lastFailureTime: 0,
434
- successCount: 0,
435
- };
436
-
437
- return {
438
- /**
439
- * Execute a hook.
440
- * @param {Object} hook - The hook definition
441
- * @param {Object} event - The hook event
442
- * @param {Object} [executionOptions] - Execution-specific options
443
- * @returns {Promise<Object>} Execution result
444
- */
445
- async execute(hook, event, executionOptions = {}) {
446
- const mergedOptions = {
447
- basePath,
448
- strictMode,
449
- timeoutMs,
450
- enableConditionEvaluation,
451
- enableSandboxing,
452
- sandboxConfig,
453
- ...executionOptions,
454
- };
455
-
456
- const result = await executeHook(hook, event, mergedOptions);
457
-
458
- if (enableMetrics) {
459
- this._updateMetrics(result);
460
- }
461
-
462
- return result;
463
- },
464
-
465
- /**
466
- * Execute multiple hooks in parallel with budget tracking.
467
- * Prevents timeout cascade where N hooks * 30s = N*30s total wait.
468
- * @param {Array} hooks - Array of hook definitions
469
- * @param {Object} event - The hook event
470
- * @param {Object} [executionOptions] - Execution-specific options
471
- * @returns {Promise<Object>} Results with succeeded/failed arrays
472
- */
473
- async executeAll(hooks, event, executionOptions = {}) {
474
- if (!Array.isArray(hooks)) {
475
- throw new TypeError('executeAll: hooks must be an array');
476
- }
477
-
478
- // Check circuit breaker before execution
479
- if (this._isCircuitOpen()) {
480
- return {
481
- succeeded: [],
482
- failed: hooks.map((hook, idx) => ({
483
- hookIndex: idx,
484
- hookName: hook?.meta?.name || `hook-${idx}`,
485
- error: 'Circuit breaker open - too many recent failures',
486
- circuitBreakerTripped: true,
487
- })),
488
- circuitBreakerOpen: true,
489
- };
490
- }
491
-
492
- const budgetMs = executionOptions.totalBudgetMs || totalBudgetMs;
493
- const perHookTimeout = Math.min(
494
- executionOptions.timeoutMs || timeoutMs,
495
- Math.floor(budgetMs / Math.max(hooks.length, 1))
496
- );
497
-
498
- const batchStartTime = Date.now();
499
- const succeeded = [];
500
- const failed = [];
501
-
502
- // Use Promise.allSettled to handle partial failures
503
- const promises = hooks.map((hook, idx) => {
504
- // Calculate remaining budget for this hook
505
- const elapsed = Date.now() - batchStartTime;
506
- const remainingBudget = budgetMs - elapsed;
507
-
508
- if (remainingBudget <= 0) {
509
- return Promise.resolve({
510
- status: 'rejected',
511
- reason: new Error('Budget exhausted - hook skipped to prevent cascade'),
512
- });
513
- }
514
-
515
- const hookTimeout = Math.min(perHookTimeout, remainingBudget);
516
-
517
- return this.execute(hook, event, {
518
- ...executionOptions,
519
- timeoutMs: hookTimeout,
520
- })
521
- .then(result => ({
522
- status: 'fulfilled',
523
- value: result,
524
- hookIndex: idx,
525
- }))
526
- .catch(error => ({
527
- status: 'rejected',
528
- reason: error,
529
- hookIndex: idx,
530
- }));
531
- });
532
-
533
- const results = await Promise.allSettled(promises);
534
-
535
- for (let i = 0; i < results.length; i++) {
536
- const result = results[i];
537
- const hook = hooks[i];
538
- const hookName = hook?.meta?.name || `hook-${i}`;
539
-
540
- if (result.status === 'fulfilled' && result.value?.status === 'fulfilled') {
541
- succeeded.push({
542
- hookIndex: i,
543
- hookName,
544
- result: result.value.value,
545
- });
546
- this._recordCircuitSuccess();
547
- } else {
548
- const error =
549
- result.status === 'rejected' ? result.reason : result.value?.reason || 'Unknown error';
550
- failed.push({
551
- hookIndex: i,
552
- hookName,
553
- error: error?.message || String(error),
554
- });
555
- this._recordCircuitFailure();
556
- }
557
- }
558
-
559
- return {
560
- succeeded,
561
- failed,
562
- totalBudgetMs: budgetMs,
563
- actualDurationMs: Date.now() - batchStartTime,
564
- circuitBreakerState: circuitBreaker.state,
565
- };
566
- },
567
-
568
- /**
569
- * Check if circuit breaker is open
570
- * @returns {boolean}
571
- * @private
572
- */
573
- _isCircuitOpen() {
574
- if (circuitBreaker.state === CircuitBreakerState.CLOSED) {
575
- return false;
576
- }
577
-
578
- if (circuitBreaker.state === CircuitBreakerState.OPEN) {
579
- // Check if enough time has passed to try again
580
- const timeSinceFailure = Date.now() - circuitBreaker.lastFailureTime;
581
- if (timeSinceFailure >= circuitBreakerResetMs) {
582
- circuitBreaker.state = CircuitBreakerState.HALF_OPEN;
583
- circuitBreaker.successCount = 0;
584
- return false;
585
- }
586
- return true;
587
- }
588
-
589
- // HALF_OPEN - allow through to test
590
- return false;
591
- },
592
-
593
- /**
594
- * Record a circuit breaker success
595
- * @private
596
- */
597
- _recordCircuitSuccess() {
598
- if (circuitBreaker.state === CircuitBreakerState.HALF_OPEN) {
599
- circuitBreaker.successCount++;
600
- // After 3 successes in half-open, close the circuit
601
- if (circuitBreaker.successCount >= 3) {
602
- circuitBreaker.state = CircuitBreakerState.CLOSED;
603
- circuitBreaker.failureCount = 0;
604
- }
605
- } else {
606
- circuitBreaker.failureCount = 0;
607
- }
608
- },
609
-
610
- /**
611
- * Record a circuit breaker failure
612
- * @private
613
- */
614
- _recordCircuitFailure() {
615
- circuitBreaker.failureCount++;
616
- circuitBreaker.lastFailureTime = Date.now();
617
-
618
- if (circuitBreaker.failureCount >= circuitBreakerThreshold) {
619
- circuitBreaker.state = CircuitBreakerState.OPEN;
620
- }
621
- },
622
-
623
- /**
624
- * Get circuit breaker status
625
- * @returns {Object}
626
- */
627
- getCircuitBreakerStatus() {
628
- return {
629
- state: circuitBreaker.state,
630
- failureCount: circuitBreaker.failureCount,
631
- lastFailureTime: circuitBreaker.lastFailureTime,
632
- threshold: circuitBreakerThreshold,
633
- resetMs: circuitBreakerResetMs,
634
- };
635
- },
636
-
637
- /**
638
- * Reset circuit breaker
639
- */
640
- resetCircuitBreaker() {
641
- circuitBreaker.state = CircuitBreakerState.CLOSED;
642
- circuitBreaker.failureCount = 0;
643
- circuitBreaker.lastFailureTime = 0;
644
- circuitBreaker.successCount = 0;
645
- },
646
-
647
- /**
648
- * Execute hooks sequentially.
649
- * @param {Array} hooks - Array of hook definitions
650
- * @param {Object} event - The hook event
651
- * @param {Object} [executionOptions] - Execution-specific options
652
- * @returns {Promise<Array>} Array of execution results
653
- */
654
- async executeSequential(hooks, event, executionOptions = {}) {
655
- if (!Array.isArray(hooks)) {
656
- throw new TypeError('executeSequential: hooks must be an array');
657
- }
658
-
659
- const results = [];
660
- for (const hook of hooks) {
661
- const result = await this.execute(hook, event, executionOptions);
662
- results.push(result);
663
-
664
- // Stop on first failure in strict mode
665
- if (executionOptions.strictMode && !result.success) {
666
- break;
667
- }
668
- }
669
-
670
- return results;
671
- },
672
-
673
- /**
674
- * Execute hooks with dependency resolution.
675
- * @param {Array} hooks - Array of hook definitions with dependencies
676
- * @param {Object} event - The hook event
677
- * @param {Object} [executionOptions] - Execution-specific options
678
- * @returns {Promise<Array>} Array of execution results
679
- */
680
- async executeWithDependencies(hooks, event, executionOptions = {}) {
681
- if (!Array.isArray(hooks)) {
682
- throw new TypeError('executeWithDependencies: hooks must be an array');
683
- }
684
-
685
- // Build dependency graph from hook.meta.dependencies
686
- const nameOf = (hook, idx) => hook?.meta?.name || `hook-${idx}`;
687
- const graph = new Map(); // name -> Set(dependencies)
688
- const byName = new Map(); // name -> hook
689
-
690
- for (let i = 0; i < hooks.length; i++) {
691
- const hook = hooks[i];
692
- const name = nameOf(hook, i);
693
- byName.set(name, hook);
694
- const deps = Array.isArray(hook?.meta?.dependencies) ? hook.meta.dependencies : [];
695
- graph.set(name, new Set(deps));
696
- }
697
-
698
- // Kahn's algorithm for topological sort with cycle tolerance
699
- const inDegree = new Map();
700
- // Initialize in-degrees
701
- for (const [name, deps] of graph.entries()) {
702
- if (!inDegree.has(name)) inDegree.set(name, 0);
703
- for (const dep of deps) {
704
- if (!graph.has(dep)) {
705
- const policy = executionOptions.missingDependencyPolicy || missingDependencyPolicy;
706
- if (policy === 'error') {
707
- throw new Error(
708
- `executeWithDependencies: missing dependency '${dep}' for hook '${name}'`
709
- );
710
- }
711
- if (policy === 'warn') {
712
- console.warn(
713
- `executeWithDependencies: missing dependency '${dep}' for hook '${name}' (continuing)`
714
- );
715
- }
716
- continue; // ignore
717
- }
718
- inDegree.set(name, (inDegree.get(name) || 0) + 1);
719
- }
720
- }
721
-
722
- const queue = [];
723
- for (const [name, deg] of inDegree.entries()) {
724
- if (deg === 0) queue.push(name);
725
- }
726
-
727
- const orderedNames = [];
728
- while (queue.length > 0) {
729
- const current = queue.shift();
730
- orderedNames.push(current);
731
- // Reduce in-degree of nodes that depend on current
732
- for (const [name, deps] of graph.entries()) {
733
- if (deps.has(current)) {
734
- const newDeg = (inDegree.get(name) || 0) - 1;
735
- inDegree.set(name, newDeg);
736
- if (newDeg === 0) queue.push(name);
737
- }
738
- }
739
- }
740
-
741
- // Detect cycles or unresolved nodes
742
- if (orderedNames.length < hooks.length) {
743
- const unresolved = hooks.map((h, i) => nameOf(h, i)).filter(n => !orderedNames.includes(n));
744
- const policy = executionOptions.missingDependencyPolicy || missingDependencyPolicy;
745
- if (policy === 'error') {
746
- throw new Error(
747
- `executeWithDependencies: cyclic or unresolved dependencies among: ${unresolved.join(', ')}`
748
- );
749
- }
750
- // Append unresolved in original order as a last resort
751
- orderedNames.push(...unresolved);
752
- }
753
-
754
- const orderedHooks = orderedNames.map(n => byName.get(n));
755
- return this.executeSequential(orderedHooks, event, executionOptions);
756
- },
757
-
758
- /**
759
- * Get execution metrics.
760
- * @returns {Object} Current metrics
761
- */
762
- getMetrics() {
763
- if (!enableMetrics) {
764
- return { metricsDisabled: true };
765
- }
766
-
767
- return {
768
- ...metrics,
769
- successRate:
770
- metrics.totalExecutions > 0 ? metrics.successfulExecutions / metrics.totalExecutions : 0,
771
- failureRate:
772
- metrics.totalExecutions > 0 ? metrics.failedExecutions / metrics.totalExecutions : 0,
773
- cancellationRate:
774
- metrics.totalExecutions > 0 ? metrics.cancelledExecutions / metrics.totalExecutions : 0,
775
- };
776
- },
777
-
778
- /**
779
- * Reset metrics.
780
- */
781
- resetMetrics() {
782
- if (enableMetrics) {
783
- Object.assign(metrics, {
784
- totalExecutions: 0,
785
- successfulExecutions: 0,
786
- failedExecutions: 0,
787
- cancelledExecutions: 0,
788
- totalDuration: 0,
789
- averageDuration: 0,
790
- executionsByPhase: {
791
- before: 0,
792
- condition: 0,
793
- run: 0,
794
- after: 0,
795
- completed: 0,
796
- },
797
- });
798
- }
799
- },
800
-
801
- /**
802
- * Update metrics with execution result.
803
- * @param {Object} result - Execution result
804
- * @private
805
- */
806
- _updateMetrics(result) {
807
- metrics.totalExecutions++;
808
- metrics.totalDuration += result.durationMs || 0;
809
- metrics.averageDuration = metrics.totalDuration / metrics.totalExecutions;
810
-
811
- if (result.success) {
812
- metrics.successfulExecutions++;
813
- } else {
814
- metrics.failedExecutions++;
815
- }
816
-
817
- if (result.cancelled) {
818
- metrics.cancelledExecutions++;
819
- }
820
-
821
- if (result.phase && metrics.executionsByPhase[result.phase] !== undefined) {
822
- metrics.executionsByPhase[result.phase]++;
823
- }
824
- },
825
- };
826
- }
827
-
828
- /**
829
- * Validate a hook definition for execution.
830
- * @param {Object} hook - The hook definition
831
- * @returns {Object} Validation result
832
- */
833
- export function validateHookForExecution(hook) {
834
- if (!hook || typeof hook !== 'object') {
835
- return { valid: false, error: 'Hook must be an object' };
836
- }
837
-
838
- if (!hook.meta || !hook.meta.name) {
839
- return { valid: false, error: 'Hook must have meta.name' };
840
- }
841
-
842
- if (!hook.run || typeof hook.run !== 'function') {
843
- return { valid: false, error: 'Hook must have a run function' };
844
- }
845
-
846
- if (!hook.when) {
847
- return { valid: false, error: 'Hook must have a when condition' };
848
- }
849
-
850
- if (!hook.when.kind) {
851
- return { valid: false, error: 'Hook when condition must have a kind' };
852
- }
853
-
854
- if (!hook.when.ref) {
855
- return { valid: false, error: 'Hook when condition must have a ref' };
856
- }
857
-
858
- if (!hook.when.ref.uri) {
859
- return { valid: false, error: 'Hook when condition ref must have a uri' };
860
- }
861
-
862
- if (!hook.when.ref.sha256) {
863
- return {
864
- valid: false,
865
- error: 'Hook when condition ref must have a sha256 hash',
866
- };
867
- }
868
-
869
- return { valid: true };
870
- }