@unrdf/hooks 26.4.7 → 26.5.5
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/README.md +1 -1
- package/examples/hook-chains/node_modules/.bin/jiti +2 -2
- package/examples/hook-chains/node_modules/.bin/msw +4 -4
- package/examples/hook-chains/node_modules/.bin/terser +2 -2
- package/examples/hook-chains/node_modules/.bin/tsc +2 -2
- package/examples/hook-chains/node_modules/.bin/tsserver +2 -2
- package/examples/hook-chains/node_modules/.bin/tsx +2 -2
- package/examples/hook-chains/node_modules/.bin/validate-hooks +2 -2
- package/examples/hook-chains/node_modules/.bin/vite +4 -4
- package/examples/hook-chains/node_modules/.bin/vitest +2 -2
- package/examples/hook-chains/node_modules/.bin/yaml +4 -4
- package/examples/hook-chains/package.json +3 -3
- package/examples/hooks-marketplace.mjs +10 -10
- package/examples/knowledge-hook-manager-usage.mjs +1 -1
- package/examples/policy-hooks/node_modules/.bin/jiti +2 -2
- package/examples/policy-hooks/node_modules/.bin/msw +4 -4
- package/examples/policy-hooks/node_modules/.bin/terser +2 -2
- package/examples/policy-hooks/node_modules/.bin/tsc +2 -2
- package/examples/policy-hooks/node_modules/.bin/tsserver +2 -2
- package/examples/policy-hooks/node_modules/.bin/tsx +2 -2
- package/examples/policy-hooks/node_modules/.bin/validate-hooks +2 -2
- package/examples/policy-hooks/node_modules/.bin/vite +4 -4
- package/examples/policy-hooks/node_modules/.bin/vitest +2 -2
- package/examples/policy-hooks/node_modules/.bin/yaml +4 -4
- package/examples/policy-hooks/package.json +3 -3
- package/examples/production-policy-chain.mjs +54 -0
- package/examples/semantic-inference-hook.mjs +66 -0
- package/examples/validate-hooks.mjs +1 -1
- package/package.json +10 -5
- package/src/define.mjs +3 -0
- package/src/hooks/condition-evaluator.mjs +2 -0
- package/src/hooks/dependency-graph.mjs +388 -0
- package/src/hooks/observability.mjs +67 -74
- package/src/hooks/parallel-executor.mjs +295 -0
- package/src/hooks/policy-pack.mjs +2 -2
- package/src/hooks/schemas.mjs +1 -0
- package/src/hooks/wasm4pm-conformance-bridge.mjs +25 -0
- package/src/hooks/worker-pool.mjs +478 -0
- package/examples/hook-chains/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/examples/hook-chains/unrdf-hooks-example-chains-5.0.0.tgz +0 -0
- package/examples/policy-hooks/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/examples/policy-hooks/unrdf-hooks-example-policy-5.0.0.tgz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unrdf/hooks",
|
|
3
|
-
"version": "26.
|
|
3
|
+
"version": "26.5.5",
|
|
4
4
|
"description": "UNRDF Knowledge Hooks - Policy Definition and Execution Framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/index.mjs",
|
|
12
12
|
"./define": "./src/define.mjs",
|
|
13
|
-
"./executor": "./src/executor.mjs"
|
|
13
|
+
"./executor": "./src/executor.mjs",
|
|
14
|
+
"./parallel-executor": "./src/hooks/parallel-executor.mjs",
|
|
15
|
+
"./dependency-graph": "./src/hooks/dependency-graph.mjs",
|
|
16
|
+
"./worker-pool": "./src/hooks/worker-pool.mjs"
|
|
14
17
|
},
|
|
15
18
|
"sideEffects": false,
|
|
16
19
|
"files": [
|
|
@@ -48,12 +51,14 @@
|
|
|
48
51
|
],
|
|
49
52
|
"dependencies": {
|
|
50
53
|
"@noble/hashes": "^1.4.0",
|
|
51
|
-
"@
|
|
52
|
-
"@unrdf/
|
|
54
|
+
"@opentelemetry/api": "^1.7.0",
|
|
55
|
+
"@unrdf/core": "workspace:*",
|
|
56
|
+
"@unrdf/otel": "workspace:*",
|
|
57
|
+
"@unrdf/oxigraph": "workspace:*",
|
|
53
58
|
"citty": "^0.1.6",
|
|
54
59
|
"eyereasoner": "^10",
|
|
55
60
|
"sparqljs": "^3.7.3",
|
|
56
|
-
"zod": "^4.
|
|
61
|
+
"zod": "^4.3.6"
|
|
57
62
|
},
|
|
58
63
|
"devDependencies": {
|
|
59
64
|
"@types/node": "^24.10.1",
|
package/src/define.mjs
CHANGED
|
@@ -48,6 +48,9 @@ export const THRESHOLD = 'threshold';
|
|
|
48
48
|
/** @type {'count'} Count-based condition */
|
|
49
49
|
export const COUNT = 'count';
|
|
50
50
|
|
|
51
|
+
/** @type {'semantic-inference'} Open Ontologies reasoning query */
|
|
52
|
+
export const SEMANTIC_INFERENCE = 'semantic-inference';
|
|
53
|
+
|
|
51
54
|
/** @type {'window'} Sliding window temporal condition */
|
|
52
55
|
export const WINDOW = 'window';
|
|
53
56
|
|
|
@@ -403,6 +403,8 @@ export async function evaluateCondition(condition, graph, options = {}) {
|
|
|
403
403
|
return await evaluateWindow(condition, graph, resolver, env, options);
|
|
404
404
|
case 'n3':
|
|
405
405
|
return await evaluateN3(condition, graph, resolver, env);
|
|
406
|
+
case 'semantic-inference':
|
|
407
|
+
return await evaluateSemanticInference(condition, graph, resolver, env);
|
|
406
408
|
default:
|
|
407
409
|
throw new Error(`Unsupported condition kind: ${condition.kind}`);
|
|
408
410
|
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Dependency Graph - Topological sort with cycle detection for parallel hook execution
|
|
3
|
+
* @module hooks/dependency-graph
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
/* ========================================================================= */
|
|
9
|
+
/* Zod Schemas */
|
|
10
|
+
/* ========================================================================= */
|
|
11
|
+
|
|
12
|
+
export const HookDependencySchema = z.object({
|
|
13
|
+
hookId: z.string(),
|
|
14
|
+
dependsOn: z.array(z.string()).optional().default([]),
|
|
15
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/* ========================================================================= */
|
|
19
|
+
/* Dependency Graph Class */
|
|
20
|
+
/* ========================================================================= */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dependency graph for topological sorting and cycle detection.
|
|
24
|
+
*
|
|
25
|
+
* @class DependencyGraph
|
|
26
|
+
*/
|
|
27
|
+
export class DependencyGraph {
|
|
28
|
+
/**
|
|
29
|
+
* Create a new dependency graph.
|
|
30
|
+
*/
|
|
31
|
+
constructor() {
|
|
32
|
+
/** @type {Map<string, Set<string>>} - hookId → set of dependencies */
|
|
33
|
+
this.dependencies = new Map();
|
|
34
|
+
|
|
35
|
+
/** @type {Map<string, Set<string>>} - hookId → set of dependents (reverse graph) */
|
|
36
|
+
this.dependents = new Map();
|
|
37
|
+
|
|
38
|
+
/** @type {Set<string>} - all registered hook IDs */
|
|
39
|
+
this.nodes = new Set();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Add a node (hook) to the graph.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} hookId - Hook identifier
|
|
46
|
+
* @param {string[]} [dependencies=[]] - List of hook IDs this hook depends on
|
|
47
|
+
*/
|
|
48
|
+
addNode(hookId, dependencies = []) {
|
|
49
|
+
const nodeExists = this.nodes.has(hookId);
|
|
50
|
+
|
|
51
|
+
// Initialize dependency and dependent sets
|
|
52
|
+
this.dependencies.set(hookId, new Set(dependencies));
|
|
53
|
+
this.dependents.set(hookId, new Set());
|
|
54
|
+
this.nodes.add(hookId);
|
|
55
|
+
|
|
56
|
+
// Update reverse graph (dependents)
|
|
57
|
+
for (const depId of dependencies) {
|
|
58
|
+
// Add the dependency as a node if it doesn't exist
|
|
59
|
+
if (!this.nodes.has(depId)) {
|
|
60
|
+
this.nodes.add(depId);
|
|
61
|
+
this.dependencies.set(depId, new Set());
|
|
62
|
+
this.dependents.set(depId, new Set());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add current hook as dependent of the dependency
|
|
66
|
+
this.dependents.get(depId).add(hookId);
|
|
67
|
+
|
|
68
|
+
// If node already existed, remove old dependency relationships
|
|
69
|
+
if (nodeExists) {
|
|
70
|
+
// Note: We're keeping the new dependencies, so old ones are effectively replaced
|
|
71
|
+
// The dependents sets are updated above
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Add multiple nodes to the graph.
|
|
78
|
+
*
|
|
79
|
+
* @param {Array<{hookId: string, dependsOn?: string[]}>} nodes - Nodes to add
|
|
80
|
+
*/
|
|
81
|
+
addNodes(nodes) {
|
|
82
|
+
for (const node of nodes) {
|
|
83
|
+
this.addNode(node.hookId, node.dependsOn || []);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Remove a node from the graph.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} hookId - Hook identifier to remove
|
|
91
|
+
* @returns {boolean} True if node was removed
|
|
92
|
+
*/
|
|
93
|
+
removeNode(hookId) {
|
|
94
|
+
if (!this.nodes.has(hookId)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Remove from dependency sets of dependent nodes
|
|
99
|
+
const dependents = this.dependents.get(hookId) || new Set();
|
|
100
|
+
for (const dependentId of dependents) {
|
|
101
|
+
this.dependencies.get(dependentId)?.delete(hookId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Remove from dependent sets of dependency nodes
|
|
105
|
+
const dependencies = this.dependencies.get(hookId) || new Set();
|
|
106
|
+
for (const depId of dependencies) {
|
|
107
|
+
this.dependents.get(depId)?.delete(hookId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Clean up maps
|
|
111
|
+
this.dependencies.delete(hookId);
|
|
112
|
+
this.dependents.delete(hookId);
|
|
113
|
+
this.nodes.delete(hookId);
|
|
114
|
+
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if the graph contains a cycle.
|
|
120
|
+
*
|
|
121
|
+
* Uses depth-first search with coloring (white/gray/black) to detect back edges.
|
|
122
|
+
*
|
|
123
|
+
* @returns {{hasCycle: boolean, cycle?: string[]}} - Cycle detection result
|
|
124
|
+
*/
|
|
125
|
+
detectCycle() {
|
|
126
|
+
const WHITE = 0; // Unvisited
|
|
127
|
+
const GRAY = 1; // Visiting (in current path)
|
|
128
|
+
const BLACK = 2; // Visited (not in current path)
|
|
129
|
+
|
|
130
|
+
/** @type {Map<string, number>} */
|
|
131
|
+
const color = new Map();
|
|
132
|
+
|
|
133
|
+
// Initialize all nodes as white
|
|
134
|
+
for (const node of this.nodes) {
|
|
135
|
+
color.set(node, WHITE);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @type {string[]} */
|
|
139
|
+
const cycle = [];
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Depth-first search to detect back edges.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} node - Current node
|
|
145
|
+
* @returns {boolean} - True if cycle found
|
|
146
|
+
*/
|
|
147
|
+
const dfs = (node) => {
|
|
148
|
+
color.set(node, GRAY);
|
|
149
|
+
|
|
150
|
+
const dependencies = this.dependencies.get(node) || new Set();
|
|
151
|
+
for (const dep of dependencies) {
|
|
152
|
+
const depColor = color.get(dep);
|
|
153
|
+
|
|
154
|
+
if (depColor === GRAY) {
|
|
155
|
+
// Back edge found - cycle detected
|
|
156
|
+
cycle.push(node, dep);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (depColor === WHITE) {
|
|
161
|
+
if (dfs(dep)) {
|
|
162
|
+
cycle.push(node);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
color.set(node, BLACK);
|
|
169
|
+
return false;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Run DFS from each white node
|
|
173
|
+
for (const node of this.nodes) {
|
|
174
|
+
if (color.get(node) === WHITE) {
|
|
175
|
+
if (dfs(node)) {
|
|
176
|
+
// Reverse to get correct order
|
|
177
|
+
cycle.reverse();
|
|
178
|
+
return { hasCycle: true, cycle };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { hasCycle: false };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Perform topological sort (Kahn's algorithm).
|
|
188
|
+
*
|
|
189
|
+
* Returns an array of layers, where each layer contains hooks that can be
|
|
190
|
+
* executed in parallel. Hooks in layer N depend only on hooks in layers < N.
|
|
191
|
+
*
|
|
192
|
+
* @returns {{layers: string[][], hasCycle: boolean, cycle?: string[]}} - Topological sort result
|
|
193
|
+
*/
|
|
194
|
+
topologicalSort() {
|
|
195
|
+
// First check for cycles
|
|
196
|
+
const cycleDetection = this.detectCycle();
|
|
197
|
+
if (cycleDetection.hasCycle) {
|
|
198
|
+
return {
|
|
199
|
+
layers: [],
|
|
200
|
+
hasCycle: true,
|
|
201
|
+
cycle: cycleDetection.cycle,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @type {Map<string, number>} - hookId → in-degree (number of unmet dependencies) */
|
|
206
|
+
const inDegree = new Map();
|
|
207
|
+
|
|
208
|
+
// Initialize in-degrees
|
|
209
|
+
for (const node of this.nodes) {
|
|
210
|
+
const deps = this.dependencies.get(node) || new Set();
|
|
211
|
+
// Only count dependencies that are actually registered nodes
|
|
212
|
+
inDegree.set(node, [...deps].filter(dep => this.nodes.has(dep)).length);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** @type {string[]} - Queue of nodes with in-degree 0 */
|
|
216
|
+
const queue = [];
|
|
217
|
+
|
|
218
|
+
// Find all nodes with in-degree 0
|
|
219
|
+
for (const [node, degree] of inDegree) {
|
|
220
|
+
if (degree === 0) {
|
|
221
|
+
queue.push(node);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @type {string[][]} - Result layers */
|
|
226
|
+
const layers = [];
|
|
227
|
+
|
|
228
|
+
// Process nodes in batches (layers)
|
|
229
|
+
while (queue.length > 0) {
|
|
230
|
+
// Current layer: all nodes currently in queue can execute in parallel
|
|
231
|
+
layers.push([...queue]);
|
|
232
|
+
|
|
233
|
+
// Process all nodes in current layer
|
|
234
|
+
/** @type {string[]} */
|
|
235
|
+
const currentLayer = [...queue];
|
|
236
|
+
queue.length = 0; // Clear queue
|
|
237
|
+
|
|
238
|
+
// For each node in current layer, reduce in-degree of its dependents
|
|
239
|
+
for (const node of currentLayer) {
|
|
240
|
+
const dependents = this.dependents.get(node) || new Set();
|
|
241
|
+
for (const dependent of dependents) {
|
|
242
|
+
const newDegree = (inDegree.get(dependent) || 0) - 1;
|
|
243
|
+
inDegree.set(dependent, newDegree);
|
|
244
|
+
|
|
245
|
+
// If in-degree becomes 0, add to next layer
|
|
246
|
+
if (newDegree === 0) {
|
|
247
|
+
queue.push(dependent);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check if all nodes were processed (should be true if no cycles)
|
|
254
|
+
const processedCount = layers.reduce((sum, layer) => sum + layer.length, 0);
|
|
255
|
+
const hasCycle = processedCount !== this.nodes.size;
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
layers,
|
|
259
|
+
hasCycle,
|
|
260
|
+
cycle: hasCycle ? [] : undefined,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get all dependencies for a hook (transitive closure).
|
|
266
|
+
*
|
|
267
|
+
* @param {string} hookId - Hook identifier
|
|
268
|
+
* @returns {Set<string>} - Set of all transitive dependencies
|
|
269
|
+
*/
|
|
270
|
+
getTransitiveDependencies(hookId) {
|
|
271
|
+
const visited = new Set();
|
|
272
|
+
const result = new Set();
|
|
273
|
+
|
|
274
|
+
const dfs = (node) => {
|
|
275
|
+
if (visited.has(node)) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
visited.add(node);
|
|
280
|
+
const deps = this.dependencies.get(node) || new Set();
|
|
281
|
+
for (const dep of deps) {
|
|
282
|
+
result.add(dep);
|
|
283
|
+
dfs(dep);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
dfs(hookId);
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get all dependents for a hook (transitive closure).
|
|
293
|
+
*
|
|
294
|
+
* @param {string} hookId - Hook identifier
|
|
295
|
+
* @returns {Set<string>} - Set of all transitive dependents
|
|
296
|
+
*/
|
|
297
|
+
getTransitiveDependents(hookId) {
|
|
298
|
+
const visited = new Set();
|
|
299
|
+
const result = new Set();
|
|
300
|
+
|
|
301
|
+
const dfs = (node) => {
|
|
302
|
+
if (visited.has(node)) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
visited.add(node);
|
|
307
|
+
const deps = this.dependents.get(node) || new Set();
|
|
308
|
+
for (const dep of deps) {
|
|
309
|
+
result.add(dep);
|
|
310
|
+
dfs(dep);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
dfs(hookId);
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get statistics about the dependency graph.
|
|
320
|
+
*
|
|
321
|
+
* @returns {{nodeCount: number, edgeCount: number, maxDepth: number, avgBranchingFactor: number}}
|
|
322
|
+
*/
|
|
323
|
+
getStats() {
|
|
324
|
+
const nodeCount = this.nodes.size;
|
|
325
|
+
let edgeCount = 0;
|
|
326
|
+
|
|
327
|
+
for (const [, deps] of this.dependencies) {
|
|
328
|
+
edgeCount += deps.size;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Calculate max depth (longest path)
|
|
332
|
+
const { layers } = this.topologicalSort();
|
|
333
|
+
const maxDepth = layers.length;
|
|
334
|
+
|
|
335
|
+
// Calculate average branching factor (avg dependents per node)
|
|
336
|
+
const avgBranchingFactor =
|
|
337
|
+
nodeCount > 0
|
|
338
|
+
? [...this.dependents.values()].reduce((sum, deps) => sum + deps.size, 0) / nodeCount
|
|
339
|
+
: 0;
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
nodeCount,
|
|
343
|
+
edgeCount,
|
|
344
|
+
maxDepth,
|
|
345
|
+
avgBranchingFactor: parseFloat(avgBranchingFactor.toFixed(2)),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Clear all nodes from the graph.
|
|
351
|
+
*/
|
|
352
|
+
clear() {
|
|
353
|
+
this.dependencies.clear();
|
|
354
|
+
this.dependents.clear();
|
|
355
|
+
this.nodes.clear();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* ========================================================================= */
|
|
360
|
+
/* Factory Functions */
|
|
361
|
+
/* ========================================================================= */
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Create a dependency graph from hook definitions.
|
|
365
|
+
*
|
|
366
|
+
* @param {Array<{id: string, dependsOn?: string[]}>} hooks - Hook definitions
|
|
367
|
+
* @returns {DependencyGraph} - Populated dependency graph
|
|
368
|
+
*/
|
|
369
|
+
export function createDependencyGraph(hooks) {
|
|
370
|
+
const graph = new DependencyGraph();
|
|
371
|
+
|
|
372
|
+
for (const hook of hooks) {
|
|
373
|
+
graph.addNode(hook.id, hook.dependsOn || []);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return graph;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Build execution layers from hooks (convenience function).
|
|
381
|
+
*
|
|
382
|
+
* @param {Array<{id: string, dependsOn?: string[]}>} hooks - Hook definitions
|
|
383
|
+
* @returns {{layers: any[][], hasCycle: boolean, cycle?: string[]}} - Topological sort result
|
|
384
|
+
*/
|
|
385
|
+
export function buildExecutionLayers(hooks) {
|
|
386
|
+
const graph = createDependencyGraph(hooks);
|
|
387
|
+
return graph.topologicalSort();
|
|
388
|
+
}
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @description
|
|
6
6
|
* Implements comprehensive observability with OpenTelemetry traces, metrics,
|
|
7
|
-
* and logging for the UNRDF Knowledge Engine.
|
|
8
|
-
* error isolation, and performance tracking.
|
|
7
|
+
* and logging for the UNRDF Knowledge Engine. Uses daemon SDK as single source of truth.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import { _randomUUID } from 'crypto';
|
|
@@ -13,6 +12,35 @@ import { _z } from 'zod';
|
|
|
13
12
|
import { ObservabilityConfigSchema, PerformanceMetricsSchema } from './schemas.mjs';
|
|
14
13
|
import { getRuntimeConfig } from '../context/config.mjs';
|
|
15
14
|
|
|
15
|
+
// Import daemon SDK functions (single source of truth)
|
|
16
|
+
let getTracer, getMeter;
|
|
17
|
+
try {
|
|
18
|
+
const daemonSdk = await import('@unrdf/daemon/integrations/otel-sdk.mjs');
|
|
19
|
+
getTracer = daemonSdk.getTracer;
|
|
20
|
+
getMeter = daemonSdk.getMeter;
|
|
21
|
+
} catch {
|
|
22
|
+
// Daemon SDK not available (e.g., in standalone hooks usage)
|
|
23
|
+
console.warn('[Observability] Daemon SDK not available - will use fallback mode');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Import generated semantic convention constants
|
|
27
|
+
import {
|
|
28
|
+
ATTR_UNRDF_HOOK_ID,
|
|
29
|
+
ATTR_UNRDF_HOOK_NAME,
|
|
30
|
+
ATTR_UNRDF_HOOK_CONDITION_KIND,
|
|
31
|
+
ATTR_UNRDF_HOOK_EFFECT_KIND,
|
|
32
|
+
ATTR_UNRDF_HOOK_ENABLED,
|
|
33
|
+
ATTR_UNRDF_HOOK_SATISFIED,
|
|
34
|
+
ATTR_UNRDF_HOOK_PRIORITY,
|
|
35
|
+
ATTR_UNRDF_HOOK_QUADS_ADDED,
|
|
36
|
+
ATTR_UNRDF_HOOK_HOOKS_SATISFIED,
|
|
37
|
+
ATTR_UNRDF_HOOK_HOOKS_TOTAL,
|
|
38
|
+
ATTR_UNRDF_TRANSACTION_ID,
|
|
39
|
+
ATTR_UNRDF_TRANSACTION_ISOLATION_LEVEL,
|
|
40
|
+
ATTR_UNRDF_TRANSACTION_OPERATION_COUNT,
|
|
41
|
+
ATTR_UNRDF_TRANSACTION_RESULT,
|
|
42
|
+
} from '@unrdf/otel/generated';
|
|
43
|
+
|
|
16
44
|
/**
|
|
17
45
|
* OpenTelemetry observability manager
|
|
18
46
|
*/
|
|
@@ -48,63 +76,29 @@ export class ObservabilityManager {
|
|
|
48
76
|
if (this.initialized) return;
|
|
49
77
|
|
|
50
78
|
try {
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
headers: this.config.headers,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const metricExporter = new OTLPMetricExporter({
|
|
76
|
-
url: this.config.endpoint ? `${this.config.endpoint}/v1/metrics` : undefined,
|
|
77
|
-
headers: this.config.headers,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Create metric reader
|
|
81
|
-
const metricReader = new PeriodicExportingMetricReader({
|
|
82
|
-
exporter: metricExporter,
|
|
83
|
-
exportIntervalMillis: this.config.scheduledDelayMillis,
|
|
84
|
-
exportTimeoutMillis: this.config.exportTimeoutMillis,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Initialize SDK
|
|
88
|
-
const sdk = new NodeSDK({
|
|
89
|
-
resource,
|
|
90
|
-
traceExporter: this.config.enableTracing ? traceExporter : undefined,
|
|
91
|
-
metricReader: this.config.enableMetrics ? metricReader : undefined,
|
|
92
|
-
instrumentations: this.config.enableTracing ? [getNodeAutoInstrumentations()] : [],
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
await sdk.start();
|
|
96
|
-
|
|
97
|
-
// Get tracer and meter
|
|
98
|
-
this.tracer = trace.getTracer(this.config.serviceName, this.config.serviceVersion);
|
|
99
|
-
this.meter = metrics.getMeter(this.config.serviceName, this.config.serviceVersion);
|
|
100
|
-
|
|
101
|
-
// Create custom metrics
|
|
102
|
-
this._createCustomMetrics();
|
|
103
|
-
|
|
104
|
-
this.initialized = true;
|
|
105
|
-
console.log(`[Observability] Initialized with service: ${this.config.serviceName}`);
|
|
79
|
+
// Use daemon SDK as single source of truth
|
|
80
|
+
if (typeof getTracer === 'function' && typeof getMeter === 'function') {
|
|
81
|
+
this.tracer = getTracer();
|
|
82
|
+
this.meter = getMeter();
|
|
83
|
+
|
|
84
|
+
if (!this.tracer || !this.meter) {
|
|
85
|
+
console.warn('[Observability] Daemon SDK not initialized - using fallback');
|
|
86
|
+
this._initializeFallback();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create custom metrics using daemon's meter
|
|
91
|
+
this._createCustomMetrics();
|
|
92
|
+
|
|
93
|
+
this.initialized = true;
|
|
94
|
+
console.log(`[Observability] Initialized with daemon SDK for service: ${this.config.serviceName}`);
|
|
95
|
+
} else {
|
|
96
|
+
// Daemon SDK not available - use fallback
|
|
97
|
+
console.warn('[Observability] Daemon SDK not available - using fallback mode');
|
|
98
|
+
this._initializeFallback();
|
|
99
|
+
}
|
|
106
100
|
} catch (error) {
|
|
107
|
-
console.warn(`[Observability] Failed to initialize
|
|
101
|
+
console.warn(`[Observability] Failed to initialize with daemon SDK: ${error.message}`);
|
|
108
102
|
// Fallback to console logging
|
|
109
103
|
this._initializeFallback();
|
|
110
104
|
}
|
|
@@ -117,42 +111,42 @@ export class ObservabilityManager {
|
|
|
117
111
|
_createCustomMetrics() {
|
|
118
112
|
if (!this.meter) return;
|
|
119
113
|
|
|
120
|
-
// Transaction metrics
|
|
121
|
-
this.transactionCounter = this.meter.createCounter('
|
|
114
|
+
// Transaction metrics (using unrdf.* prefix from registry)
|
|
115
|
+
this.transactionCounter = this.meter.createCounter('unrdf_transactions_total', {
|
|
122
116
|
description: 'Total number of transactions processed',
|
|
123
117
|
});
|
|
124
118
|
|
|
125
|
-
this.transactionDuration = this.meter.createHistogram('
|
|
119
|
+
this.transactionDuration = this.meter.createHistogram('unrdf_transaction_duration_ms', {
|
|
126
120
|
description: 'Transaction processing duration in milliseconds',
|
|
127
121
|
unit: 'ms',
|
|
128
122
|
});
|
|
129
123
|
|
|
130
|
-
this.hookExecutionCounter = this.meter.createCounter('
|
|
124
|
+
this.hookExecutionCounter = this.meter.createCounter('unrdf_hooks_executed_total', {
|
|
131
125
|
description: 'Total number of hooks executed',
|
|
132
126
|
});
|
|
133
127
|
|
|
134
|
-
this.hookDuration = this.meter.createHistogram('
|
|
128
|
+
this.hookDuration = this.meter.createHistogram('unrdf_hook_duration_ms', {
|
|
135
129
|
description: 'Hook execution duration in milliseconds',
|
|
136
130
|
unit: 'ms',
|
|
137
131
|
});
|
|
138
132
|
|
|
139
|
-
this.errorCounter = this.meter.createCounter('
|
|
133
|
+
this.errorCounter = this.meter.createCounter('unrdf_errors_total', {
|
|
140
134
|
description: 'Total number of errors',
|
|
141
135
|
});
|
|
142
136
|
|
|
143
|
-
this.memoryGauge = this.meter.createUpDownCounter('
|
|
137
|
+
this.memoryGauge = this.meter.createUpDownCounter('unrdf_memory_usage_bytes', {
|
|
144
138
|
description: 'Memory usage in bytes',
|
|
145
139
|
});
|
|
146
140
|
|
|
147
|
-
this.cacheHitCounter = this.meter.createCounter('
|
|
141
|
+
this.cacheHitCounter = this.meter.createCounter('unrdf_cache_hits_total', {
|
|
148
142
|
description: 'Total cache hits',
|
|
149
143
|
});
|
|
150
144
|
|
|
151
|
-
this.cacheMissCounter = this.meter.createCounter('
|
|
145
|
+
this.cacheMissCounter = this.meter.createCounter('unrdf_cache_misses_total', {
|
|
152
146
|
description: 'Total cache misses',
|
|
153
147
|
});
|
|
154
148
|
|
|
155
|
-
this.queueDepthGauge = this.meter.createUpDownCounter('
|
|
149
|
+
this.queueDepthGauge = this.meter.createUpDownCounter('unrdf_queue_depth', {
|
|
156
150
|
description: 'Current queue depth',
|
|
157
151
|
});
|
|
158
152
|
}
|
|
@@ -177,10 +171,9 @@ export class ObservabilityManager {
|
|
|
177
171
|
return { transactionId, startTime: Date.now() };
|
|
178
172
|
}
|
|
179
173
|
|
|
180
|
-
const span = this.tracer.startSpan('
|
|
174
|
+
const span = this.tracer.startSpan('unrdf.transaction', {
|
|
181
175
|
attributes: {
|
|
182
|
-
|
|
183
|
-
'kgc.service.name': this.config.serviceName,
|
|
176
|
+
[ATTR_UNRDF_TRANSACTION_ID]: transactionId,
|
|
184
177
|
...attributes,
|
|
185
178
|
},
|
|
186
179
|
});
|
|
@@ -239,11 +232,11 @@ export class ObservabilityManager {
|
|
|
239
232
|
}
|
|
240
233
|
|
|
241
234
|
const parentSpan = this.activeSpans.get(transactionId)?.span;
|
|
242
|
-
const span = this.tracer.startSpan('
|
|
235
|
+
const span = this.tracer.startSpan('unrdf.hook', {
|
|
243
236
|
parent: parentSpan,
|
|
244
237
|
attributes: {
|
|
245
|
-
|
|
246
|
-
|
|
238
|
+
[ATTR_UNRDF_HOOK_ID]: hookId,
|
|
239
|
+
[ATTR_UNRDF_TRANSACTION_ID]: transactionId,
|
|
247
240
|
...attributes,
|
|
248
241
|
},
|
|
249
242
|
});
|