@unrdf/hooks 5.0.1 → 26.4.3

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 (58) hide show
  1. package/dist/index.d.mts +1738 -0
  2. package/dist/index.d.ts +1738 -0
  3. package/dist/index.mjs +1738 -0
  4. package/examples/basic.mjs +113 -0
  5. package/examples/hook-chains/README.md +263 -0
  6. package/examples/hook-chains/node_modules/.bin/jiti +21 -0
  7. package/examples/hook-chains/node_modules/.bin/msw +21 -0
  8. package/examples/hook-chains/node_modules/.bin/terser +21 -0
  9. package/examples/hook-chains/node_modules/.bin/tsc +21 -0
  10. package/examples/hook-chains/node_modules/.bin/tsserver +21 -0
  11. package/examples/hook-chains/node_modules/.bin/tsx +21 -0
  12. package/examples/hook-chains/node_modules/.bin/validate-hooks +21 -0
  13. package/examples/hook-chains/node_modules/.bin/vite +21 -0
  14. package/examples/hook-chains/node_modules/.bin/vitest +21 -0
  15. package/examples/hook-chains/node_modules/.bin/yaml +21 -0
  16. package/examples/hook-chains/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  17. package/examples/hook-chains/package.json +25 -0
  18. package/examples/hook-chains/src/index.mjs +348 -0
  19. package/examples/hook-chains/test/example.test.mjs +252 -0
  20. package/examples/hook-chains/unrdf-hooks-example-chains-5.0.0.tgz +0 -0
  21. package/examples/hook-chains/vitest.config.mjs +14 -0
  22. package/examples/knowledge-hook-manager-usage.mjs +65 -0
  23. package/examples/policy-hooks/README.md +193 -0
  24. package/examples/policy-hooks/node_modules/.bin/jiti +21 -0
  25. package/examples/policy-hooks/node_modules/.bin/msw +21 -0
  26. package/examples/policy-hooks/node_modules/.bin/terser +21 -0
  27. package/examples/policy-hooks/node_modules/.bin/tsc +21 -0
  28. package/examples/policy-hooks/node_modules/.bin/tsserver +21 -0
  29. package/examples/policy-hooks/node_modules/.bin/tsx +21 -0
  30. package/examples/policy-hooks/node_modules/.bin/validate-hooks +21 -0
  31. package/examples/policy-hooks/node_modules/.bin/vite +21 -0
  32. package/examples/policy-hooks/node_modules/.bin/vitest +21 -0
  33. package/examples/policy-hooks/node_modules/.bin/yaml +21 -0
  34. package/examples/policy-hooks/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  35. package/examples/policy-hooks/package.json +25 -0
  36. package/examples/policy-hooks/src/index.mjs +275 -0
  37. package/examples/policy-hooks/test/example.test.mjs +204 -0
  38. package/examples/policy-hooks/unrdf-hooks-example-policy-5.0.0.tgz +0 -0
  39. package/examples/policy-hooks/vitest.config.mjs +14 -0
  40. package/examples/validate-hooks.mjs +154 -0
  41. package/package.json +12 -7
  42. package/src/hooks/builtin-hooks.mjs +72 -48
  43. package/src/hooks/condition-evaluator.mjs +1 -1
  44. package/src/hooks/define-hook.mjs +27 -9
  45. package/src/hooks/effect-sandbox-worker.mjs +1 -1
  46. package/src/hooks/effect-sandbox.mjs +5 -2
  47. package/src/hooks/file-resolver.mjs +2 -2
  48. package/src/hooks/hook-executor.mjs +12 -19
  49. package/src/hooks/policy-pack.mjs +9 -3
  50. package/src/hooks/query-optimizer.mjs +192 -0
  51. package/src/hooks/query.mjs +150 -0
  52. package/src/hooks/schemas.mjs +164 -0
  53. package/src/hooks/security/path-validator.mjs +1 -1
  54. package/src/hooks/security/sandbox-restrictions.mjs +2 -2
  55. package/src/hooks/store-cache.mjs +189 -0
  56. package/src/hooks/validate.mjs +133 -0
  57. package/src/index.mjs +62 -0
  58. package/src/policy-compiler.mjs +503 -0
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { defineHook } from './define-hook.mjs';
7
- import { dataFactory } from '@unrdf/oxigraph';
7
+ import { dataFactory } from '../../../oxigraph/src/index.mjs';
8
8
  import { quadPool } from './quad-pool.mjs';
9
9
 
10
10
  /**
@@ -45,29 +45,31 @@ export const validatePredicateIRI = defineHook({
45
45
  });
46
46
 
47
47
  /**
48
- * Validate that quad object is a Literal.
48
+ * Validate that quad object is a Literal with non-empty value.
49
49
  */
50
50
  export const validateObjectLiteral = defineHook({
51
51
  name: 'validate-object-literal',
52
52
  trigger: 'before-add',
53
53
  validate: quad => {
54
- return quad.object.termType === 'Literal';
54
+ return quad.object.termType === 'Literal' && quad.object.value.length > 0;
55
55
  },
56
56
  metadata: {
57
- description: 'Validates that quad object is a Literal',
57
+ description: 'Validates that quad object is a non-empty Literal',
58
58
  },
59
59
  });
60
60
 
61
61
  /**
62
- * Validate that IRI values are well-formed.
62
+ * Validate that IRI values are well-formed (no spaces, valid URL structure).
63
+ * Only validates subject and predicate (objects can be literals).
63
64
  */
64
65
  export const validateIRIFormat = defineHook({
65
66
  name: 'validate-iri-format',
66
67
  trigger: 'before-add',
67
68
  validate: quad => {
68
69
  const validateIRI = term => {
69
- if (term.termType !== 'NamedNode') {
70
- return true;
70
+ // Check for spaces or invalid characters
71
+ if (/\s/.test(term.value)) {
72
+ return false;
71
73
  }
72
74
  try {
73
75
  new URL(term.value);
@@ -77,27 +79,32 @@ export const validateIRIFormat = defineHook({
77
79
  }
78
80
  };
79
81
 
80
- return validateIRI(quad.subject) && validateIRI(quad.predicate) && validateIRI(quad.object);
82
+ // Only validate subject and predicate (not object, which can be a literal)
83
+ return validateIRI(quad.subject) && validateIRI(quad.predicate);
81
84
  },
82
85
  metadata: {
83
- description: 'Validates that IRI values are well-formed URLs',
86
+ description: 'Validates that subject and predicate IRIs are well-formed URLs without spaces',
84
87
  },
85
88
  });
86
89
 
87
90
  /**
88
- * Validate that literals have language tags if required.
91
+ * Validate that language tags are well-formed (BCP 47 format: en, en-US, etc).
89
92
  */
90
93
  export const validateLanguageTag = defineHook({
91
94
  name: 'validate-language-tag',
92
95
  trigger: 'before-add',
93
96
  validate: quad => {
94
- if (quad.object.termType !== 'Literal') {
97
+ // Skip if no language tag present
98
+ if (!quad.object.language) {
95
99
  return true;
96
100
  }
97
- return quad.object.language !== undefined && quad.object.language !== '';
101
+ // BCP 47 language tag: letters and hyphens only, no underscores
102
+ // Examples: en, en-US, fr, de-DE
103
+ const validTag = /^[a-zA-Z]{2,3}(-[a-zA-Z]{2,4})?$/;
104
+ return validTag.test(quad.object.language);
98
105
  },
99
106
  metadata: {
100
- description: 'Validates that literal objects have language tags',
107
+ description: 'Validates that language tags conform to BCP 47 format',
101
108
  },
102
109
  });
103
110
 
@@ -141,17 +148,19 @@ export const normalizeLanguageTag = defineHook({
141
148
  name: 'normalize-language-tag',
142
149
  trigger: 'before-add',
143
150
  transform: quad => {
144
- if (quad.object.termType !== 'Literal' || !quad.object.language) {
151
+ if (!quad.object.language) {
145
152
  return quad;
146
153
  }
147
154
 
148
- // Use imported dataFactory to create new quad with normalized language tag
149
- return dataFactory.quad(
150
- quad.subject,
151
- quad.predicate,
152
- dataFactory.literal(quad.object.value, quad.object.language.toLowerCase()),
153
- quad.graph
154
- );
155
+ // Create new quad with lowercase language tag
156
+ return {
157
+ ...quad,
158
+ object: {
159
+ ...quad.object,
160
+ value: quad.object.value,
161
+ language: quad.object.language.toLowerCase(),
162
+ },
163
+ };
155
164
  },
156
165
  metadata: {
157
166
  description: 'Normalizes language tags to lowercase',
@@ -169,13 +178,14 @@ export const trimLiterals = defineHook({
169
178
  return quad;
170
179
  }
171
180
 
172
- // Use imported dataFactory to create new quad with trimmed literal
173
- return dataFactory.quad(
174
- quad.subject,
175
- quad.predicate,
176
- dataFactory.literal(quad.object.value.trim(), quad.object.language || quad.object.datatype),
177
- quad.graph
178
- );
181
+ // Create new quad with trimmed literal
182
+ return {
183
+ ...quad,
184
+ object: {
185
+ ...quad.object,
186
+ value: quad.object.value.trim(),
187
+ },
188
+ };
179
189
  },
180
190
  metadata: {
181
191
  description: 'Trims whitespace from literal values',
@@ -187,20 +197,36 @@ export const trimLiterals = defineHook({
187
197
  /* ========================================================================= */
188
198
 
189
199
  /**
190
- * Standard validation for RDF quads.
191
- * Combines IRI validation and predicate validation.
200
+ * Standard validation for RDF quads (includes IRI format validation).
201
+ * Combines IRI validation, predicate validation, and format checks.
192
202
  */
193
203
  export const standardValidation = defineHook({
194
204
  name: 'standard-validation',
195
205
  trigger: 'before-add',
196
206
  validate: quad => {
197
- return (
198
- quad.predicate.termType === 'NamedNode' &&
199
- (quad.subject.termType === 'NamedNode' || quad.subject.termType === 'BlankNode')
200
- );
207
+ // Validate predicate is NamedNode
208
+ if (quad.predicate.termType !== 'NamedNode') {
209
+ return false;
210
+ }
211
+ // Validate subject is NamedNode or BlankNode
212
+ if (quad.subject.termType !== 'NamedNode' && quad.subject.termType !== 'BlankNode') {
213
+ return false;
214
+ }
215
+ // Validate IRI format (no spaces)
216
+ const validateIRI = term => {
217
+ if (term.termType !== 'NamedNode') return true;
218
+ if (/\s/.test(term.value)) return false;
219
+ try {
220
+ new URL(term.value);
221
+ return true;
222
+ } catch {
223
+ return false;
224
+ }
225
+ };
226
+ return validateIRI(quad.subject) && validateIRI(quad.predicate);
201
227
  },
202
228
  metadata: {
203
- description: 'Standard RDF validation rules',
229
+ description: 'Standard RDF validation rules with IRI format checks',
204
230
  },
205
231
  });
206
232
 
@@ -217,21 +243,19 @@ export const normalizeLanguageTagPooled = defineHook({
217
243
  name: 'normalize-language-tag-pooled',
218
244
  trigger: 'before-add',
219
245
  transform: quad => {
220
- // Branchless early return using bitwise (Rust: match arm optimization)
221
- const isLiteral = quad.object.termType === 'Literal';
222
- const hasLanguage = isLiteral && quad.object.language;
223
-
224
- // Branchless: (condition) * value + (!condition) * default
225
- // In JS, we use conditional but optimizer should inline
226
- if (!hasLanguage) return quad;
246
+ if (!quad.object.language) return quad;
227
247
 
228
248
  // Pool-allocated quad (zero-copy transform)
229
- return quadPool.acquire(
249
+ const pooledQuad = quadPool.acquire(
230
250
  quad.subject,
231
251
  quad.predicate,
232
- dataFactory.literal(quad.object.value, quad.object.language.toLowerCase()),
252
+ {
253
+ ...quad.object,
254
+ language: quad.object.language.toLowerCase(),
255
+ },
233
256
  quad.graph
234
257
  );
258
+ return pooledQuad;
235
259
  },
236
260
  metadata: {
237
261
  description: 'Zero-allocation language tag normalization using quad pool',
@@ -247,19 +271,19 @@ export const trimLiteralsPooled = defineHook({
247
271
  name: 'trim-literals-pooled',
248
272
  trigger: 'before-add',
249
273
  transform: quad => {
250
- // Branchless early return
251
274
  if (quad.object.termType !== 'Literal') return quad;
252
275
 
253
276
  const trimmed = quad.object.value.trim();
254
-
255
- // Branchless: avoid allocation if no change (Rust: Cow semantics)
256
277
  if (trimmed === quad.object.value) return quad;
257
278
 
258
279
  // Pool-allocated quad (zero-copy transform)
259
280
  return quadPool.acquire(
260
281
  quad.subject,
261
282
  quad.predicate,
262
- dataFactory.literal(trimmed, quad.object.language || quad.object.datatype),
283
+ {
284
+ ...quad.object,
285
+ value: trimmed,
286
+ },
263
287
  quad.graph
264
288
  );
265
289
  },
@@ -11,7 +11,7 @@ import { createFileResolver } from './file-resolver.mjs';
11
11
  import { ask, select } from './query.mjs';
12
12
  import { validateShacl } from './validate.mjs';
13
13
  import { createQueryOptimizer } from './query-optimizer.mjs';
14
- import { createStore } from '@unrdf/oxigraph';
14
+ import { createStore } from '../../../oxigraph/src/index.mjs';
15
15
 
16
16
  /**
17
17
  * Evaluate a hook condition against a graph.
@@ -95,12 +95,21 @@ export const HookTriggerSchema = z.enum([
95
95
  ]);
96
96
 
97
97
  export const HookConfigSchema = z.object({
98
- name: z.string().min(1, 'Hook name is required'),
98
+ name: z.string().min(1, 'Hook name is required').optional(),
99
99
  trigger: HookTriggerSchema,
100
100
  // Note: No return type enforcement - runtime POKA-YOKE guard handles non-boolean returns
101
101
  validate: z.function().optional(),
102
102
  transform: z.function().optional(),
103
103
  metadata: z.record(z.string(), z.any()).optional(),
104
+ // Old format compatibility
105
+ meta: z
106
+ .object({
107
+ name: z.string(),
108
+ description: z.string().optional(),
109
+ })
110
+ .optional(),
111
+ pattern: z.string().optional(),
112
+ run: z.function().optional(),
104
113
  });
105
114
 
106
115
  export const HookSchema = z.object({
@@ -134,19 +143,28 @@ export const HookSchema = z.object({
134
143
  export function defineHook(config) {
135
144
  const validated = HookConfigSchema.parse(config);
136
145
 
137
- if (!validated.validate && !validated.transform) {
138
- throw new Error('Hook must define either validate or transform function');
146
+ // Support old format with meta.name and run()
147
+ const name = validated.name || validated.meta?.name;
148
+ const validate = validated.validate;
149
+ const transform = validated.transform || validated.run;
150
+ const metadata = validated.metadata || {
151
+ description: validated.meta?.description,
152
+ pattern: validated.pattern,
153
+ };
154
+
155
+ if (!validate && !transform) {
156
+ throw new Error('Hook must define either validate, transform, or run function');
139
157
  }
140
158
 
141
159
  return {
142
- name: validated.name,
160
+ name,
143
161
  trigger: validated.trigger,
144
- validate: validated.validate,
145
- transform: validated.transform,
146
- metadata: validated.metadata || {},
162
+ validate,
163
+ transform,
164
+ metadata,
147
165
  // Pre-computed flags for sub-1μs execution (skip Zod in hot path)
148
- _hasValidation: typeof validated.validate === 'function',
149
- _hasTransformation: typeof validated.transform === 'function',
166
+ _hasValidation: typeof validate === 'function',
167
+ _hasTransformation: typeof transform === 'function',
150
168
  _validated: true,
151
169
  };
152
170
  }
@@ -109,7 +109,7 @@ function createSafeEffect(effectCode, safeGlobals) {
109
109
  const require = undefined;
110
110
  const module = undefined;
111
111
  const exports = undefined;
112
- const __dirname = undefined;
112
+ const _dirname = undefined;
113
113
  const __filename = undefined;
114
114
 
115
115
  // Create effect function
@@ -7,9 +7,12 @@
7
7
  * to prevent malicious code execution and system access.
8
8
  */
9
9
 
10
- import { Worker, _isMainThread, _parentPort, _workerData } from 'worker_threads';
11
- import { join } from 'path';
10
+ import { Worker } from 'worker_threads';
11
+ import { join, dirname } from 'path';
12
12
  import { randomUUID } from 'crypto';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
16
  import { z } from 'zod';
14
17
 
15
18
  /**
@@ -9,8 +9,8 @@
9
9
 
10
10
  import { readFile } from 'fs/promises';
11
11
  import { createHash } from 'crypto';
12
- import { _fileURLToPath } from 'url';
13
- import { _dirname, _join, _resolve } from 'path';
12
+ import { fileURLToPath as _fileURLToPath } from 'url';
13
+ import { dirname as _dirname, join as _join, resolve as _resolve } from 'path';
14
14
  import { createPathValidator } from './security/path-validator.mjs';
15
15
 
16
16
  /**
@@ -322,23 +322,23 @@ export function executeBatch(hooks, quads, options = {}) {
322
322
  }
323
323
  }
324
324
 
325
- return { results, validCount, invalidCount };
325
+ return results;
326
326
  }
327
327
 
328
328
  /**
329
- * Validate batch of quads, returning bitmap of valid quads.
330
- * Hyper-speed: Zod-free hot path, returns Uint8Array directly.
329
+ * Validate batch of quads, returning array of boolean results.
330
+ * Hyper-speed: Zod-free hot path, returns boolean array directly.
331
331
  *
332
332
  * @param {Hook[]} hooks - Hooks to execute (must be pre-validated via defineHook)
333
333
  * @param {Quad[]} quads - Array of quads to validate
334
- * @returns {Uint8Array} - Bitmap where 1 = valid, 0 = invalid
334
+ * @returns {boolean[]} - Array where true = valid, false = invalid
335
335
  */
336
336
  export function validateBatch(hooks, quads) {
337
337
  // Filter validation hooks once (no Zod)
338
338
  const validationHooks = hooks.filter(hasValidation);
339
339
 
340
- // Use Uint8Array for compact boolean storage - returned directly
341
- const bitmap = new Uint8Array(quads.length);
340
+ // Return boolean array for test compatibility
341
+ const results = [];
342
342
 
343
343
  for (let i = 0; i < quads.length; i++) {
344
344
  const quad = quads[i];
@@ -356,10 +356,10 @@ export function validateBatch(hooks, quads) {
356
356
  }
357
357
  }
358
358
 
359
- bitmap[i] = isValid ? 1 : 0;
359
+ results.push(isValid);
360
360
  }
361
361
 
362
- return bitmap;
362
+ return results;
363
363
  }
364
364
 
365
365
  /**
@@ -369,16 +369,14 @@ export function validateBatch(hooks, quads) {
369
369
  * @param {Hook[]} hooks - Hooks to execute (must be pre-validated via defineHook)
370
370
  * @param {Quad[]} quads - Array of quads to transform
371
371
  * @param {Object} [options] - Transform options
372
- * @param {boolean} [options.validateFirst=true] - Validate before transform
373
- * @returns {{ transformed: Quad[], errors: Array<{index: number, error: string}> }}
372
+ * @param {boolean} [options.validateFirst=false] - Validate before transform
373
+ * @returns {Quad[]} Array of transformed quads
374
374
  */
375
375
  export function transformBatch(hooks, quads, options = {}) {
376
- const { validateFirst = true } = options;
376
+ const { validateFirst = false } = options;
377
377
 
378
378
  /** @type {Quad[]} */
379
379
  const transformed = [];
380
- /** @type {Array<{index: number, error: string}>} */
381
- const errors = [];
382
380
 
383
381
  for (let i = 0; i < quads.length; i++) {
384
382
  let currentQuad = quads[i];
@@ -389,7 +387,6 @@ export function transformBatch(hooks, quads, options = {}) {
389
387
  // Validate first if required
390
388
  if (validateFirst && hasValidation(hook)) {
391
389
  if (!hook.validate(currentQuad)) {
392
- errors.push({ index: i, error: `Validation failed: ${hook.name}` });
393
390
  hasError = true;
394
391
  break;
395
392
  }
@@ -400,10 +397,6 @@ export function transformBatch(hooks, quads, options = {}) {
400
397
  currentQuad = hook.transform(currentQuad);
401
398
  }
402
399
  } catch (error) {
403
- errors.push({
404
- index: i,
405
- error: error instanceof Error ? error.message : String(error),
406
- });
407
400
  hasError = true;
408
401
  break;
409
402
  }
@@ -414,7 +407,7 @@ export function transformBatch(hooks, quads, options = {}) {
414
407
  }
415
408
  }
416
409
 
417
- return { transformed, errors };
410
+ return transformed;
418
411
  }
419
412
 
420
413
  /* ========================================================================= */
@@ -7,9 +7,15 @@
7
7
  * governance units that can be activated/deactivated as cohesive sets.
8
8
  */
9
9
 
10
- import { readFileSync, _writeFileSync, existsSync, _mkdirSync, readdirSync } from 'fs';
11
- import { join, dirname, _basename, _extname } from 'path';
12
- import { _createKnowledgeHook, validateKnowledgeHook } from './schemas.mjs';
10
+ import {
11
+ readFileSync,
12
+ writeFileSync as _writeFileSync,
13
+ existsSync,
14
+ mkdirSync as _mkdirSync,
15
+ readdirSync,
16
+ } from 'fs';
17
+ import { join, dirname, basename as _basename, extname as _extname } from 'path';
18
+ import { createKnowledgeHook as _createKnowledgeHook, validateKnowledgeHook } from './schemas.mjs';
13
19
  import { z } from 'zod';
14
20
  import { randomUUID } from 'crypto';
15
21
 
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @fileoverview Query optimizer for SPARQL queries
3
+ * @module hooks/query-optimizer
4
+ *
5
+ * @description
6
+ * Optimizes SPARQL queries for better performance with indexing and caching.
7
+ */
8
+
9
+ /**
10
+ * Create a query optimizer
11
+ *
12
+ * @param {object} options - Optimizer configuration
13
+ * @param {boolean} options.enableIndexing - Enable query indexing
14
+ * @param {boolean} options.enableCaching - Enable query result caching
15
+ * @param {number} options.cacheMaxSize - Maximum cache size
16
+ * @returns {object} Query optimizer instance
17
+ */
18
+ export function createQueryOptimizer(options = {}) {
19
+ const { enableIndexing = true, enableCaching = true, cacheMaxSize = 1000 } = options;
20
+
21
+ const queryCache = new Map();
22
+ const indexes = new Map();
23
+ const stats = {
24
+ queriesOptimized: 0,
25
+ cacheHits: 0,
26
+ cacheMisses: 0,
27
+ indexesCreated: 0,
28
+ };
29
+
30
+ return {
31
+ /**
32
+ * Optimize a SPARQL query
33
+ *
34
+ * @param {string} query - SPARQL query string
35
+ * @returns {string} Optimized query
36
+ */
37
+ optimize(query) {
38
+ if (!query || typeof query !== 'string') {
39
+ return query;
40
+ }
41
+
42
+ stats.queriesOptimized++;
43
+
44
+ // Simple optimizations:
45
+ // 1. Remove unnecessary whitespace
46
+ let optimized = query.replace(/\s+/g, ' ').trim();
47
+
48
+ // 2. Move filters closer to triple patterns
49
+ // This is a simplified optimization - full implementation would use SPARQL parser
50
+
51
+ return optimized;
52
+ },
53
+
54
+ /**
55
+ * Create indexes for a graph
56
+ *
57
+ * @param {object} graph - RDF graph
58
+ * @returns {Promise<Array>} Created indexes
59
+ */
60
+ async createIndexes(graph) {
61
+ if (!enableIndexing) {
62
+ return [];
63
+ }
64
+
65
+ const createdIndexes = [];
66
+
67
+ // Create predicate index
68
+ const predicateIndex = new Map();
69
+ for (const quad of graph) {
70
+ const pred = quad.predicate.value;
71
+ if (!predicateIndex.has(pred)) {
72
+ predicateIndex.set(pred, []);
73
+ }
74
+ predicateIndex.get(pred).push(quad);
75
+ }
76
+
77
+ indexes.set('predicate', predicateIndex);
78
+ createdIndexes.push('predicate');
79
+ stats.indexesCreated++;
80
+
81
+ return createdIndexes;
82
+ },
83
+
84
+ /**
85
+ * Update indexes with delta
86
+ *
87
+ * @param {object} delta - Delta changes
88
+ * @returns {Promise<void>}
89
+ */
90
+ async updateIndexes(delta) {
91
+ if (!enableIndexing || !indexes.has('predicate')) {
92
+ return;
93
+ }
94
+
95
+ const predicateIndex = indexes.get('predicate');
96
+
97
+ // Remove deleted quads
98
+ if (delta.deletions) {
99
+ for (const quad of delta.deletions) {
100
+ const pred = quad.predicate.value;
101
+ if (predicateIndex.has(pred)) {
102
+ const quads = predicateIndex.get(pred);
103
+ const index = quads.indexOf(quad);
104
+ if (index > -1) {
105
+ quads.splice(index, 1);
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ // Add new quads
112
+ if (delta.additions) {
113
+ for (const quad of delta.additions) {
114
+ const pred = quad.predicate.value;
115
+ if (!predicateIndex.has(pred)) {
116
+ predicateIndex.set(pred, []);
117
+ }
118
+ predicateIndex.get(pred).push(quad);
119
+ }
120
+ }
121
+ },
122
+
123
+ /**
124
+ * Cache query result
125
+ *
126
+ * @param {string} query - SPARQL query
127
+ * @param {*} result - Query result
128
+ */
129
+ cacheResult(query, result) {
130
+ if (!enableCaching) {
131
+ return;
132
+ }
133
+
134
+ // Evict oldest if at capacity
135
+ if (queryCache.size >= cacheMaxSize) {
136
+ const firstKey = queryCache.keys().next().value;
137
+ queryCache.delete(firstKey);
138
+ }
139
+
140
+ queryCache.set(query, {
141
+ result,
142
+ timestamp: Date.now(),
143
+ });
144
+ },
145
+
146
+ /**
147
+ * Get cached query result
148
+ *
149
+ * @param {string} query - SPARQL query
150
+ * @param {number} maxAge - Maximum cache age in ms
151
+ * @returns {*} Cached result or null
152
+ */
153
+ getCachedResult(query, maxAge = 60000) {
154
+ if (!enableCaching || !queryCache.has(query)) {
155
+ stats.cacheMisses++;
156
+ return null;
157
+ }
158
+
159
+ const cached = queryCache.get(query);
160
+ if (Date.now() - cached.timestamp > maxAge) {
161
+ queryCache.delete(query);
162
+ stats.cacheMisses++;
163
+ return null;
164
+ }
165
+
166
+ stats.cacheHits++;
167
+ return cached.result;
168
+ },
169
+
170
+ /**
171
+ * Clear optimizer caches and indexes
172
+ */
173
+ clear() {
174
+ queryCache.clear();
175
+ indexes.clear();
176
+ },
177
+
178
+ /**
179
+ * Get optimizer statistics
180
+ *
181
+ * @returns {object} Statistics
182
+ */
183
+ getStats() {
184
+ return {
185
+ ...stats,
186
+ cacheSize: queryCache.size,
187
+ indexCount: indexes.size,
188
+ cacheHitRate: stats.cacheHits / (stats.cacheHits + stats.cacheMisses) || 0,
189
+ };
190
+ },
191
+ };
192
+ }