@unrdf/hooks 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.
- package/dist/index.d.mts +1738 -0
- package/dist/index.d.ts +1738 -0
- package/dist/index.mjs +1738 -0
- package/examples/basic.mjs +113 -0
- package/examples/hook-chains/README.md +263 -0
- package/examples/hook-chains/node_modules/.bin/validate-hooks +21 -0
- package/examples/hook-chains/node_modules/.bin/vitest +21 -0
- package/examples/hook-chains/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/examples/hook-chains/package.json +25 -0
- package/examples/hook-chains/src/index.mjs +348 -0
- package/examples/hook-chains/test/example.test.mjs +252 -0
- package/examples/hook-chains/vitest.config.mjs +14 -0
- package/examples/knowledge-hook-manager-usage.mjs +65 -0
- package/examples/policy-hooks/README.md +193 -0
- package/examples/policy-hooks/node_modules/.bin/validate-hooks +21 -0
- package/examples/policy-hooks/node_modules/.bin/vitest +21 -0
- package/examples/policy-hooks/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/examples/policy-hooks/package.json +25 -0
- package/examples/policy-hooks/src/index.mjs +275 -0
- package/examples/policy-hooks/test/example.test.mjs +204 -0
- package/examples/policy-hooks/vitest.config.mjs +14 -0
- package/examples/validate-hooks.mjs +154 -0
- package/package.json +29 -24
- package/src/hooks/builtin-hooks.mjs +72 -48
- package/src/hooks/condition-evaluator.mjs +1 -1
- package/src/hooks/define-hook.mjs +25 -9
- package/src/hooks/effect-sandbox-worker.mjs +1 -1
- package/src/hooks/effect-sandbox.mjs +5 -2
- package/src/hooks/file-resolver.mjs +2 -2
- package/src/hooks/hook-executor.mjs +12 -19
- package/src/hooks/policy-pack.mjs +3 -3
- package/src/hooks/query-optimizer.mjs +196 -0
- package/src/hooks/query.mjs +150 -0
- package/src/hooks/schemas.mjs +158 -0
- package/src/hooks/security/path-validator.mjs +1 -1
- package/src/hooks/security/sandbox-restrictions.mjs +2 -2
- package/src/hooks/store-cache.mjs +189 -0
- package/src/hooks/validate.mjs +133 -0
- package/src/index.mjs +62 -0
- package/src/policy-compiler.mjs +503 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1738 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { dataFactory } from '@unrdf/oxigraph';
|
|
3
|
+
|
|
4
|
+
const HookTriggerSchema = z.enum([
|
|
5
|
+
// Core CRUD (6)
|
|
6
|
+
"before-add",
|
|
7
|
+
"after-add",
|
|
8
|
+
"before-query",
|
|
9
|
+
"after-query",
|
|
10
|
+
"before-remove",
|
|
11
|
+
"after-remove",
|
|
12
|
+
// Transaction Hooks (4)
|
|
13
|
+
"before-commit",
|
|
14
|
+
"after-commit",
|
|
15
|
+
"before-rollback",
|
|
16
|
+
"after-rollback",
|
|
17
|
+
// Error/Event Hooks (5)
|
|
18
|
+
"on-error",
|
|
19
|
+
"on-validation-fail",
|
|
20
|
+
"on-transform",
|
|
21
|
+
"on-timeout",
|
|
22
|
+
"on-circuit-open",
|
|
23
|
+
// Async/IO Hooks (6)
|
|
24
|
+
"before-fetch",
|
|
25
|
+
"after-fetch",
|
|
26
|
+
"before-sync",
|
|
27
|
+
"after-sync",
|
|
28
|
+
"before-import",
|
|
29
|
+
"after-import",
|
|
30
|
+
// Cron/Time Hooks (4)
|
|
31
|
+
"on-schedule",
|
|
32
|
+
"on-interval",
|
|
33
|
+
"on-idle",
|
|
34
|
+
"on-startup",
|
|
35
|
+
// Lean Six Sigma Quality Hooks (8)
|
|
36
|
+
"quality-gate",
|
|
37
|
+
"defect-detection",
|
|
38
|
+
"continuous-improvement",
|
|
39
|
+
"spc-control",
|
|
40
|
+
"capability-analysis",
|
|
41
|
+
"root-cause",
|
|
42
|
+
"kaizen-event",
|
|
43
|
+
"audit-trail"
|
|
44
|
+
]);
|
|
45
|
+
const HookConfigSchema = z.object({
|
|
46
|
+
name: z.string().min(1, "Hook name is required"),
|
|
47
|
+
trigger: HookTriggerSchema,
|
|
48
|
+
// Note: No return type enforcement - runtime POKA-YOKE guard handles non-boolean returns
|
|
49
|
+
validate: z.function().optional(),
|
|
50
|
+
transform: z.function().optional(),
|
|
51
|
+
metadata: z.record(z.string(), z.any()).optional()
|
|
52
|
+
});
|
|
53
|
+
const HookSchema = z.object({
|
|
54
|
+
name: z.string(),
|
|
55
|
+
trigger: HookTriggerSchema,
|
|
56
|
+
validate: z.function().optional(),
|
|
57
|
+
transform: z.function().optional(),
|
|
58
|
+
metadata: z.record(z.string(), z.any()).optional()
|
|
59
|
+
});
|
|
60
|
+
function defineHook(config) {
|
|
61
|
+
const validated = HookConfigSchema.parse(config);
|
|
62
|
+
if (!validated.validate && !validated.transform) {
|
|
63
|
+
throw new Error("Hook must define either validate or transform function");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
name: validated.name,
|
|
67
|
+
trigger: validated.trigger,
|
|
68
|
+
validate: validated.validate,
|
|
69
|
+
transform: validated.transform,
|
|
70
|
+
metadata: validated.metadata || {},
|
|
71
|
+
// Pre-computed flags for sub-1μs execution (skip Zod in hot path)
|
|
72
|
+
_hasValidation: typeof validated.validate === "function",
|
|
73
|
+
_hasTransformation: typeof validated.transform === "function",
|
|
74
|
+
_validated: true
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function isValidHook(hook) {
|
|
78
|
+
try {
|
|
79
|
+
HookSchema.parse(hook);
|
|
80
|
+
return hook.validate !== void 0 || hook.transform !== void 0;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function getHookMetadata(hook, key) {
|
|
86
|
+
const validated = HookSchema.parse(hook);
|
|
87
|
+
return validated.metadata?.[key];
|
|
88
|
+
}
|
|
89
|
+
function hasValidation$1(hook) {
|
|
90
|
+
if (hook._validated) {
|
|
91
|
+
return hook._hasValidation;
|
|
92
|
+
}
|
|
93
|
+
return typeof hook.validate === "function";
|
|
94
|
+
}
|
|
95
|
+
function hasTransformation$1(hook) {
|
|
96
|
+
if (hook._validated) {
|
|
97
|
+
return hook._hasTransformation;
|
|
98
|
+
}
|
|
99
|
+
return typeof hook.transform === "function";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const HookResultSchema = z.object({
|
|
103
|
+
valid: z.boolean(),
|
|
104
|
+
quad: z.any().optional(),
|
|
105
|
+
error: z.string().optional(),
|
|
106
|
+
hookName: z.string()
|
|
107
|
+
});
|
|
108
|
+
const ChainResultSchema = z.object({
|
|
109
|
+
valid: z.boolean(),
|
|
110
|
+
quad: z.any(),
|
|
111
|
+
results: z.array(HookResultSchema),
|
|
112
|
+
error: z.string().optional()
|
|
113
|
+
});
|
|
114
|
+
function executeHook(hook, quad, options = {}) {
|
|
115
|
+
const validatedHook = hook._validated ? hook : HookSchema.parse(hook);
|
|
116
|
+
const result = {
|
|
117
|
+
valid: true,
|
|
118
|
+
quad,
|
|
119
|
+
hookName: validatedHook.name
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
if (hasValidation$1(validatedHook)) {
|
|
123
|
+
const validationResult = validatedHook.validate(quad);
|
|
124
|
+
if (typeof validationResult !== "boolean") {
|
|
125
|
+
console.warn(
|
|
126
|
+
`[POKA-YOKE] Hook "${validatedHook.name}": validate() returned ${typeof validationResult}, expected boolean. Coercing to boolean.`
|
|
127
|
+
);
|
|
128
|
+
result.warning = `Non-boolean validation return (${typeof validationResult}) coerced to boolean`;
|
|
129
|
+
}
|
|
130
|
+
if (!validationResult) {
|
|
131
|
+
result.valid = false;
|
|
132
|
+
result.error = `Validation failed for hook: ${validatedHook.name}`;
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (hasTransformation$1(validatedHook)) {
|
|
137
|
+
const transformed = validatedHook.transform(quad);
|
|
138
|
+
if (!transformed || typeof transformed !== "object") {
|
|
139
|
+
throw new TypeError(
|
|
140
|
+
`Hook "${validatedHook.name}": transform() must return a Quad object, got ${typeof transformed}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!transformed.subject || !transformed.predicate || !transformed.object) {
|
|
144
|
+
throw new TypeError(
|
|
145
|
+
`Hook "${validatedHook.name}": transform() returned object missing subject/predicate/object`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (transformed._pooled && options.warnPooledQuads !== false) {
|
|
149
|
+
console.warn(
|
|
150
|
+
`[POKA-YOKE] Hook "${validatedHook.name}": returned pooled quad. Clone before storing to prevent memory issues.`
|
|
151
|
+
);
|
|
152
|
+
result.warning = "Pooled quad returned - consider cloning";
|
|
153
|
+
}
|
|
154
|
+
result.quad = transformed;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
result.valid = false;
|
|
159
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
160
|
+
result.errorDetails = {
|
|
161
|
+
hookName: validatedHook.name,
|
|
162
|
+
hookTrigger: validatedHook.trigger,
|
|
163
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
164
|
+
originalError: error instanceof Error ? error : void 0,
|
|
165
|
+
rawError: !(error instanceof Error) ? error : void 0
|
|
166
|
+
};
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function executeHookChain(hooks, quad) {
|
|
171
|
+
const results = [];
|
|
172
|
+
let currentQuad = quad;
|
|
173
|
+
let chainValid = true;
|
|
174
|
+
let chainError = void 0;
|
|
175
|
+
for (const hook of hooks) {
|
|
176
|
+
const result = executeHook(hook, currentQuad);
|
|
177
|
+
results.push(result);
|
|
178
|
+
if (!result.valid) {
|
|
179
|
+
chainValid = false;
|
|
180
|
+
chainError = result.error;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (result.quad) {
|
|
184
|
+
currentQuad = result.quad;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
valid: chainValid,
|
|
189
|
+
quad: currentQuad,
|
|
190
|
+
results,
|
|
191
|
+
error: chainError
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function executeHooksByTrigger(hooks, trigger, quad) {
|
|
195
|
+
const matchingHooks = hooks.filter((h) => h.trigger === trigger);
|
|
196
|
+
return executeHookChain(matchingHooks, quad);
|
|
197
|
+
}
|
|
198
|
+
function wouldPassHooks(hooks, quad) {
|
|
199
|
+
const result = executeHookChain(hooks, quad);
|
|
200
|
+
return result.valid;
|
|
201
|
+
}
|
|
202
|
+
function validateOnly(hooks, quad) {
|
|
203
|
+
for (const hook of hooks) {
|
|
204
|
+
if (hasValidation$1(hook)) {
|
|
205
|
+
try {
|
|
206
|
+
if (!hook.validate(quad)) {
|
|
207
|
+
return {
|
|
208
|
+
valid: false,
|
|
209
|
+
quad,
|
|
210
|
+
error: `Validation failed for hook: ${hook.name}`,
|
|
211
|
+
hookName: hook.name
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return {
|
|
216
|
+
valid: false,
|
|
217
|
+
quad,
|
|
218
|
+
error: error instanceof Error ? error.message : String(error),
|
|
219
|
+
hookName: hook.name
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { valid: true, quad, hookName: "validateOnly" };
|
|
225
|
+
}
|
|
226
|
+
function executeBatch(hooks, quads, options = {}) {
|
|
227
|
+
const { stopOnError = false } = options;
|
|
228
|
+
const results = [];
|
|
229
|
+
let validCount = 0;
|
|
230
|
+
let invalidCount = 0;
|
|
231
|
+
for (let i = 0; i < quads.length; i++) {
|
|
232
|
+
const quad = quads[i];
|
|
233
|
+
let currentQuad = quad;
|
|
234
|
+
let isValid = true;
|
|
235
|
+
let error;
|
|
236
|
+
for (const hook of hooks) {
|
|
237
|
+
if (hasValidation$1(hook)) {
|
|
238
|
+
try {
|
|
239
|
+
if (!hook.validate(currentQuad)) {
|
|
240
|
+
isValid = false;
|
|
241
|
+
error = `Validation failed: ${hook.name}`;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
isValid = false;
|
|
246
|
+
error = e instanceof Error ? e.message : String(e);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (isValid && hasTransformation$1(hook)) {
|
|
251
|
+
try {
|
|
252
|
+
currentQuad = hook.transform(currentQuad);
|
|
253
|
+
} catch (e) {
|
|
254
|
+
isValid = false;
|
|
255
|
+
error = e instanceof Error ? e.message : String(e);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
results.push({ valid: isValid, quad: currentQuad, error, results: [] });
|
|
261
|
+
if (isValid) {
|
|
262
|
+
validCount++;
|
|
263
|
+
} else {
|
|
264
|
+
invalidCount++;
|
|
265
|
+
if (stopOnError) break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { results, validCount, invalidCount };
|
|
269
|
+
}
|
|
270
|
+
function validateBatch(hooks, quads) {
|
|
271
|
+
const validationHooks = hooks.filter(hasValidation$1);
|
|
272
|
+
const bitmap = new Uint8Array(quads.length);
|
|
273
|
+
for (let i = 0; i < quads.length; i++) {
|
|
274
|
+
const quad = quads[i];
|
|
275
|
+
let isValid = true;
|
|
276
|
+
for (const hook of validationHooks) {
|
|
277
|
+
try {
|
|
278
|
+
if (!hook.validate(quad)) {
|
|
279
|
+
isValid = false;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
isValid = false;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
bitmap[i] = isValid ? 1 : 0;
|
|
288
|
+
}
|
|
289
|
+
return bitmap;
|
|
290
|
+
}
|
|
291
|
+
function transformBatch(hooks, quads, options = {}) {
|
|
292
|
+
const { validateFirst = true } = options;
|
|
293
|
+
const transformed = [];
|
|
294
|
+
const errors = [];
|
|
295
|
+
for (let i = 0; i < quads.length; i++) {
|
|
296
|
+
let currentQuad = quads[i];
|
|
297
|
+
let hasError = false;
|
|
298
|
+
for (const hook of hooks) {
|
|
299
|
+
try {
|
|
300
|
+
if (validateFirst && hasValidation$1(hook)) {
|
|
301
|
+
if (!hook.validate(currentQuad)) {
|
|
302
|
+
errors.push({ index: i, error: `Validation failed: ${hook.name}` });
|
|
303
|
+
hasError = true;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (hasTransformation$1(hook)) {
|
|
308
|
+
currentQuad = hook.transform(currentQuad);
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
errors.push({
|
|
312
|
+
index: i,
|
|
313
|
+
error: error instanceof Error ? error.message : String(error)
|
|
314
|
+
});
|
|
315
|
+
hasError = true;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (!hasError) {
|
|
320
|
+
transformed.push(currentQuad);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { transformed, errors };
|
|
324
|
+
}
|
|
325
|
+
const hookValidationCache = /* @__PURE__ */ new WeakMap();
|
|
326
|
+
function clearHookCaches() {
|
|
327
|
+
}
|
|
328
|
+
function prewarmHookCache(hooks) {
|
|
329
|
+
const errors = [];
|
|
330
|
+
let prewarmed = 0;
|
|
331
|
+
for (const hook of hooks) {
|
|
332
|
+
try {
|
|
333
|
+
HookSchema.parse(hook);
|
|
334
|
+
hookValidationCache.set(hook, true);
|
|
335
|
+
prewarmed++;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
errors.push(
|
|
338
|
+
`Hook "${hook?.name || "unknown"}": ${error instanceof Error ? error.message : String(error)}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return { prewarmed, errors };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const compiledChains = /* @__PURE__ */ new Map();
|
|
346
|
+
let jitAvailable = true;
|
|
347
|
+
try {
|
|
348
|
+
new Function("return true")();
|
|
349
|
+
} catch {
|
|
350
|
+
jitAvailable = false;
|
|
351
|
+
}
|
|
352
|
+
function hasValidation(hook) {
|
|
353
|
+
return typeof hook.validate === "function";
|
|
354
|
+
}
|
|
355
|
+
function hasTransformation(hook) {
|
|
356
|
+
return typeof hook.transform === "function";
|
|
357
|
+
}
|
|
358
|
+
function getChainKey(hooks) {
|
|
359
|
+
return hooks.map((h) => h.name).join("|");
|
|
360
|
+
}
|
|
361
|
+
function compileHookChain(hooks) {
|
|
362
|
+
const chainKey = getChainKey(hooks);
|
|
363
|
+
if (compiledChains.has(chainKey)) {
|
|
364
|
+
return compiledChains.get(chainKey);
|
|
365
|
+
}
|
|
366
|
+
if (!jitAvailable) {
|
|
367
|
+
const interpretedFn = createInterpretedChain(hooks);
|
|
368
|
+
compiledChains.set(chainKey, interpretedFn);
|
|
369
|
+
return interpretedFn;
|
|
370
|
+
}
|
|
371
|
+
const validationSteps = hooks.map(
|
|
372
|
+
(h, i) => hasValidation(h) ? `if (!hooks[${i}].validate(quad)) return { valid: false, quad, failedHook: hooks[${i}].name };` : ""
|
|
373
|
+
).filter(Boolean).join("\n ");
|
|
374
|
+
const transformSteps = hooks.map((h, i) => hasTransformation(h) ? `quad = hooks[${i}].transform(quad);` : "").filter(Boolean).join("\n ");
|
|
375
|
+
const fnBody = `
|
|
376
|
+
${validationSteps}
|
|
377
|
+
${transformSteps}
|
|
378
|
+
return { valid: true, quad };
|
|
379
|
+
`;
|
|
380
|
+
try {
|
|
381
|
+
const compiledFn = new Function("hooks", "quad", fnBody);
|
|
382
|
+
compiledChains.set(chainKey, compiledFn);
|
|
383
|
+
return compiledFn;
|
|
384
|
+
} catch {
|
|
385
|
+
jitAvailable = false;
|
|
386
|
+
const interpretedFn = createInterpretedChain(hooks);
|
|
387
|
+
compiledChains.set(chainKey, interpretedFn);
|
|
388
|
+
return interpretedFn;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function createInterpretedChain(hooks) {
|
|
392
|
+
const capturedHooks = hooks;
|
|
393
|
+
return function interpretedChain(_hooks, quad) {
|
|
394
|
+
let currentQuad = quad;
|
|
395
|
+
for (const hook of capturedHooks) {
|
|
396
|
+
if (hasValidation(hook)) {
|
|
397
|
+
if (!hook.validate(currentQuad)) {
|
|
398
|
+
return { valid: false, quad: currentQuad, failedHook: hook.name };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (hasTransformation(hook)) {
|
|
402
|
+
currentQuad = hook.transform(currentQuad);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return { valid: true, quad: currentQuad };
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function compileValidationOnlyChain(hooks) {
|
|
409
|
+
const chainKey = `validate:${getChainKey(hooks)}`;
|
|
410
|
+
if (compiledChains.has(chainKey)) {
|
|
411
|
+
return compiledChains.get(chainKey);
|
|
412
|
+
}
|
|
413
|
+
const validationHooks = hooks.filter(hasValidation);
|
|
414
|
+
if (!jitAvailable || validationHooks.length === 0) {
|
|
415
|
+
const fn = validationHooks.length === 0 ? () => true : (_hooks, quad) => validationHooks.every((h) => h.validate(quad));
|
|
416
|
+
compiledChains.set(chainKey, fn);
|
|
417
|
+
return fn;
|
|
418
|
+
}
|
|
419
|
+
const checks = validationHooks.map((_, i) => `hooks[${i}].validate(quad)`).join(" && ");
|
|
420
|
+
const fnBody = `return ${checks || "true"};`;
|
|
421
|
+
try {
|
|
422
|
+
const compiledFn = new Function("hooks", "quad", fnBody);
|
|
423
|
+
const wrapper = (_, quad) => compiledFn(validationHooks, quad);
|
|
424
|
+
compiledChains.set(chainKey, wrapper);
|
|
425
|
+
return wrapper;
|
|
426
|
+
} catch {
|
|
427
|
+
const fn = (_hooks, quad) => validationHooks.every((h) => h.validate(quad));
|
|
428
|
+
compiledChains.set(chainKey, fn);
|
|
429
|
+
return fn;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function clearCompiledChainCache() {
|
|
433
|
+
compiledChains.clear();
|
|
434
|
+
}
|
|
435
|
+
function getCompilerStats() {
|
|
436
|
+
return {
|
|
437
|
+
size: compiledChains.size,
|
|
438
|
+
jitAvailable
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function isJitAvailable() {
|
|
442
|
+
return jitAvailable;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
class QuadPool {
|
|
446
|
+
/**
|
|
447
|
+
* Create a new quad pool.
|
|
448
|
+
*
|
|
449
|
+
* @param {object} options - Pool options
|
|
450
|
+
* @param {number} options.size - Initial pool size (default: 1000)
|
|
451
|
+
* @param {boolean} options.autoGrow - Auto-grow pool when exhausted (default: true)
|
|
452
|
+
*/
|
|
453
|
+
constructor(options = {}) {
|
|
454
|
+
this.size = options.size || 1e3;
|
|
455
|
+
this.autoGrow = options.autoGrow !== false;
|
|
456
|
+
this.pool = new Array(this.size);
|
|
457
|
+
this.index = 0;
|
|
458
|
+
this.acquired = 0;
|
|
459
|
+
this.highWaterMark = 0;
|
|
460
|
+
for (let i = 0; i < this.size; i++) {
|
|
461
|
+
this.pool[i] = this._createEmptyQuad();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Create an empty quad object for the pool.
|
|
466
|
+
*
|
|
467
|
+
* @private
|
|
468
|
+
* @returns {PooledQuad} - Empty quad object
|
|
469
|
+
*/
|
|
470
|
+
_createEmptyQuad() {
|
|
471
|
+
return {
|
|
472
|
+
subject: null,
|
|
473
|
+
predicate: null,
|
|
474
|
+
object: null,
|
|
475
|
+
graph: null,
|
|
476
|
+
_pooled: true
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Acquire a quad from the pool.
|
|
481
|
+
*
|
|
482
|
+
* @param {import('rdf-js').Term} subject - Subject term
|
|
483
|
+
* @param {import('rdf-js').Term} predicate - Predicate term
|
|
484
|
+
* @param {import('rdf-js').Term} object - Object term
|
|
485
|
+
* @param {import('rdf-js').Term} [graph] - Graph term (optional)
|
|
486
|
+
* @returns {PooledQuad} - Pooled quad with assigned values
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* const quad = quadPool.acquire(subject, predicate, object, graph);
|
|
490
|
+
* // Use quad...
|
|
491
|
+
* quadPool.release(quad);
|
|
492
|
+
*/
|
|
493
|
+
acquire(subject, predicate, object, graph = null) {
|
|
494
|
+
const quad = this.pool[this.index];
|
|
495
|
+
quad.subject = subject;
|
|
496
|
+
quad.predicate = predicate;
|
|
497
|
+
quad.object = object;
|
|
498
|
+
quad.graph = graph;
|
|
499
|
+
this.index = (this.index + 1) % this.size;
|
|
500
|
+
this.acquired++;
|
|
501
|
+
if (this.acquired > this.highWaterMark) {
|
|
502
|
+
this.highWaterMark = this.acquired;
|
|
503
|
+
}
|
|
504
|
+
if (this.autoGrow && this.acquired >= this.size) {
|
|
505
|
+
this._grow();
|
|
506
|
+
}
|
|
507
|
+
return quad;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Release a quad back to the pool.
|
|
511
|
+
*
|
|
512
|
+
* @param {PooledQuad} quad - Quad to release
|
|
513
|
+
*/
|
|
514
|
+
release(quad) {
|
|
515
|
+
if (quad && quad._pooled) {
|
|
516
|
+
quad.subject = null;
|
|
517
|
+
quad.predicate = null;
|
|
518
|
+
quad.object = null;
|
|
519
|
+
quad.graph = null;
|
|
520
|
+
this.acquired = Math.max(0, this.acquired - 1);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Clone a pooled quad to a non-pooled object.
|
|
525
|
+
* Use this when you need to persist a quad beyond the current operation.
|
|
526
|
+
*
|
|
527
|
+
* @param {PooledQuad} quad - Quad to clone
|
|
528
|
+
* @param {Function} dataFactory - Data factory with quad() method
|
|
529
|
+
* @returns {import('rdf-js').Quad} - Non-pooled quad
|
|
530
|
+
*/
|
|
531
|
+
clone(quad, dataFactory) {
|
|
532
|
+
return dataFactory.quad(quad.subject, quad.predicate, quad.object, quad.graph);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Grow the pool by doubling its size.
|
|
536
|
+
*
|
|
537
|
+
* @private
|
|
538
|
+
*/
|
|
539
|
+
_grow() {
|
|
540
|
+
const newSize = this.size * 2;
|
|
541
|
+
const newPool = new Array(newSize);
|
|
542
|
+
for (let i = 0; i < this.size; i++) {
|
|
543
|
+
newPool[i] = this.pool[i];
|
|
544
|
+
}
|
|
545
|
+
for (let i = this.size; i < newSize; i++) {
|
|
546
|
+
newPool[i] = this._createEmptyQuad();
|
|
547
|
+
}
|
|
548
|
+
this.pool = newPool;
|
|
549
|
+
this.size = newSize;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Reset the pool (clear all references, reset index).
|
|
553
|
+
*/
|
|
554
|
+
reset() {
|
|
555
|
+
for (let i = 0; i < this.size; i++) {
|
|
556
|
+
const quad = this.pool[i];
|
|
557
|
+
quad.subject = null;
|
|
558
|
+
quad.predicate = null;
|
|
559
|
+
quad.object = null;
|
|
560
|
+
quad.graph = null;
|
|
561
|
+
}
|
|
562
|
+
this.index = 0;
|
|
563
|
+
this.acquired = 0;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Get pool statistics.
|
|
567
|
+
*
|
|
568
|
+
* @returns {{size: number, acquired: number, available: number, highWaterMark: number}} - Pool stats
|
|
569
|
+
*/
|
|
570
|
+
stats() {
|
|
571
|
+
return {
|
|
572
|
+
size: this.size,
|
|
573
|
+
acquired: this.acquired,
|
|
574
|
+
available: this.size - this.acquired,
|
|
575
|
+
highWaterMark: this.highWaterMark,
|
|
576
|
+
utilizationPercent: (this.acquired / this.size * 100).toFixed(1)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const quadPool = new QuadPool({ size: 1e3 });
|
|
581
|
+
function createPooledTransform(transformFn, pool = quadPool) {
|
|
582
|
+
return function pooledTransform(quad) {
|
|
583
|
+
const result = transformFn(quad);
|
|
584
|
+
if (result !== quad) {
|
|
585
|
+
return pool.acquire(result.subject, result.predicate, result.object, result.graph);
|
|
586
|
+
}
|
|
587
|
+
return quad;
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function isPooledQuad(quad) {
|
|
591
|
+
return quad && quad._pooled === true;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const HookRegistrySchema = z.object({
|
|
595
|
+
hooks: z.instanceof(Map),
|
|
596
|
+
triggerIndex: z.instanceof(Map)
|
|
597
|
+
});
|
|
598
|
+
function createHookRegistry() {
|
|
599
|
+
return {
|
|
600
|
+
hooks: /* @__PURE__ */ new Map(),
|
|
601
|
+
triggerIndex: /* @__PURE__ */ new Map()
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function registerHook(registry, hook) {
|
|
605
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
606
|
+
const validatedHook = HookSchema.parse(hook);
|
|
607
|
+
if (validatedRegistry.hooks.has(validatedHook.name)) {
|
|
608
|
+
throw new Error(`Hook already registered: ${validatedHook.name}`);
|
|
609
|
+
}
|
|
610
|
+
validatedRegistry.hooks.set(validatedHook.name, validatedHook);
|
|
611
|
+
if (!validatedRegistry.triggerIndex.has(validatedHook.trigger)) {
|
|
612
|
+
validatedRegistry.triggerIndex.set(validatedHook.trigger, /* @__PURE__ */ new Set());
|
|
613
|
+
}
|
|
614
|
+
validatedRegistry.triggerIndex.get(validatedHook.trigger).add(validatedHook.name);
|
|
615
|
+
}
|
|
616
|
+
function unregisterHook(registry, name) {
|
|
617
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
618
|
+
const hook = validatedRegistry.hooks.get(name);
|
|
619
|
+
if (!hook) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
validatedRegistry.hooks.delete(name);
|
|
623
|
+
const triggerSet = validatedRegistry.triggerIndex.get(hook.trigger);
|
|
624
|
+
if (triggerSet) {
|
|
625
|
+
triggerSet.delete(name);
|
|
626
|
+
if (triggerSet.size === 0) {
|
|
627
|
+
validatedRegistry.triggerIndex.delete(hook.trigger);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
function getHook(registry, name) {
|
|
633
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
634
|
+
return validatedRegistry.hooks.get(name);
|
|
635
|
+
}
|
|
636
|
+
function listHooks(registry) {
|
|
637
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
638
|
+
return Array.from(validatedRegistry.hooks.values());
|
|
639
|
+
}
|
|
640
|
+
function getHooksByTrigger(registry, trigger) {
|
|
641
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
642
|
+
const hookNames = validatedRegistry.triggerIndex.get(trigger);
|
|
643
|
+
if (!hookNames) {
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
const hooks = [];
|
|
647
|
+
for (const name of hookNames) {
|
|
648
|
+
const hook = validatedRegistry.hooks.get(name);
|
|
649
|
+
if (hook) {
|
|
650
|
+
hooks.push(hook);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return hooks;
|
|
654
|
+
}
|
|
655
|
+
function hasHook(registry, name) {
|
|
656
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
657
|
+
return validatedRegistry.hooks.has(name);
|
|
658
|
+
}
|
|
659
|
+
function clearHooks(registry) {
|
|
660
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
661
|
+
validatedRegistry.hooks.clear();
|
|
662
|
+
validatedRegistry.triggerIndex.clear();
|
|
663
|
+
}
|
|
664
|
+
function getRegistryStats(registry) {
|
|
665
|
+
const validatedRegistry = HookRegistrySchema.parse(registry);
|
|
666
|
+
const byTrigger = {};
|
|
667
|
+
for (const [trigger, names] of validatedRegistry.triggerIndex) {
|
|
668
|
+
byTrigger[trigger] = names.size;
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
totalHooks: validatedRegistry.hooks.size,
|
|
672
|
+
byTrigger
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const validateSubjectIRI = defineHook({
|
|
677
|
+
name: "validate-subject-iri",
|
|
678
|
+
trigger: "before-add",
|
|
679
|
+
validate: (quad) => {
|
|
680
|
+
return quad.subject.termType === "NamedNode";
|
|
681
|
+
},
|
|
682
|
+
metadata: {
|
|
683
|
+
description: "Validates that quad subject is a Named Node (IRI)"
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
const validatePredicateIRI = defineHook({
|
|
687
|
+
name: "validate-predicate-iri",
|
|
688
|
+
trigger: "before-add",
|
|
689
|
+
validate: (quad) => {
|
|
690
|
+
return quad.predicate.termType === "NamedNode";
|
|
691
|
+
},
|
|
692
|
+
metadata: {
|
|
693
|
+
description: "Validates that quad predicate is a Named Node (IRI)"
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
const validateObjectLiteral = defineHook({
|
|
697
|
+
name: "validate-object-literal",
|
|
698
|
+
trigger: "before-add",
|
|
699
|
+
validate: (quad) => {
|
|
700
|
+
return quad.object.termType === "Literal";
|
|
701
|
+
},
|
|
702
|
+
metadata: {
|
|
703
|
+
description: "Validates that quad object is a Literal"
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
const validateIRIFormat = defineHook({
|
|
707
|
+
name: "validate-iri-format",
|
|
708
|
+
trigger: "before-add",
|
|
709
|
+
validate: (quad) => {
|
|
710
|
+
const validateIRI = (term) => {
|
|
711
|
+
if (term.termType !== "NamedNode") {
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
new URL(term.value);
|
|
716
|
+
return true;
|
|
717
|
+
} catch {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
return validateIRI(quad.subject) && validateIRI(quad.predicate) && validateIRI(quad.object);
|
|
722
|
+
},
|
|
723
|
+
metadata: {
|
|
724
|
+
description: "Validates that IRI values are well-formed URLs"
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
const validateLanguageTag = defineHook({
|
|
728
|
+
name: "validate-language-tag",
|
|
729
|
+
trigger: "before-add",
|
|
730
|
+
validate: (quad) => {
|
|
731
|
+
if (quad.object.termType !== "Literal") {
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
return quad.object.language !== void 0 && quad.object.language !== "";
|
|
735
|
+
},
|
|
736
|
+
metadata: {
|
|
737
|
+
description: "Validates that literal objects have language tags"
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
const rejectBlankNodes = defineHook({
|
|
741
|
+
name: "reject-blank-nodes",
|
|
742
|
+
trigger: "before-add",
|
|
743
|
+
validate: (quad) => {
|
|
744
|
+
return quad.subject.termType !== "BlankNode" && quad.object.termType !== "BlankNode";
|
|
745
|
+
},
|
|
746
|
+
metadata: {
|
|
747
|
+
description: "Rejects quads containing blank nodes"
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
const normalizeNamespace = defineHook({
|
|
751
|
+
name: "normalize-namespace",
|
|
752
|
+
trigger: "before-add",
|
|
753
|
+
transform: (quad) => {
|
|
754
|
+
return quad;
|
|
755
|
+
},
|
|
756
|
+
metadata: {
|
|
757
|
+
description: "Normalizes namespace prefixes to full IRIs"
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
const normalizeLanguageTag = defineHook({
|
|
761
|
+
name: "normalize-language-tag",
|
|
762
|
+
trigger: "before-add",
|
|
763
|
+
transform: (quad) => {
|
|
764
|
+
if (quad.object.termType !== "Literal" || !quad.object.language) {
|
|
765
|
+
return quad;
|
|
766
|
+
}
|
|
767
|
+
return dataFactory.quad(
|
|
768
|
+
quad.subject,
|
|
769
|
+
quad.predicate,
|
|
770
|
+
dataFactory.literal(quad.object.value, quad.object.language.toLowerCase()),
|
|
771
|
+
quad.graph
|
|
772
|
+
);
|
|
773
|
+
},
|
|
774
|
+
metadata: {
|
|
775
|
+
description: "Normalizes language tags to lowercase"
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
const trimLiterals = defineHook({
|
|
779
|
+
name: "trim-literals",
|
|
780
|
+
trigger: "before-add",
|
|
781
|
+
transform: (quad) => {
|
|
782
|
+
if (quad.object.termType !== "Literal") {
|
|
783
|
+
return quad;
|
|
784
|
+
}
|
|
785
|
+
return dataFactory.quad(
|
|
786
|
+
quad.subject,
|
|
787
|
+
quad.predicate,
|
|
788
|
+
dataFactory.literal(quad.object.value.trim(), quad.object.language || quad.object.datatype),
|
|
789
|
+
quad.graph
|
|
790
|
+
);
|
|
791
|
+
},
|
|
792
|
+
metadata: {
|
|
793
|
+
description: "Trims whitespace from literal values"
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
const standardValidation = defineHook({
|
|
797
|
+
name: "standard-validation",
|
|
798
|
+
trigger: "before-add",
|
|
799
|
+
validate: (quad) => {
|
|
800
|
+
return quad.predicate.termType === "NamedNode" && (quad.subject.termType === "NamedNode" || quad.subject.termType === "BlankNode");
|
|
801
|
+
},
|
|
802
|
+
metadata: {
|
|
803
|
+
description: "Standard RDF validation rules"
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
const normalizeLanguageTagPooled = defineHook({
|
|
807
|
+
name: "normalize-language-tag-pooled",
|
|
808
|
+
trigger: "before-add",
|
|
809
|
+
transform: (quad) => {
|
|
810
|
+
const isLiteral = quad.object.termType === "Literal";
|
|
811
|
+
const hasLanguage = isLiteral && quad.object.language;
|
|
812
|
+
if (!hasLanguage) return quad;
|
|
813
|
+
return quadPool.acquire(
|
|
814
|
+
quad.subject,
|
|
815
|
+
quad.predicate,
|
|
816
|
+
dataFactory.literal(quad.object.value, quad.object.language.toLowerCase()),
|
|
817
|
+
quad.graph
|
|
818
|
+
);
|
|
819
|
+
},
|
|
820
|
+
metadata: {
|
|
821
|
+
description: "Zero-allocation language tag normalization using quad pool",
|
|
822
|
+
pooled: true
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
const trimLiteralsPooled = defineHook({
|
|
826
|
+
name: "trim-literals-pooled",
|
|
827
|
+
trigger: "before-add",
|
|
828
|
+
transform: (quad) => {
|
|
829
|
+
if (quad.object.termType !== "Literal") return quad;
|
|
830
|
+
const trimmed = quad.object.value.trim();
|
|
831
|
+
if (trimmed === quad.object.value) return quad;
|
|
832
|
+
return quadPool.acquire(
|
|
833
|
+
quad.subject,
|
|
834
|
+
quad.predicate,
|
|
835
|
+
dataFactory.literal(trimmed, quad.object.language || quad.object.datatype),
|
|
836
|
+
quad.graph
|
|
837
|
+
);
|
|
838
|
+
},
|
|
839
|
+
metadata: {
|
|
840
|
+
description: "Zero-allocation literal trimming using quad pool",
|
|
841
|
+
pooled: true
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
const builtinHooks = {
|
|
845
|
+
// Validation
|
|
846
|
+
validateSubjectIRI,
|
|
847
|
+
validatePredicateIRI,
|
|
848
|
+
validateObjectLiteral,
|
|
849
|
+
validateIRIFormat,
|
|
850
|
+
validateLanguageTag,
|
|
851
|
+
rejectBlankNodes,
|
|
852
|
+
// Transformation
|
|
853
|
+
normalizeNamespace,
|
|
854
|
+
normalizeLanguageTag,
|
|
855
|
+
trimLiterals,
|
|
856
|
+
// Pooled variants (zero-allocation)
|
|
857
|
+
normalizeLanguageTagPooled,
|
|
858
|
+
trimLiteralsPooled,
|
|
859
|
+
// Composite
|
|
860
|
+
standardValidation
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
class KnowledgeHookManager {
|
|
864
|
+
/**
|
|
865
|
+
* @private
|
|
866
|
+
* @type {import('./hook-management.mjs').HookRegistry}
|
|
867
|
+
*/
|
|
868
|
+
#registry;
|
|
869
|
+
/**
|
|
870
|
+
* POKA-YOKE: Recursive execution guard (RPN 128 → 0)
|
|
871
|
+
* @private
|
|
872
|
+
* @type {number}
|
|
873
|
+
*/
|
|
874
|
+
#executionDepth = 0;
|
|
875
|
+
/**
|
|
876
|
+
* Maximum allowed execution depth
|
|
877
|
+
* @private
|
|
878
|
+
* @type {number}
|
|
879
|
+
*/
|
|
880
|
+
#maxExecutionDepth = 3;
|
|
881
|
+
/**
|
|
882
|
+
* Create a new KnowledgeHookManager
|
|
883
|
+
*
|
|
884
|
+
* @param {Object} [options] - Configuration options
|
|
885
|
+
* @param {boolean} [options.includeBuiltins=false] - Include built-in hooks
|
|
886
|
+
* @param {number} [options.maxExecutionDepth=3] - Maximum recursion depth (1-10)
|
|
887
|
+
*/
|
|
888
|
+
constructor(options = {}) {
|
|
889
|
+
this.#registry = createHookRegistry();
|
|
890
|
+
if (options.maxExecutionDepth !== void 0) {
|
|
891
|
+
if (options.maxExecutionDepth < 1 || options.maxExecutionDepth > 10) {
|
|
892
|
+
throw new Error(
|
|
893
|
+
`[POKA-YOKE] maxExecutionDepth must be between 1 and 10, got ${options.maxExecutionDepth}`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
this.#maxExecutionDepth = options.maxExecutionDepth;
|
|
897
|
+
}
|
|
898
|
+
if (options.includeBuiltins) {
|
|
899
|
+
for (const hook of Object.values(builtinHooks)) {
|
|
900
|
+
registerHook(this.#registry, hook);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Get current execution depth
|
|
906
|
+
* @returns {number}
|
|
907
|
+
*/
|
|
908
|
+
getExecutionDepth() {
|
|
909
|
+
return this.#executionDepth;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Check if currently executing hooks
|
|
913
|
+
* @returns {boolean}
|
|
914
|
+
*/
|
|
915
|
+
isExecuting() {
|
|
916
|
+
return this.#executionDepth > 0;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Define and register a hook
|
|
920
|
+
*
|
|
921
|
+
* @param {import('./define-hook.mjs').HookConfig} hookDef - Hook definition
|
|
922
|
+
* @returns {import('./define-hook.mjs').Hook} The defined hook
|
|
923
|
+
*/
|
|
924
|
+
define(hookDef) {
|
|
925
|
+
const hook = defineHook(hookDef);
|
|
926
|
+
registerHook(this.#registry, hook);
|
|
927
|
+
return hook;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Register a hook
|
|
931
|
+
*
|
|
932
|
+
* @param {import('./define-hook.mjs').Hook} hook - Hook to register
|
|
933
|
+
* @returns {void}
|
|
934
|
+
*/
|
|
935
|
+
registerHook(hook) {
|
|
936
|
+
registerHook(this.#registry, hook);
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Unregister a hook
|
|
940
|
+
*
|
|
941
|
+
* @param {string} hookId - ID of hook to unregister
|
|
942
|
+
* @returns {boolean} True if hook was removed
|
|
943
|
+
*/
|
|
944
|
+
unregisterHook(hookId) {
|
|
945
|
+
return unregisterHook(this.#registry, hookId);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Get a hook by ID
|
|
949
|
+
*
|
|
950
|
+
* @param {string} hookId - Hook ID
|
|
951
|
+
* @returns {import('./define-hook.mjs').Hook | undefined}
|
|
952
|
+
*/
|
|
953
|
+
getHook(hookId) {
|
|
954
|
+
return getHook(this.#registry, hookId);
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* List all hooks
|
|
958
|
+
*
|
|
959
|
+
* @param {Object} [options] - Filter options
|
|
960
|
+
* @param {string} [options.trigger] - Filter by trigger
|
|
961
|
+
* @param {boolean} [options.enabled] - Filter by enabled status
|
|
962
|
+
* @returns {import('./define-hook.mjs').Hook[]}
|
|
963
|
+
*/
|
|
964
|
+
listHooks(options) {
|
|
965
|
+
return listHooks(this.#registry);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Get hooks by trigger
|
|
969
|
+
*
|
|
970
|
+
* @param {string} trigger - Trigger to filter by
|
|
971
|
+
* @returns {import('./define-hook.mjs').Hook[]}
|
|
972
|
+
*/
|
|
973
|
+
getHooksByTrigger(trigger) {
|
|
974
|
+
return getHooksByTrigger(this.#registry, trigger);
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Check if hook exists
|
|
978
|
+
*
|
|
979
|
+
* @param {string} hookId - Hook ID
|
|
980
|
+
* @returns {boolean}
|
|
981
|
+
*/
|
|
982
|
+
hasHook(hookId) {
|
|
983
|
+
return hasHook(this.#registry, hookId);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Clear all hooks
|
|
987
|
+
*
|
|
988
|
+
* @returns {void}
|
|
989
|
+
*/
|
|
990
|
+
clearHooks() {
|
|
991
|
+
clearHooks(this.#registry);
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Get registry statistics
|
|
995
|
+
*
|
|
996
|
+
* @returns {{ total: number, enabled: number, disabled: number, byTrigger: Record<string, number> }}
|
|
997
|
+
*/
|
|
998
|
+
getStats() {
|
|
999
|
+
return getRegistryStats(this.#registry);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Execute a specific hook
|
|
1003
|
+
*
|
|
1004
|
+
* @param {string} hookId - Hook ID
|
|
1005
|
+
* @param {*} data - Data to process
|
|
1006
|
+
* @param {*} context - Execution context
|
|
1007
|
+
* @returns {Promise<import('./hook-executor.mjs').HookResult>}
|
|
1008
|
+
*/
|
|
1009
|
+
async executeHook(hookId, data, context) {
|
|
1010
|
+
const hook = this.getHook(hookId);
|
|
1011
|
+
if (!hook) {
|
|
1012
|
+
throw new Error(`Hook not found: ${hookId}`);
|
|
1013
|
+
}
|
|
1014
|
+
return executeHook(hook, data, context);
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Execute hooks in chain
|
|
1018
|
+
*
|
|
1019
|
+
* @param {import('./define-hook.mjs').Hook[]} hooks - Hooks to execute
|
|
1020
|
+
* @param {*} data - Initial data
|
|
1021
|
+
* @param {*} context - Execution context
|
|
1022
|
+
* @returns {Promise<import('./hook-executor.mjs').ChainResult>}
|
|
1023
|
+
*/
|
|
1024
|
+
async executeChain(hooks, data, context) {
|
|
1025
|
+
return executeHookChain(hooks, data);
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Execute hooks by trigger
|
|
1029
|
+
*
|
|
1030
|
+
* @param {string} trigger - Trigger to execute
|
|
1031
|
+
* @param {*} data - Data to process
|
|
1032
|
+
* @param {*} context - Execution context
|
|
1033
|
+
* @returns {Promise<import('./hook-executor.mjs').ChainResult>}
|
|
1034
|
+
*/
|
|
1035
|
+
async executeByTrigger(trigger, data, context) {
|
|
1036
|
+
if (this.#executionDepth >= this.#maxExecutionDepth) {
|
|
1037
|
+
const error = new Error(
|
|
1038
|
+
`[POKA-YOKE] Recursive hook execution detected (depth: ${this.#executionDepth}, max: ${this.#maxExecutionDepth}). Trigger: ${trigger}`
|
|
1039
|
+
);
|
|
1040
|
+
error.code = "RECURSIVE_HOOK_EXECUTION";
|
|
1041
|
+
throw error;
|
|
1042
|
+
}
|
|
1043
|
+
this.#executionDepth++;
|
|
1044
|
+
try {
|
|
1045
|
+
const hooks = this.listHooks();
|
|
1046
|
+
return executeHooksByTrigger(hooks, trigger, data, context);
|
|
1047
|
+
} finally {
|
|
1048
|
+
this.#executionDepth--;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Check if data would pass hooks
|
|
1053
|
+
*
|
|
1054
|
+
* @param {string} trigger - Trigger to check
|
|
1055
|
+
* @param {*} data - Data to validate
|
|
1056
|
+
* @param {*} context - Execution context
|
|
1057
|
+
* @returns {Promise<boolean>}
|
|
1058
|
+
*/
|
|
1059
|
+
async wouldPass(trigger, data, context) {
|
|
1060
|
+
const hooks = this.getHooksByTrigger(trigger);
|
|
1061
|
+
return wouldPassHooks(hooks, data);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Get built-in hooks
|
|
1065
|
+
*
|
|
1066
|
+
* @returns {import('./define-hook.mjs').Hook[]}
|
|
1067
|
+
*/
|
|
1068
|
+
static getBuiltinHooks() {
|
|
1069
|
+
return Object.values(builtinHooks);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const ScheduleConfigSchema = z.object({
|
|
1074
|
+
id: z.string().min(1),
|
|
1075
|
+
hookId: z.string().min(1),
|
|
1076
|
+
type: z.enum(["cron", "interval", "idle", "startup"]),
|
|
1077
|
+
expression: z.string().optional(),
|
|
1078
|
+
// Cron expression
|
|
1079
|
+
// POKA-YOKE: Interval bounds validation (RPN 168 → 0)
|
|
1080
|
+
// Min 10ms prevents CPU thrashing, max 24h prevents integer overflow
|
|
1081
|
+
intervalMs: z.number().positive().min(10, "Interval must be at least 10ms to prevent CPU thrashing").max(864e5, "Interval cannot exceed 24 hours (86400000ms)").optional(),
|
|
1082
|
+
idleTimeoutMs: z.number().positive().min(100, "Idle timeout must be at least 100ms").max(36e5, "Idle timeout cannot exceed 1 hour").optional(),
|
|
1083
|
+
enabled: z.boolean().default(true),
|
|
1084
|
+
maxRuns: z.number().positive().optional(),
|
|
1085
|
+
// Max executions
|
|
1086
|
+
metadata: z.record(z.any()).optional()
|
|
1087
|
+
});
|
|
1088
|
+
class HookScheduler {
|
|
1089
|
+
/**
|
|
1090
|
+
* Create a new hook scheduler
|
|
1091
|
+
*
|
|
1092
|
+
* @param {object} options - Scheduler options
|
|
1093
|
+
* @param {Function} options.executeHook - Hook execution function
|
|
1094
|
+
* @param {number} options.tickInterval - Scheduler tick interval (default: 1000ms)
|
|
1095
|
+
*/
|
|
1096
|
+
constructor(options = {}) {
|
|
1097
|
+
this.schedules = /* @__PURE__ */ new Map();
|
|
1098
|
+
this.executeHook = options.executeHook || (async () => {
|
|
1099
|
+
});
|
|
1100
|
+
this.tickInterval = options.tickInterval || 1e3;
|
|
1101
|
+
this.ticker = null;
|
|
1102
|
+
this.running = false;
|
|
1103
|
+
this.lastTick = null;
|
|
1104
|
+
this.idleThreshold = options.idleThreshold || 5e3;
|
|
1105
|
+
this.idleStart = Date.now();
|
|
1106
|
+
this.startupQueue = [];
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Register a scheduled hook
|
|
1110
|
+
*
|
|
1111
|
+
* @param {import('./define-hook.mjs').Hook} hook - Hook to schedule
|
|
1112
|
+
* @param {object} config - Schedule configuration
|
|
1113
|
+
* @returns {ScheduledHook} - Registered scheduled hook
|
|
1114
|
+
*/
|
|
1115
|
+
register(hook, config) {
|
|
1116
|
+
const validConfig = ScheduleConfigSchema.parse(config);
|
|
1117
|
+
const scheduled = {
|
|
1118
|
+
id: validConfig.id,
|
|
1119
|
+
hook,
|
|
1120
|
+
type: validConfig.type,
|
|
1121
|
+
expression: validConfig.expression,
|
|
1122
|
+
interval: validConfig.intervalMs,
|
|
1123
|
+
idleTimeout: validConfig.idleTimeoutMs,
|
|
1124
|
+
enabled: validConfig.enabled,
|
|
1125
|
+
lastRun: null,
|
|
1126
|
+
nextRun: this._calculateNextRun(validConfig),
|
|
1127
|
+
runCount: 0,
|
|
1128
|
+
maxRuns: validConfig.maxRuns,
|
|
1129
|
+
metadata: validConfig.metadata || {}
|
|
1130
|
+
};
|
|
1131
|
+
this.schedules.set(validConfig.id, scheduled);
|
|
1132
|
+
if (validConfig.type === "startup") {
|
|
1133
|
+
this.startupQueue.push(scheduled);
|
|
1134
|
+
}
|
|
1135
|
+
return scheduled;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Unregister a scheduled hook
|
|
1139
|
+
*
|
|
1140
|
+
* @param {string} id - Schedule ID to remove
|
|
1141
|
+
*/
|
|
1142
|
+
unregister(id) {
|
|
1143
|
+
this.schedules.delete(id);
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Start the scheduler
|
|
1147
|
+
*/
|
|
1148
|
+
start() {
|
|
1149
|
+
if (this.running) return;
|
|
1150
|
+
this.running = true;
|
|
1151
|
+
this.lastTick = /* @__PURE__ */ new Date();
|
|
1152
|
+
this._executeStartupHooks();
|
|
1153
|
+
this.ticker = setInterval(() => this._tick(), this.tickInterval);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Stop the scheduler
|
|
1157
|
+
*/
|
|
1158
|
+
stop() {
|
|
1159
|
+
if (!this.running) return;
|
|
1160
|
+
this.running = false;
|
|
1161
|
+
if (this.ticker) {
|
|
1162
|
+
clearInterval(this.ticker);
|
|
1163
|
+
this.ticker = null;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Notify scheduler of activity (resets idle timer)
|
|
1168
|
+
*/
|
|
1169
|
+
notifyActivity() {
|
|
1170
|
+
this.idleStart = Date.now();
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Get scheduler statistics
|
|
1174
|
+
*
|
|
1175
|
+
* @returns {object} - Scheduler stats
|
|
1176
|
+
*/
|
|
1177
|
+
getStats() {
|
|
1178
|
+
const schedules = Array.from(this.schedules.values());
|
|
1179
|
+
return {
|
|
1180
|
+
totalSchedules: schedules.length,
|
|
1181
|
+
enabledSchedules: schedules.filter((s) => s.enabled).length,
|
|
1182
|
+
totalRuns: schedules.reduce((sum, s) => sum + s.runCount, 0),
|
|
1183
|
+
byType: {
|
|
1184
|
+
cron: schedules.filter((s) => s.type === "cron").length,
|
|
1185
|
+
interval: schedules.filter((s) => s.type === "interval").length,
|
|
1186
|
+
idle: schedules.filter((s) => s.type === "idle").length,
|
|
1187
|
+
startup: schedules.filter((s) => s.type === "startup").length
|
|
1188
|
+
},
|
|
1189
|
+
running: this.running,
|
|
1190
|
+
idleSince: this.idleStart
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Execute startup hooks
|
|
1195
|
+
* @private
|
|
1196
|
+
*/
|
|
1197
|
+
async _executeStartupHooks() {
|
|
1198
|
+
for (const scheduled of this.startupQueue) {
|
|
1199
|
+
if (scheduled.enabled && scheduled.runCount === 0) {
|
|
1200
|
+
await this._executeScheduled(scheduled);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
this.startupQueue = [];
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Scheduler tick - check and execute due hooks
|
|
1207
|
+
* @private
|
|
1208
|
+
*/
|
|
1209
|
+
async _tick() {
|
|
1210
|
+
const now = /* @__PURE__ */ new Date();
|
|
1211
|
+
const isIdle = Date.now() - this.idleStart > this.idleThreshold;
|
|
1212
|
+
for (const scheduled of this.schedules.values()) {
|
|
1213
|
+
if (!scheduled.enabled) continue;
|
|
1214
|
+
if (scheduled.maxRuns && scheduled.runCount >= scheduled.maxRuns) continue;
|
|
1215
|
+
let shouldRun = false;
|
|
1216
|
+
switch (scheduled.type) {
|
|
1217
|
+
case "interval":
|
|
1218
|
+
shouldRun = scheduled.nextRun && now >= scheduled.nextRun;
|
|
1219
|
+
break;
|
|
1220
|
+
case "idle":
|
|
1221
|
+
shouldRun = isIdle && (!scheduled.lastRun || Date.now() - scheduled.lastRun.getTime() > (scheduled.idleTimeout || this.idleThreshold));
|
|
1222
|
+
break;
|
|
1223
|
+
case "cron":
|
|
1224
|
+
shouldRun = scheduled.nextRun && now >= scheduled.nextRun;
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
if (shouldRun) {
|
|
1228
|
+
await this._executeScheduled(scheduled);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
this.lastTick = now;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Execute a scheduled hook
|
|
1235
|
+
* POKA-YOKE: Circuit breaker disables after 3 consecutive failures (RPN 432 → 43)
|
|
1236
|
+
* @private
|
|
1237
|
+
*/
|
|
1238
|
+
async _executeScheduled(scheduled) {
|
|
1239
|
+
try {
|
|
1240
|
+
scheduled.lastRun = /* @__PURE__ */ new Date();
|
|
1241
|
+
scheduled.runCount++;
|
|
1242
|
+
scheduled.nextRun = this._calculateNextRun(scheduled);
|
|
1243
|
+
await this.executeHook(scheduled.hook, {
|
|
1244
|
+
scheduledId: scheduled.id,
|
|
1245
|
+
runCount: scheduled.runCount,
|
|
1246
|
+
scheduledTime: scheduled.lastRun
|
|
1247
|
+
});
|
|
1248
|
+
scheduled.errorCount = 0;
|
|
1249
|
+
scheduled.lastError = null;
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
scheduled.lastError = error instanceof Error ? error : new Error(String(error));
|
|
1252
|
+
scheduled.errorCount = (scheduled.errorCount || 0) + 1;
|
|
1253
|
+
if (this.onError) {
|
|
1254
|
+
this.onError({
|
|
1255
|
+
scheduledId: scheduled.id,
|
|
1256
|
+
hookName: scheduled.hook?.name || "unknown",
|
|
1257
|
+
error: scheduled.lastError,
|
|
1258
|
+
errorCount: scheduled.errorCount,
|
|
1259
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
console.error(
|
|
1263
|
+
`[POKA-YOKE] Scheduled hook "${scheduled.id}" failed (attempt ${scheduled.errorCount}/3):`,
|
|
1264
|
+
scheduled.lastError.message
|
|
1265
|
+
);
|
|
1266
|
+
if (scheduled.errorCount >= 3) {
|
|
1267
|
+
scheduled.enabled = false;
|
|
1268
|
+
console.warn(
|
|
1269
|
+
`[POKA-YOKE] Scheduled hook "${scheduled.id}" disabled after 3 consecutive failures. Last error: ${scheduled.lastError.message}. Re-enable with scheduler.enable("${scheduled.id}") after fixing the issue.`
|
|
1270
|
+
);
|
|
1271
|
+
if (this.onCircuitOpen) {
|
|
1272
|
+
this.onCircuitOpen({
|
|
1273
|
+
scheduledId: scheduled.id,
|
|
1274
|
+
hookName: scheduled.hook?.name || "unknown",
|
|
1275
|
+
lastError: scheduled.lastError,
|
|
1276
|
+
totalErrors: scheduled.errorCount,
|
|
1277
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Re-enable a disabled scheduled hook
|
|
1285
|
+
*
|
|
1286
|
+
* @param {string} id - Schedule ID to enable
|
|
1287
|
+
* @returns {boolean} - True if enabled, false if not found
|
|
1288
|
+
*/
|
|
1289
|
+
enable(id) {
|
|
1290
|
+
const scheduled = this.schedules.get(id);
|
|
1291
|
+
if (!scheduled) return false;
|
|
1292
|
+
scheduled.enabled = true;
|
|
1293
|
+
scheduled.errorCount = 0;
|
|
1294
|
+
scheduled.lastError = null;
|
|
1295
|
+
return true;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Calculate next run time for a schedule
|
|
1299
|
+
* @private
|
|
1300
|
+
*/
|
|
1301
|
+
_calculateNextRun(config) {
|
|
1302
|
+
const now = /* @__PURE__ */ new Date();
|
|
1303
|
+
switch (config.type) {
|
|
1304
|
+
case "interval":
|
|
1305
|
+
return new Date(now.getTime() + (config.intervalMs || config.interval || 6e4));
|
|
1306
|
+
case "cron":
|
|
1307
|
+
return this._parseCronExpression(config.expression);
|
|
1308
|
+
case "idle":
|
|
1309
|
+
case "startup":
|
|
1310
|
+
return null;
|
|
1311
|
+
// Event-driven, not time-driven
|
|
1312
|
+
default:
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Simple cron expression parser
|
|
1318
|
+
* POKA-YOKE: Strict validation instead of silent fallback (RPN 315 → 0)
|
|
1319
|
+
* Supports intervals in the format star-slash-n (e.g., star-slash-5 = every 5 minutes)
|
|
1320
|
+
* @private
|
|
1321
|
+
*/
|
|
1322
|
+
_parseCronExpression(expression) {
|
|
1323
|
+
if (!expression) {
|
|
1324
|
+
throw new Error(
|
|
1325
|
+
'Cron expression is required for type "cron". Use "*/n" format for intervals (e.g., "*/5" for every 5 minutes).'
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
const intervalMatch = expression.match(/^\*\/(\d+)$/);
|
|
1329
|
+
if (intervalMatch) {
|
|
1330
|
+
const minutes = parseInt(intervalMatch[1], 10);
|
|
1331
|
+
if (minutes < 1 || minutes > 1440) {
|
|
1332
|
+
throw new Error(
|
|
1333
|
+
`Invalid cron interval: */[${minutes}]. Value must be between 1 and 1440 minutes (24 hours). Example: "*/5" for every 5 minutes.`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
return new Date(Date.now() + minutes * 60 * 1e3);
|
|
1337
|
+
}
|
|
1338
|
+
throw new Error(
|
|
1339
|
+
`Invalid cron expression: "${expression}". Supported format: "*/n" where n is minutes (1-1440). Example: "*/5" for every 5 minutes, "*/60" for every hour.`
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
function createHookScheduler(options = {}) {
|
|
1344
|
+
return new HookScheduler(options);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const QualityGateSchema = z.object({
|
|
1348
|
+
name: z.string().min(1, "Gate name is required"),
|
|
1349
|
+
metric: z.string().min(1, "Metric name is required"),
|
|
1350
|
+
operator: z.enum(["gt", "gte", "lt", "lte", "eq", "neq", "between"]),
|
|
1351
|
+
threshold: z.number().or(z.array(z.number())),
|
|
1352
|
+
severity: z.enum(["critical", "major", "minor"]).default("major"),
|
|
1353
|
+
action: z.enum(["block", "warn", "log"]).default("block")
|
|
1354
|
+
}).refine(
|
|
1355
|
+
(data) => {
|
|
1356
|
+
if (data.operator === "between") {
|
|
1357
|
+
return Array.isArray(data.threshold) && data.threshold.length === 2;
|
|
1358
|
+
}
|
|
1359
|
+
return typeof data.threshold === "number";
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
message: "Operator 'between' requires threshold as [min, max] array; other operators require a single number. Example: { operator: 'between', threshold: [10, 90] } or { operator: 'gt', threshold: 50 }"
|
|
1363
|
+
}
|
|
1364
|
+
).refine(
|
|
1365
|
+
(data) => {
|
|
1366
|
+
if (data.operator === "between" && Array.isArray(data.threshold)) {
|
|
1367
|
+
return data.threshold[0] <= data.threshold[1];
|
|
1368
|
+
}
|
|
1369
|
+
return true;
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
message: "For 'between' operator, threshold[0] (min) must be <= threshold[1] (max)"
|
|
1373
|
+
}
|
|
1374
|
+
);
|
|
1375
|
+
const SPCDataPointSchema = z.object({
|
|
1376
|
+
timestamp: z.date().or(z.string().transform((s) => new Date(s))),
|
|
1377
|
+
value: z.number(),
|
|
1378
|
+
subgroup: z.string().optional()
|
|
1379
|
+
});
|
|
1380
|
+
class QualityMetricsCollector {
|
|
1381
|
+
/**
|
|
1382
|
+
* Create a new quality metrics collector
|
|
1383
|
+
*
|
|
1384
|
+
* @param {object} options - Collector options
|
|
1385
|
+
*/
|
|
1386
|
+
constructor(options = {}) {
|
|
1387
|
+
this.dataPoints = /* @__PURE__ */ new Map();
|
|
1388
|
+
this.metrics = /* @__PURE__ */ new Map();
|
|
1389
|
+
this.defects = [];
|
|
1390
|
+
this.maxDataPoints = options.maxDataPoints || 1e3;
|
|
1391
|
+
this.qualityGates = /* @__PURE__ */ new Map();
|
|
1392
|
+
this.auditLog = [];
|
|
1393
|
+
this.maxAuditLogSize = options.maxAuditLogSize || 1e4;
|
|
1394
|
+
this.defectCount = 0;
|
|
1395
|
+
this.totalCount = 0;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Record a data point for a metric
|
|
1399
|
+
*
|
|
1400
|
+
* @param {string} metricName - Metric name
|
|
1401
|
+
* @param {number} value - Measured value
|
|
1402
|
+
* @param {object} context - Additional context
|
|
1403
|
+
*/
|
|
1404
|
+
record(metricName, value, context = {}) {
|
|
1405
|
+
if (!this.dataPoints.has(metricName)) {
|
|
1406
|
+
this.dataPoints.set(metricName, []);
|
|
1407
|
+
}
|
|
1408
|
+
const points = this.dataPoints.get(metricName);
|
|
1409
|
+
points.push(value);
|
|
1410
|
+
if (points.length > this.maxDataPoints) {
|
|
1411
|
+
points.shift();
|
|
1412
|
+
}
|
|
1413
|
+
this.totalCount++;
|
|
1414
|
+
this._checkControlLimits(metricName, value, context);
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Record a defect
|
|
1418
|
+
*
|
|
1419
|
+
* @param {object} defect - Defect information
|
|
1420
|
+
*/
|
|
1421
|
+
recordDefect(defect) {
|
|
1422
|
+
const record = {
|
|
1423
|
+
id: `DEF-${Date.now()}-${this.defects.length}`,
|
|
1424
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1425
|
+
...defect
|
|
1426
|
+
};
|
|
1427
|
+
this.defects.push(record);
|
|
1428
|
+
this.defectCount++;
|
|
1429
|
+
this._auditLog("defect-recorded", record);
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Register a quality gate
|
|
1433
|
+
*
|
|
1434
|
+
* @param {object} gate - Quality gate configuration
|
|
1435
|
+
*/
|
|
1436
|
+
registerQualityGate(gate) {
|
|
1437
|
+
const validated = QualityGateSchema.parse(gate);
|
|
1438
|
+
this.qualityGates.set(validated.name, validated);
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Check a value against quality gates
|
|
1442
|
+
*
|
|
1443
|
+
* @param {string} metricName - Metric to check
|
|
1444
|
+
* @param {number} value - Value to check
|
|
1445
|
+
* @returns {{passed: boolean, violations: object[]}} - Check result
|
|
1446
|
+
*/
|
|
1447
|
+
checkQualityGates(metricName, value) {
|
|
1448
|
+
const violations = [];
|
|
1449
|
+
for (const gate of this.qualityGates.values()) {
|
|
1450
|
+
if (gate.metric !== metricName) continue;
|
|
1451
|
+
const passed = this._evaluateGate(gate, value);
|
|
1452
|
+
if (!passed) {
|
|
1453
|
+
violations.push({
|
|
1454
|
+
gate: gate.name,
|
|
1455
|
+
metric: metricName,
|
|
1456
|
+
value,
|
|
1457
|
+
threshold: gate.threshold,
|
|
1458
|
+
operator: gate.operator,
|
|
1459
|
+
severity: gate.severity,
|
|
1460
|
+
action: gate.action
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return {
|
|
1465
|
+
passed: violations.length === 0,
|
|
1466
|
+
violations
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Calculate Statistical Process Control metrics
|
|
1471
|
+
*
|
|
1472
|
+
* @param {string} metricName - Metric name
|
|
1473
|
+
* @returns {object} - SPC metrics (mean, stdDev, UCL, LCL, Cp, Cpk)
|
|
1474
|
+
*/
|
|
1475
|
+
calculateSPC(metricName) {
|
|
1476
|
+
const points = this.dataPoints.get(metricName) || [];
|
|
1477
|
+
if (points.length < 2) {
|
|
1478
|
+
return { error: "Insufficient data points" };
|
|
1479
|
+
}
|
|
1480
|
+
const mean = points.reduce((sum, v) => sum + v, 0) / points.length;
|
|
1481
|
+
const variance = points.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / (points.length - 1);
|
|
1482
|
+
const stdDev = Math.sqrt(variance);
|
|
1483
|
+
const ucl = mean + 3 * stdDev;
|
|
1484
|
+
const lcl = mean - 3 * stdDev;
|
|
1485
|
+
const metric = this.metrics.get(metricName);
|
|
1486
|
+
let cp = null;
|
|
1487
|
+
let cpk = null;
|
|
1488
|
+
if (metric && metric.ucl !== void 0 && metric.lcl !== void 0) {
|
|
1489
|
+
const usl = metric.ucl;
|
|
1490
|
+
const lsl = metric.lcl;
|
|
1491
|
+
cp = (usl - lsl) / (6 * stdDev);
|
|
1492
|
+
cpk = Math.min((usl - mean) / (3 * stdDev), (mean - lsl) / (3 * stdDev));
|
|
1493
|
+
}
|
|
1494
|
+
return {
|
|
1495
|
+
mean,
|
|
1496
|
+
stdDev,
|
|
1497
|
+
ucl,
|
|
1498
|
+
lcl,
|
|
1499
|
+
cp,
|
|
1500
|
+
cpk,
|
|
1501
|
+
n: points.length,
|
|
1502
|
+
min: Math.min(...points),
|
|
1503
|
+
max: Math.max(...points)
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Detect statistical outliers (defects)
|
|
1508
|
+
*
|
|
1509
|
+
* @param {string} metricName - Metric name
|
|
1510
|
+
* @param {number} sigmaLevel - Sigma level for detection (default: 3)
|
|
1511
|
+
* @returns {number[]} - Indices of outlier data points
|
|
1512
|
+
*/
|
|
1513
|
+
detectOutliers(metricName, sigmaLevel = 3) {
|
|
1514
|
+
const spc = this.calculateSPC(metricName);
|
|
1515
|
+
if (spc.error) return [];
|
|
1516
|
+
const points = this.dataPoints.get(metricName) || [];
|
|
1517
|
+
const threshold = sigmaLevel * spc.stdDev;
|
|
1518
|
+
return points.map((v, i) => ({ value: v, index: i })).filter(({ value }) => Math.abs(value - spc.mean) > threshold).map(({ index }) => index);
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Calculate Defects Per Million Opportunities (DPMO)
|
|
1522
|
+
*
|
|
1523
|
+
* @returns {number} - DPMO value
|
|
1524
|
+
*/
|
|
1525
|
+
calculateDPMO() {
|
|
1526
|
+
if (this.totalCount === 0) return 0;
|
|
1527
|
+
return this.defectCount / this.totalCount * 1e6;
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Calculate Sigma Level
|
|
1531
|
+
*
|
|
1532
|
+
* @returns {number} - Sigma level (higher is better)
|
|
1533
|
+
*/
|
|
1534
|
+
calculateSigmaLevel() {
|
|
1535
|
+
const dpmo = this.calculateDPMO();
|
|
1536
|
+
if (dpmo === 0) return 6;
|
|
1537
|
+
if (dpmo <= 3.4) return 6;
|
|
1538
|
+
if (dpmo <= 233) return 5;
|
|
1539
|
+
if (dpmo <= 6210) return 4;
|
|
1540
|
+
if (dpmo <= 66807) return 3;
|
|
1541
|
+
if (dpmo <= 308538) return 2;
|
|
1542
|
+
if (dpmo <= 691462) return 1;
|
|
1543
|
+
return 0;
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Generate 5 Whys root cause analysis template
|
|
1547
|
+
*
|
|
1548
|
+
* @param {DefectRecord} defect - Defect to analyze
|
|
1549
|
+
* @returns {object} - 5 Whys template
|
|
1550
|
+
*/
|
|
1551
|
+
generateRootCauseTemplate(defect) {
|
|
1552
|
+
return {
|
|
1553
|
+
defectId: defect.id,
|
|
1554
|
+
problem: defect.type,
|
|
1555
|
+
why1: { question: "Why did this happen?", answer: null },
|
|
1556
|
+
why2: { question: "Why did that cause this?", answer: null },
|
|
1557
|
+
why3: { question: "Why was that the case?", answer: null },
|
|
1558
|
+
why4: { question: "Why did that occur?", answer: null },
|
|
1559
|
+
why5: { question: "What is the root cause?", answer: null },
|
|
1560
|
+
rootCause: null,
|
|
1561
|
+
preventiveAction: null,
|
|
1562
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Register a Kaizen improvement event
|
|
1567
|
+
*
|
|
1568
|
+
* @param {object} event - Kaizen event details
|
|
1569
|
+
*/
|
|
1570
|
+
registerKaizenEvent(event) {
|
|
1571
|
+
const record = {
|
|
1572
|
+
id: `KAIZEN-${Date.now()}`,
|
|
1573
|
+
type: "kaizen-event",
|
|
1574
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1575
|
+
status: "open",
|
|
1576
|
+
...event
|
|
1577
|
+
};
|
|
1578
|
+
this._auditLog("kaizen-registered", record);
|
|
1579
|
+
return record;
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Get quality summary report
|
|
1583
|
+
*
|
|
1584
|
+
* @returns {object} - Quality summary
|
|
1585
|
+
*/
|
|
1586
|
+
getSummary() {
|
|
1587
|
+
return {
|
|
1588
|
+
totalMeasurements: this.totalCount,
|
|
1589
|
+
totalDefects: this.defectCount,
|
|
1590
|
+
dpmo: this.calculateDPMO(),
|
|
1591
|
+
sigmaLevel: this.calculateSigmaLevel(),
|
|
1592
|
+
metricsTracked: this.metrics.size,
|
|
1593
|
+
dataPointsStored: Array.from(this.dataPoints.values()).reduce(
|
|
1594
|
+
(sum, arr) => sum + arr.length,
|
|
1595
|
+
0
|
|
1596
|
+
),
|
|
1597
|
+
qualityGatesRegistered: this.qualityGates.size,
|
|
1598
|
+
auditLogEntries: this.auditLog.length,
|
|
1599
|
+
recentDefects: this.defects.slice(-10)
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Export audit trail
|
|
1604
|
+
*
|
|
1605
|
+
* @param {object} options - Export options
|
|
1606
|
+
* @returns {Array} - Audit trail entries
|
|
1607
|
+
*/
|
|
1608
|
+
exportAuditTrail(options = {}) {
|
|
1609
|
+
let entries = [...this.auditLog];
|
|
1610
|
+
if (options.startTime) {
|
|
1611
|
+
entries = entries.filter((e) => e.timestamp >= options.startTime);
|
|
1612
|
+
}
|
|
1613
|
+
if (options.endTime) {
|
|
1614
|
+
entries = entries.filter((e) => e.timestamp <= options.endTime);
|
|
1615
|
+
}
|
|
1616
|
+
if (options.type) {
|
|
1617
|
+
entries = entries.filter((e) => e.type === options.type);
|
|
1618
|
+
}
|
|
1619
|
+
return entries;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Check control limits and record violations
|
|
1623
|
+
* @private
|
|
1624
|
+
*/
|
|
1625
|
+
_checkControlLimits(metricName, value, context) {
|
|
1626
|
+
const spc = this.calculateSPC(metricName);
|
|
1627
|
+
if (spc.error) return;
|
|
1628
|
+
if (value > spc.ucl || value < spc.lcl) {
|
|
1629
|
+
this.recordDefect({
|
|
1630
|
+
type: "control-limit-violation",
|
|
1631
|
+
source: metricName,
|
|
1632
|
+
severity: "major",
|
|
1633
|
+
context: {
|
|
1634
|
+
value,
|
|
1635
|
+
ucl: spc.ucl,
|
|
1636
|
+
lcl: spc.lcl,
|
|
1637
|
+
mean: spc.mean,
|
|
1638
|
+
...context
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Evaluate a quality gate
|
|
1645
|
+
* @private
|
|
1646
|
+
*/
|
|
1647
|
+
_evaluateGate(gate, value) {
|
|
1648
|
+
switch (gate.operator) {
|
|
1649
|
+
case "gt":
|
|
1650
|
+
return value > gate.threshold;
|
|
1651
|
+
case "gte":
|
|
1652
|
+
return value >= gate.threshold;
|
|
1653
|
+
case "lt":
|
|
1654
|
+
return value < gate.threshold;
|
|
1655
|
+
case "lte":
|
|
1656
|
+
return value <= gate.threshold;
|
|
1657
|
+
case "eq":
|
|
1658
|
+
return value === gate.threshold;
|
|
1659
|
+
case "neq":
|
|
1660
|
+
return value !== gate.threshold;
|
|
1661
|
+
case "between":
|
|
1662
|
+
return Array.isArray(gate.threshold) && value >= gate.threshold[0] && value <= gate.threshold[1];
|
|
1663
|
+
default:
|
|
1664
|
+
return true;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Add entry to audit log
|
|
1669
|
+
* POKA-YOKE: Includes size limit to prevent memory exhaustion (RPN 448 → 45)
|
|
1670
|
+
* @private
|
|
1671
|
+
*/
|
|
1672
|
+
_auditLog(type, data) {
|
|
1673
|
+
if (this.auditLog.length >= this.maxAuditLogSize) {
|
|
1674
|
+
const removeCount = Math.max(1, Math.floor(this.maxAuditLogSize * 0.1));
|
|
1675
|
+
this.auditLog.splice(0, removeCount);
|
|
1676
|
+
if (!this._auditEvictionWarned) {
|
|
1677
|
+
console.warn(
|
|
1678
|
+
`[POKA-YOKE] Audit log reached max size (${this.maxAuditLogSize}). Oldest entries will be evicted. Increase maxAuditLogSize if needed.`
|
|
1679
|
+
);
|
|
1680
|
+
this._auditEvictionWarned = true;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
this.auditLog.push({
|
|
1684
|
+
type,
|
|
1685
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1686
|
+
data
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
function createQualityHooks(collector) {
|
|
1691
|
+
return {
|
|
1692
|
+
/**
|
|
1693
|
+
* Create a quality gate validation hook
|
|
1694
|
+
*/
|
|
1695
|
+
createQualityGateHook: (gateName, metricExtractor) => ({
|
|
1696
|
+
name: `quality-gate-${gateName}`,
|
|
1697
|
+
trigger: "quality-gate",
|
|
1698
|
+
validate: (data) => {
|
|
1699
|
+
const value = metricExtractor(data);
|
|
1700
|
+
const result = collector.checkQualityGates(gateName, value);
|
|
1701
|
+
if (!result.passed) {
|
|
1702
|
+
const criticalViolations = result.violations.filter((v) => v.action === "block");
|
|
1703
|
+
return criticalViolations.length === 0;
|
|
1704
|
+
}
|
|
1705
|
+
return true;
|
|
1706
|
+
},
|
|
1707
|
+
metadata: { gateName }
|
|
1708
|
+
}),
|
|
1709
|
+
/**
|
|
1710
|
+
* Create a defect detection hook
|
|
1711
|
+
*/
|
|
1712
|
+
createDefectDetectionHook: (metricName, extractor) => ({
|
|
1713
|
+
name: `defect-detection-${metricName}`,
|
|
1714
|
+
trigger: "defect-detection",
|
|
1715
|
+
validate: (data) => {
|
|
1716
|
+
const value = extractor(data);
|
|
1717
|
+
collector.record(metricName, value);
|
|
1718
|
+
const outliers = collector.detectOutliers(metricName);
|
|
1719
|
+
return outliers.length === 0;
|
|
1720
|
+
},
|
|
1721
|
+
metadata: { metricName }
|
|
1722
|
+
}),
|
|
1723
|
+
/**
|
|
1724
|
+
* Create an audit trail hook
|
|
1725
|
+
*/
|
|
1726
|
+
createAuditTrailHook: (operationType) => ({
|
|
1727
|
+
name: `audit-trail-${operationType}`,
|
|
1728
|
+
trigger: "audit-trail",
|
|
1729
|
+
transform: (data) => {
|
|
1730
|
+
collector._auditLog(operationType, { data, timestamp: /* @__PURE__ */ new Date() });
|
|
1731
|
+
return data;
|
|
1732
|
+
},
|
|
1733
|
+
metadata: { operationType }
|
|
1734
|
+
})
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
export { ChainResultSchema, HookConfigSchema, HookRegistrySchema, HookResultSchema, HookScheduler, HookSchema, HookTriggerSchema, KnowledgeHookManager, QuadPool, QualityGateSchema, QualityMetricsCollector, SPCDataPointSchema, ScheduleConfigSchema, builtinHooks, clearCompiledChainCache, clearHookCaches, clearHooks, compileHookChain, compileValidationOnlyChain, createHookRegistry, createHookScheduler, createPooledTransform, createQualityHooks, defineHook, executeBatch, executeHook, executeHookChain, executeHooksByTrigger, getChainKey, getCompilerStats, getHook, getHookMetadata, getHooksByTrigger, getRegistryStats, hasHook, hasTransformation$1 as hasTransformation, hasValidation$1 as hasValidation, isJitAvailable, isPooledQuad, isValidHook, listHooks, normalizeLanguageTag, normalizeLanguageTagPooled, normalizeNamespace, prewarmHookCache, quadPool, registerHook, rejectBlankNodes, standardValidation, transformBatch, trimLiterals, trimLiteralsPooled, unregisterHook, validateBatch, validateIRIFormat, validateLanguageTag, validateObjectLiteral, validateOnly, validatePredicateIRI, validateSubjectIRI, wouldPassHooks };
|