@unrdf/knowledge-engine 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/package.json +64 -0
  4. package/src/browser-shims.mjs +343 -0
  5. package/src/browser.mjs +910 -0
  6. package/src/canonicalize.mjs +414 -0
  7. package/src/condition-cache.mjs +109 -0
  8. package/src/condition-evaluator.mjs +722 -0
  9. package/src/dark-matter-core.mjs +742 -0
  10. package/src/define-hook.mjs +213 -0
  11. package/src/effect-sandbox-browser.mjs +283 -0
  12. package/src/effect-sandbox-worker.mjs +170 -0
  13. package/src/effect-sandbox.mjs +517 -0
  14. package/src/engines/index.mjs +11 -0
  15. package/src/engines/rdf-engine.mjs +299 -0
  16. package/src/file-resolver.mjs +387 -0
  17. package/src/hook-executor-batching.mjs +277 -0
  18. package/src/hook-executor.mjs +870 -0
  19. package/src/hook-management.mjs +150 -0
  20. package/src/index.mjs +93 -0
  21. package/src/ken-parliment.mjs +119 -0
  22. package/src/ken.mjs +149 -0
  23. package/src/knowledge-engine/builtin-rules.mjs +190 -0
  24. package/src/knowledge-engine/inference-engine.mjs +418 -0
  25. package/src/knowledge-engine/knowledge-engine.mjs +317 -0
  26. package/src/knowledge-engine/pattern-dsl.mjs +142 -0
  27. package/src/knowledge-engine/pattern-matcher.mjs +215 -0
  28. package/src/knowledge-engine/rules.mjs +184 -0
  29. package/src/knowledge-engine.mjs +319 -0
  30. package/src/knowledge-hook-engine.mjs +360 -0
  31. package/src/knowledge-hook-manager.mjs +469 -0
  32. package/src/knowledge-substrate-core.mjs +927 -0
  33. package/src/lite.mjs +222 -0
  34. package/src/lockchain-writer-browser.mjs +414 -0
  35. package/src/lockchain-writer.mjs +602 -0
  36. package/src/monitoring/andon-signals.mjs +775 -0
  37. package/src/observability.mjs +531 -0
  38. package/src/parse.mjs +290 -0
  39. package/src/performance-optimizer.mjs +678 -0
  40. package/src/policy-pack.mjs +572 -0
  41. package/src/query-cache.mjs +116 -0
  42. package/src/query-optimizer.mjs +1051 -0
  43. package/src/query.mjs +306 -0
  44. package/src/reason.mjs +350 -0
  45. package/src/resolution-layer.mjs +506 -0
  46. package/src/schemas.mjs +1063 -0
  47. package/src/security/error-sanitizer.mjs +257 -0
  48. package/src/security/path-validator.mjs +194 -0
  49. package/src/security/sandbox-restrictions.mjs +331 -0
  50. package/src/security-validator.mjs +389 -0
  51. package/src/store-cache.mjs +137 -0
  52. package/src/telemetry.mjs +167 -0
  53. package/src/transaction.mjs +810 -0
  54. package/src/utils/adaptive-monitor.mjs +746 -0
  55. package/src/utils/circuit-breaker.mjs +513 -0
  56. package/src/utils/edge-case-handler.mjs +503 -0
  57. package/src/utils/memory-manager.mjs +498 -0
  58. package/src/utils/ring-buffer.mjs +282 -0
  59. package/src/validate.mjs +319 -0
  60. package/src/validators/index.mjs +338 -0
@@ -0,0 +1,503 @@
1
+ /**
2
+ * @file Edge Case Handler - Null/undefined safety utilities
3
+ * @module edge-case-handler
4
+ *
5
+ * @description
6
+ * Centralized utilities for handling edge cases, null/undefined checks,
7
+ * and defensive programming patterns across the knowledge engine.
8
+ *
9
+ * Addresses 45 test failures related to missing null guards.
10
+ */
11
+
12
+ /**
13
+ * Safe property access with optional chaining and default value.
14
+ *
15
+ * @param {any} obj - Object to access
16
+ * @param {string} path - Dot-notation path (e.g., "a.b.c")
17
+ * @param {any} defaultValue - Default value if path not found
18
+ * @returns {any} Property value or default
19
+ *
20
+ * @example
21
+ * safeGet({ a: { b: { c: 42 } } }, 'a.b.c', 0) // 42
22
+ * safeGet({ a: null }, 'a.b.c', 0) // 0
23
+ * safeGet(null, 'a.b.c', 0) // 0
24
+ */
25
+ export function safeGet(obj, path, defaultValue = undefined) {
26
+ if (obj == null || typeof path !== 'string') {
27
+ return defaultValue;
28
+ }
29
+
30
+ return path.split('.').reduce((current, key) => current?.[key], obj) ?? defaultValue;
31
+ }
32
+
33
+ /**
34
+ * Validate that an object has all required properties.
35
+ * Throws TypeError if any properties are missing.
36
+ *
37
+ * @param {Object} obj - Object to validate
38
+ * @param {string[]} props - Required property paths
39
+ * @param {string} [errorMessage] - Custom error message prefix
40
+ * @throws {TypeError} If any required properties are missing
41
+ *
42
+ * @example
43
+ * requireProperties({ a: 1, b: 2 }, ['a', 'b']) // OK
44
+ * requireProperties({ a: 1 }, ['a', 'b'], 'Config') // Throws: "Config: missing b"
45
+ */
46
+ export function requireProperties(obj, props, errorMessage = 'Object') {
47
+ if (obj == null) {
48
+ throw new TypeError(`${errorMessage} cannot be null or undefined`);
49
+ }
50
+
51
+ if (!Array.isArray(props)) {
52
+ throw new TypeError('props must be an array');
53
+ }
54
+
55
+ const missing = [];
56
+ for (const prop of props) {
57
+ if (safeGet(obj, prop) === undefined) {
58
+ missing.push(prop);
59
+ }
60
+ }
61
+
62
+ if (missing.length > 0) {
63
+ throw new TypeError(`${errorMessage}: missing required properties: ${missing.join(', ')}`);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Safe array access with bounds checking.
69
+ *
70
+ * @param {Array} arr - Array to access
71
+ * @param {number} index - Index to access
72
+ * @param {any} defaultValue - Default value if index out of bounds
73
+ * @returns {any} Array element or default value
74
+ *
75
+ * @example
76
+ * safeArrayAccess([1, 2, 3], 1) // 2
77
+ * safeArrayAccess([1, 2, 3], 10, 0) // 0
78
+ * safeArrayAccess(null, 0, 0) // 0
79
+ */
80
+ export function safeArrayAccess(arr, index, defaultValue = undefined) {
81
+ if (!Array.isArray(arr)) {
82
+ return defaultValue;
83
+ }
84
+
85
+ if (typeof index !== 'number' || index < 0 || index >= arr.length) {
86
+ return defaultValue;
87
+ }
88
+
89
+ return arr[index];
90
+ }
91
+
92
+ /**
93
+ * Safe first element access.
94
+ *
95
+ * @param {Array} arr - Array to access
96
+ * @param {any} defaultValue - Default if array is empty
97
+ * @returns {any} First element or default
98
+ */
99
+ export function safeFirst(arr, defaultValue = undefined) {
100
+ return safeArrayAccess(arr, 0, defaultValue);
101
+ }
102
+
103
+ /**
104
+ * Safe last element access.
105
+ *
106
+ * @param {Array} arr - Array to access
107
+ * @param {any} defaultValue - Default if array is empty
108
+ * @returns {any} Last element or default
109
+ */
110
+ export function safeLast(arr, defaultValue = undefined) {
111
+ if (!Array.isArray(arr) || arr.length === 0) {
112
+ return defaultValue;
113
+ }
114
+ return arr[arr.length - 1];
115
+ }
116
+
117
+ /**
118
+ * Check if a value is empty.
119
+ * Works with: null, undefined, arrays, strings, objects, Maps, Sets, RDF Stores.
120
+ *
121
+ * @param {any} value - Value to check
122
+ * @returns {boolean} True if value is empty
123
+ *
124
+ * @example
125
+ * isEmpty(null) // true
126
+ * isEmpty([]) // true
127
+ * isEmpty({ size: 0 }) // true (Store, Map, Set)
128
+ * isEmpty({ a: 1 }) // false
129
+ */
130
+ export function isEmpty(value) {
131
+ // Null/undefined
132
+ if (value == null) return true;
133
+
134
+ // Array
135
+ if (Array.isArray(value)) return value.length === 0;
136
+
137
+ // String
138
+ if (typeof value === 'string') return value.length === 0;
139
+
140
+ // Objects with .size property (Store, Map, Set)
141
+ if (typeof value === 'object' && 'size' in value) {
142
+ return value.size === 0;
143
+ }
144
+
145
+ // Plain objects
146
+ if (typeof value === 'object') {
147
+ return Object.keys(value).length === 0;
148
+ }
149
+
150
+ return false;
151
+ }
152
+
153
+ /**
154
+ * Check if value is not empty.
155
+ *
156
+ * @param {any} value - Value to check
157
+ * @returns {boolean} True if value is not empty
158
+ */
159
+ export function isNotEmpty(value) {
160
+ return !isEmpty(value);
161
+ }
162
+
163
+ /**
164
+ * Detect circular references in an object.
165
+ * Uses WeakSet to track visited objects.
166
+ *
167
+ * @param {any} obj - Object to check
168
+ * @param {WeakSet} [seen] - Internal tracking (don't pass)
169
+ * @returns {boolean} True if circular references detected
170
+ *
171
+ * @example
172
+ * const obj = { a: 1 }
173
+ * obj.self = obj
174
+ * hasCircularRefs(obj) // true
175
+ */
176
+ export function hasCircularRefs(obj, seen = new WeakSet()) {
177
+ // Primitives can't have circular refs
178
+ if (obj == null || typeof obj !== 'object') {
179
+ return false;
180
+ }
181
+
182
+ // Circular reference detected
183
+ if (seen.has(obj)) {
184
+ return true;
185
+ }
186
+
187
+ seen.add(obj);
188
+
189
+ // Check all values
190
+ for (const value of Object.values(obj)) {
191
+ if (hasCircularRefs(value, seen)) {
192
+ return true;
193
+ }
194
+ }
195
+
196
+ return false;
197
+ }
198
+
199
+ /**
200
+ * Get strongly connected components in a graph (for circular ref analysis).
201
+ *
202
+ * @param {Object} graph - Graph as adjacency list { node: [neighbors] }
203
+ * @returns {Array<Array<string>>} Array of strongly connected components
204
+ *
205
+ * @example
206
+ * const graph = { a: ['b'], b: ['c'], c: ['a'] }
207
+ * getStronglyConnectedComponents(graph) // [['a', 'b', 'c']]
208
+ */
209
+ export function getStronglyConnectedComponents(graph) {
210
+ if (!graph || typeof graph !== 'object') {
211
+ return [];
212
+ }
213
+
214
+ const visited = new Set();
215
+ const stack = [];
216
+ const components = [];
217
+
218
+ // DFS to fill stack
219
+ function dfs1(node) {
220
+ if (visited.has(node)) return;
221
+ visited.add(node);
222
+
223
+ const neighbors = graph[node] || [];
224
+ for (const neighbor of neighbors) {
225
+ dfs1(neighbor);
226
+ }
227
+
228
+ stack.push(node);
229
+ }
230
+
231
+ // Get transpose graph
232
+ const transpose = {};
233
+ for (const [node, neighbors] of Object.entries(graph)) {
234
+ for (const neighbor of neighbors) {
235
+ if (!transpose[neighbor]) transpose[neighbor] = [];
236
+ transpose[neighbor].push(node);
237
+ }
238
+ }
239
+
240
+ // DFS on transpose
241
+ function dfs2(node, component) {
242
+ if (visited.has(node)) return;
243
+ visited.add(node);
244
+ component.push(node);
245
+
246
+ const neighbors = transpose[node] || [];
247
+ for (const neighbor of neighbors) {
248
+ dfs2(neighbor, component);
249
+ }
250
+ }
251
+
252
+ // Fill stack
253
+ for (const node of Object.keys(graph)) {
254
+ dfs1(node);
255
+ }
256
+
257
+ // Find components
258
+ visited.clear();
259
+ while (stack.length > 0) {
260
+ const node = stack.pop();
261
+ if (!visited.has(node)) {
262
+ const component = [];
263
+ dfs2(node, component);
264
+ if (component.length > 0) {
265
+ components.push(component);
266
+ }
267
+ }
268
+ }
269
+
270
+ return components;
271
+ }
272
+
273
+ /**
274
+ * Check if a graph has self-referencing nodes.
275
+ *
276
+ * @param {Object} graph - Graph as adjacency list
277
+ * @returns {boolean} True if any node references itself
278
+ */
279
+ export function hasSelfReferences(graph) {
280
+ if (!graph || typeof graph !== 'object') {
281
+ return false;
282
+ }
283
+
284
+ for (const [node, neighbors] of Object.entries(graph)) {
285
+ if (Array.isArray(neighbors) && neighbors.includes(node)) {
286
+ return true;
287
+ }
288
+ }
289
+
290
+ return false;
291
+ }
292
+
293
+ /**
294
+ * Count quads/triples in an RDF graph/store.
295
+ *
296
+ * @param {Object} store - RDF Store instance
297
+ * @returns {number} Number of quads
298
+ */
299
+ export function quadCount(store) {
300
+ if (!store || typeof store !== 'object') {
301
+ return 0;
302
+ }
303
+
304
+ // Check for .size property (n3.Store)
305
+ if ('size' in store && typeof store.size === 'number') {
306
+ return store.size;
307
+ }
308
+
309
+ // Check for .length property
310
+ if ('length' in store && typeof store.length === 'number') {
311
+ return store.length;
312
+ }
313
+
314
+ // Try counting quads
315
+ if (typeof store.getQuads === 'function') {
316
+ return store.getQuads().length;
317
+ }
318
+
319
+ // Try iterating
320
+ if (typeof store[Symbol.iterator] === 'function') {
321
+ return Array.from(store).length;
322
+ }
323
+
324
+ return 0;
325
+ }
326
+
327
+ /**
328
+ * Count timestamps in RDF data.
329
+ *
330
+ * @param {Object} store - RDF Store instance
331
+ * @returns {number} Number of xsd:dateTime literals
332
+ */
333
+ export function timestampCount(store) {
334
+ if (!store || typeof store.getQuads !== 'function') {
335
+ return 0;
336
+ }
337
+
338
+ let count = 0;
339
+ const quads = store.getQuads();
340
+
341
+ for (const quad of quads) {
342
+ const obj = quad.object;
343
+ if (obj?.datatype?.value === 'http://www.w3.org/2001/XMLSchema#dateTime') {
344
+ count++;
345
+ }
346
+ }
347
+
348
+ return count;
349
+ }
350
+
351
+ /**
352
+ * Detect DST (Daylight Saving Time) transitions in timestamp data.
353
+ *
354
+ * @param {Object} store - RDF Store with timestamp data
355
+ * @returns {Array<Object>} DST transitions detected
356
+ */
357
+ export function detectDstTransitions(store) {
358
+ if (!store || typeof store.getQuads !== 'function') {
359
+ return [];
360
+ }
361
+
362
+ const transitions = [];
363
+ const timestamps = [];
364
+
365
+ // Extract all timestamps
366
+ const quads = store.getQuads();
367
+ for (const quad of quads) {
368
+ const obj = quad.object;
369
+ if (obj?.datatype?.value === 'http://www.w3.org/2001/XMLSchema#dateTime') {
370
+ try {
371
+ timestamps.push(new Date(obj.value));
372
+ } catch {
373
+ // Skip invalid timestamps
374
+ }
375
+ }
376
+ }
377
+
378
+ // Sort timestamps
379
+ timestamps.sort((a, b) => a - b);
380
+
381
+ // Detect hour-offset jumps (DST transitions)
382
+ for (let i = 1; i < timestamps.length; i++) {
383
+ const diff = timestamps[i] - timestamps[i - 1];
384
+ const hourDiff = diff / (1000 * 60 * 60);
385
+
386
+ // DST transition is typically 1-hour jump
387
+ if (Math.abs(hourDiff - 1) < 0.1 || Math.abs(hourDiff + 1) < 0.1) {
388
+ transitions.push({
389
+ from: timestamps[i - 1],
390
+ to: timestamps[i],
391
+ offsetHours: hourDiff,
392
+ });
393
+ }
394
+ }
395
+
396
+ return transitions;
397
+ }
398
+
399
+ /**
400
+ * Safe numeric parsing with validation.
401
+ *
402
+ * @param {any} value - Value to parse
403
+ * @param {number} defaultValue - Default if parsing fails
404
+ * @returns {number} Parsed number or default
405
+ */
406
+ export function safeParseNumber(value, defaultValue = 0) {
407
+ if (typeof value === 'number') {
408
+ return isNaN(value) ? defaultValue : value;
409
+ }
410
+
411
+ if (typeof value === 'string') {
412
+ const parsed = parseFloat(value);
413
+ return isNaN(parsed) ? defaultValue : parsed;
414
+ }
415
+
416
+ return defaultValue;
417
+ }
418
+
419
+ /**
420
+ * Safe integer parsing with validation.
421
+ *
422
+ * @param {any} value - Value to parse
423
+ * @param {number} defaultValue - Default if parsing fails
424
+ * @returns {number} Parsed integer or default
425
+ */
426
+ export function safeParseInt(value, defaultValue = 0) {
427
+ if (typeof value === 'number') {
428
+ return Number.isInteger(value) ? value : defaultValue;
429
+ }
430
+
431
+ if (typeof value === 'string') {
432
+ const parsed = parseInt(value, 10);
433
+ return isNaN(parsed) ? defaultValue : parsed;
434
+ }
435
+
436
+ return defaultValue;
437
+ }
438
+
439
+ /**
440
+ * Clamp a number to a range.
441
+ *
442
+ * @param {number} value - Value to clamp
443
+ * @param {number} min - Minimum value
444
+ * @param {number} max - Maximum value
445
+ * @returns {number} Clamped value
446
+ */
447
+ export function clamp(value, min, max) {
448
+ return Math.max(min, Math.min(max, value));
449
+ }
450
+
451
+ /**
452
+ * Safe division (prevents divide by zero).
453
+ *
454
+ * @param {number} numerator - Numerator
455
+ * @param {number} denominator - Denominator
456
+ * @param {number} defaultValue - Default if division by zero
457
+ * @returns {number} Result or default
458
+ */
459
+ export function safeDivide(numerator, denominator, defaultValue = 0) {
460
+ if (denominator === 0) {
461
+ return defaultValue;
462
+ }
463
+ return numerator / denominator;
464
+ }
465
+
466
+ /**
467
+ * Retry an async operation with exponential backoff.
468
+ *
469
+ * @param {Function} fn - Async function to retry
470
+ * @param {Object} options - Retry options
471
+ * @returns {Promise<any>} Result of successful execution
472
+ */
473
+ export async function retry(fn, options = {}) {
474
+ const {
475
+ maxAttempts = 3,
476
+ baseDelay = 100,
477
+ maxDelay = 5000,
478
+ backoffFactor = 2,
479
+ onRetry = null,
480
+ } = options;
481
+
482
+ let lastError;
483
+
484
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
485
+ try {
486
+ return await fn();
487
+ } catch (error) {
488
+ lastError = error;
489
+
490
+ if (attempt < maxAttempts - 1) {
491
+ const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
492
+
493
+ if (onRetry) {
494
+ onRetry(error, attempt + 1, delay);
495
+ }
496
+
497
+ await new Promise(resolve => setTimeout(resolve, delay));
498
+ }
499
+ }
500
+ }
501
+
502
+ throw lastError;
503
+ }