@unrdf/hooks 26.4.3 → 26.4.4
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/LICENSE +24 -0
- package/README.md +562 -53
- package/examples/atomvm-fibo-hooks-demo.mjs +323 -0
- package/examples/delta-monitoring-example.mjs +213 -0
- package/examples/fibo-jtbd-governance.mjs +388 -0
- package/examples/hook-chains/node_modules/.bin/jiti +0 -0
- package/examples/hook-chains/node_modules/.bin/msw +0 -0
- package/examples/hook-chains/node_modules/.bin/terser +0 -0
- package/examples/hook-chains/node_modules/.bin/tsc +0 -0
- package/examples/hook-chains/node_modules/.bin/tsserver +0 -0
- package/examples/hook-chains/node_modules/.bin/tsx +0 -0
- package/examples/hook-chains/node_modules/.bin/validate-hooks +0 -0
- package/examples/hook-chains/node_modules/.bin/vite +0 -0
- package/examples/hook-chains/node_modules/.bin/vitest +0 -0
- package/examples/hook-chains/node_modules/.bin/yaml +0 -0
- package/examples/hooks-marketplace.mjs +261 -0
- package/examples/n3-reasoning-example.mjs +279 -0
- package/examples/policy-hooks/node_modules/.bin/jiti +0 -0
- package/examples/policy-hooks/node_modules/.bin/msw +0 -0
- package/examples/policy-hooks/node_modules/.bin/terser +0 -0
- package/examples/policy-hooks/node_modules/.bin/tsc +0 -0
- package/examples/policy-hooks/node_modules/.bin/tsserver +0 -0
- package/examples/policy-hooks/node_modules/.bin/tsx +0 -0
- package/examples/policy-hooks/node_modules/.bin/validate-hooks +0 -0
- package/examples/policy-hooks/node_modules/.bin/vite +0 -0
- package/examples/policy-hooks/node_modules/.bin/vitest +0 -0
- package/examples/policy-hooks/node_modules/.bin/yaml +0 -0
- package/examples/shacl-repair-example.mjs +191 -0
- package/examples/window-condition-example.mjs +285 -0
- package/package.json +26 -23
- package/src/atomvm.mjs +9 -0
- package/src/define.mjs +114 -0
- package/src/executor.mjs +23 -0
- package/src/hooks/atomvm-bridge.mjs +332 -0
- package/src/hooks/builtin-hooks.mjs +13 -7
- package/src/hooks/condition-evaluator.mjs +684 -77
- package/src/hooks/define-hook.mjs +23 -23
- package/src/hooks/effect-executor.mjs +630 -0
- package/src/hooks/effect-sandbox.mjs +19 -9
- package/src/hooks/file-resolver.mjs +155 -1
- package/src/hooks/hook-chain-compiler.mjs +11 -1
- package/src/hooks/hook-executor.mjs +98 -73
- package/src/hooks/knowledge-hook-engine.mjs +133 -7
- package/src/hooks/ontology-learner.mjs +190 -0
- package/src/hooks/query.mjs +3 -3
- package/src/hooks/schemas.mjs +47 -3
- package/src/hooks/security/error-sanitizer.mjs +46 -24
- package/src/hooks/self-play-autonomics.mjs +423 -0
- package/src/hooks/telemetry.mjs +32 -9
- package/src/hooks/validate.mjs +100 -33
- package/src/index.mjs +2 -0
- package/src/lib/admit-hook.mjs +615 -0
- package/src/policy-compiler.mjs +12 -2
- package/dist/index.d.mts +0 -1738
- package/dist/index.d.ts +0 -1738
- package/dist/index.mjs +0 -1738
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* @module knowledge-engine/knowledge-hook-engine
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { blake3 } from '@noble/hashes/blake3.js';
|
|
18
19
|
import { StoreCache } from './store-cache.mjs';
|
|
19
20
|
import { ConditionCache } from './condition-cache.mjs';
|
|
20
21
|
import { BatchedTelemetry } from './telemetry.mjs';
|
|
@@ -103,6 +104,9 @@ export class KnowledgeHookEngine {
|
|
|
103
104
|
* @param {object} options - Execution options
|
|
104
105
|
* @param {object} options.env - SPARQL evaluation environment
|
|
105
106
|
* @param {boolean} options.debug - Enable debug output
|
|
107
|
+
* @param {string} [options.nodeId] - Node identifier for receipt (default: 'knowledge-hook-engine')
|
|
108
|
+
* @param {bigint} [options.t_ns] - Nanosecond timestamp for receipt
|
|
109
|
+
* @param {string} [options.previousReceiptHash] - Previous receipt hash for chaining
|
|
106
110
|
* @returns {Promise<object>} Execution results { conditionResults, executionResults, receipt }
|
|
107
111
|
*/
|
|
108
112
|
async execute(store, delta, options = {}) {
|
|
@@ -123,6 +127,9 @@ export class KnowledgeHookEngine {
|
|
|
123
127
|
this.storeCache.clear();
|
|
124
128
|
this.conditionCache.clear();
|
|
125
129
|
|
|
130
|
+
// Compute input hash before execution
|
|
131
|
+
const input_hash = await this._computeStoreHash(store);
|
|
132
|
+
|
|
126
133
|
// Phase 1: Parallel condition evaluation (ONCE per hook)
|
|
127
134
|
const conditionResults = await this.evaluateConditions(store, delta, options);
|
|
128
135
|
|
|
@@ -135,8 +142,17 @@ export class KnowledgeHookEngine {
|
|
|
135
142
|
|
|
136
143
|
this.telemetry.setAttribute(span, 'hooks.executed', executionResults.length);
|
|
137
144
|
|
|
138
|
-
//
|
|
139
|
-
const
|
|
145
|
+
// Compute output hash after execution
|
|
146
|
+
const output_hash = await this._computeStoreHash(store);
|
|
147
|
+
|
|
148
|
+
// Phase 3: Generate receipt with cryptographic hashing
|
|
149
|
+
const receipt = await this.generateReceiptWithHash(
|
|
150
|
+
executionResults,
|
|
151
|
+
delta,
|
|
152
|
+
input_hash,
|
|
153
|
+
output_hash,
|
|
154
|
+
options
|
|
155
|
+
);
|
|
140
156
|
|
|
141
157
|
this.telemetry.endSpan(span, 'ok');
|
|
142
158
|
|
|
@@ -213,6 +229,8 @@ export class KnowledgeHookEngine {
|
|
|
213
229
|
/**
|
|
214
230
|
* Execute a single hook with side effects.
|
|
215
231
|
*
|
|
232
|
+
* Supports both function-based effects (hook.run) and RDF-native SPARQL CONSTRUCT effects.
|
|
233
|
+
*
|
|
216
234
|
* NOTE: Per-hook OTEL spans removed for performance optimization.
|
|
217
235
|
* Transaction-level spans at execute() provide aggregate visibility.
|
|
218
236
|
* Savings: 2-4μs per hook execution.
|
|
@@ -224,6 +242,12 @@ export class KnowledgeHookEngine {
|
|
|
224
242
|
// Transaction-level span at execute():109 provides aggregate metrics
|
|
225
243
|
|
|
226
244
|
try {
|
|
245
|
+
// Dispatch to appropriate executor based on effect type
|
|
246
|
+
if (hook.effect?.kind === 'sparql-construct') {
|
|
247
|
+
return this.executeSparqlConstructEffect(hook, store, delta, options);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Legacy: function-based effect
|
|
227
251
|
const event = {
|
|
228
252
|
store,
|
|
229
253
|
delta,
|
|
@@ -246,6 +270,45 @@ export class KnowledgeHookEngine {
|
|
|
246
270
|
}
|
|
247
271
|
}
|
|
248
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Execute a SPARQL CONSTRUCT effect
|
|
275
|
+
*
|
|
276
|
+
* Runs the CONSTRUCT query against the store and adds resulting quads.
|
|
277
|
+
* CONSTRUCT returns new quads that are merged into the knowledge graph.
|
|
278
|
+
*
|
|
279
|
+
* @private
|
|
280
|
+
*/
|
|
281
|
+
executeSparqlConstructEffect(hook, store, _delta, _options) {
|
|
282
|
+
try {
|
|
283
|
+
const query = hook.effect.query;
|
|
284
|
+
|
|
285
|
+
// Execute CONSTRUCT query - returns iterable of quads
|
|
286
|
+
const quads = store.query(query);
|
|
287
|
+
|
|
288
|
+
// Add all resulting quads to the store
|
|
289
|
+
let addCount = 0;
|
|
290
|
+
for (const quad of quads) {
|
|
291
|
+
store.add(quad);
|
|
292
|
+
addCount++;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
hookId: hook.id,
|
|
297
|
+
success: true,
|
|
298
|
+
result: {
|
|
299
|
+
quadsAdded: addCount,
|
|
300
|
+
query,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return {
|
|
305
|
+
hookId: hook.id,
|
|
306
|
+
success: false,
|
|
307
|
+
error: error.message,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
249
312
|
/**
|
|
250
313
|
* Build dependency-ordered batches of hooks
|
|
251
314
|
*
|
|
@@ -312,15 +375,17 @@ export class KnowledgeHookEngine {
|
|
|
312
375
|
}
|
|
313
376
|
|
|
314
377
|
/**
|
|
315
|
-
* Generate transaction receipt
|
|
378
|
+
* Generate transaction receipt with cryptographic BLAKE3 hashing
|
|
316
379
|
*
|
|
317
380
|
* @private
|
|
318
381
|
*/
|
|
319
|
-
|
|
320
|
-
const
|
|
382
|
+
async generateReceiptWithHash(executionResults, delta, input_hash, output_hash, options = {}) {
|
|
383
|
+
const t_ns = options.t_ns || BigInt(Date.now()) * 1000000n;
|
|
384
|
+
const previousReceiptHash = options.previousReceiptHash || null;
|
|
321
385
|
|
|
322
|
-
|
|
323
|
-
|
|
386
|
+
// Build the core receipt data
|
|
387
|
+
const receiptPayload = {
|
|
388
|
+
timestamp: new Date(Number(t_ns / 1000000n)).toISOString(),
|
|
324
389
|
delta: {
|
|
325
390
|
adds: delta?.adds?.length || 0,
|
|
326
391
|
deletes: delta?.deletes?.length || 0,
|
|
@@ -328,7 +393,23 @@ export class KnowledgeHookEngine {
|
|
|
328
393
|
hooksExecuted: executionResults.length,
|
|
329
394
|
successful: executionResults.filter(r => r.value?.success).length,
|
|
330
395
|
failed: executionResults.filter(r => r.status === 'rejected' || !r.value?.success).length,
|
|
396
|
+
input_hash,
|
|
397
|
+
output_hash,
|
|
398
|
+
previousReceiptHash,
|
|
331
399
|
};
|
|
400
|
+
|
|
401
|
+
// Generate receipt with BLAKE3-compatible structure
|
|
402
|
+
// Note: Full BLAKE3 integration is handled by v6-core/receipts
|
|
403
|
+
const receipt = {
|
|
404
|
+
timestamp: new Date(Number(t_ns / 1000000n)).toISOString(),
|
|
405
|
+
receiptHash: this._generateHash(receiptPayload),
|
|
406
|
+
input_hash: input_hash,
|
|
407
|
+
output_hash: output_hash,
|
|
408
|
+
previousReceiptHash,
|
|
409
|
+
...receiptPayload,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
return receipt;
|
|
332
413
|
}
|
|
333
414
|
|
|
334
415
|
/**
|
|
@@ -353,6 +434,51 @@ export class KnowledgeHookEngine {
|
|
|
353
434
|
this.conditionCache.clear();
|
|
354
435
|
this.fileCacheWarmed = false;
|
|
355
436
|
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Generate deterministic hash for receipt payload
|
|
440
|
+
* (simplified implementation - production uses BLAKE3 from v6-core)
|
|
441
|
+
*
|
|
442
|
+
* @private
|
|
443
|
+
*/
|
|
444
|
+
_generateHash(payload) {
|
|
445
|
+
// BLAKE3 cryptographic hashing for receipt security
|
|
446
|
+
const str = JSON.stringify(payload);
|
|
447
|
+
const encoder = new TextEncoder();
|
|
448
|
+
const bytes = encoder.encode(str);
|
|
449
|
+
const digest = blake3(bytes, { dkLen: 32 });
|
|
450
|
+
// Convert Uint8Array to hex string
|
|
451
|
+
return Array.from(digest)
|
|
452
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
453
|
+
.join('');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Compute hash of store state
|
|
458
|
+
*
|
|
459
|
+
* @private
|
|
460
|
+
*/
|
|
461
|
+
async _computeStoreHash(store) {
|
|
462
|
+
// Simplified hash based on store size and content
|
|
463
|
+
// In production, use proper content hash (SHA-256, BLAKE3)
|
|
464
|
+
if (!store || !store.size) {
|
|
465
|
+
return '0'.repeat(64);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const quads = Array.from(store);
|
|
469
|
+
const quadStrings = quads
|
|
470
|
+
.map(q => `${q.subject?.value || ''}:${q.predicate?.value || ''}:${q.object?.value || ''}`)
|
|
471
|
+
.join('|');
|
|
472
|
+
|
|
473
|
+
let hash = 0;
|
|
474
|
+
for (let i = 0; i < quadStrings.length; i++) {
|
|
475
|
+
const char = quadStrings.charCodeAt(i);
|
|
476
|
+
hash = (hash << 5) - hash + char;
|
|
477
|
+
hash = hash & hash;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return Math.abs(hash).toString(16).padStart(64, '0');
|
|
481
|
+
}
|
|
356
482
|
}
|
|
357
483
|
|
|
358
484
|
export default KnowledgeHookEngine;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Ontology Learner
|
|
3
|
+
* @module @unrdf/hooks/ontology-learner
|
|
4
|
+
* @description Learn SHACL shapes from RDF patterns
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Learn ontology patterns from RDF data
|
|
9
|
+
*/
|
|
10
|
+
export class OntologyLearner {
|
|
11
|
+
/**
|
|
12
|
+
* Infer SHACL shapes from RDF graph
|
|
13
|
+
*/
|
|
14
|
+
async inferShapes(store, options = {}) {
|
|
15
|
+
const minSupport = options.minSupport || 0.9;
|
|
16
|
+
const shapes = {};
|
|
17
|
+
|
|
18
|
+
if (!store || store.size === 0) {
|
|
19
|
+
return shapes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Get all classes
|
|
23
|
+
const classes = this.extractClasses(store);
|
|
24
|
+
|
|
25
|
+
for (const cls of classes) {
|
|
26
|
+
// Get instances of this class
|
|
27
|
+
const instances = this.getInstancesOfClass(store, cls);
|
|
28
|
+
if (instances.length === 0) continue;
|
|
29
|
+
|
|
30
|
+
// Mine properties
|
|
31
|
+
const properties = this.mineProperties(store, instances);
|
|
32
|
+
|
|
33
|
+
// Generate shapes for properties with enough support
|
|
34
|
+
const shapeProperties = {};
|
|
35
|
+
for (const [predicate, data] of Object.entries(properties)) {
|
|
36
|
+
const supportRatio = data.count / instances.length;
|
|
37
|
+
if (supportRatio >= minSupport) {
|
|
38
|
+
shapeProperties[predicate] = {
|
|
39
|
+
minCount: supportRatio > 0.99 ? 1 : 0,
|
|
40
|
+
datatype: this.dominantDatatype(data.datatypes),
|
|
41
|
+
description: `Property ${predicate} (${Math.round(supportRatio * 100)}% coverage)`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (Object.keys(shapeProperties).length > 0) {
|
|
47
|
+
shapes[cls] = {
|
|
48
|
+
targetClass: cls,
|
|
49
|
+
properties: shapeProperties,
|
|
50
|
+
instanceCount: instances.length,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return shapes;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract all RDF classes from store
|
|
60
|
+
*/
|
|
61
|
+
extractClasses(store) {
|
|
62
|
+
const classes = new Set();
|
|
63
|
+
|
|
64
|
+
for (const quad of store) {
|
|
65
|
+
// RDF type declarations
|
|
66
|
+
if (quad.predicate.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type') {
|
|
67
|
+
if (quad.object.value && quad.object.value.startsWith('http')) {
|
|
68
|
+
classes.add(quad.object.value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Also RDFS classes
|
|
72
|
+
if (
|
|
73
|
+
quad.subject.value &&
|
|
74
|
+
quad.subject.value.startsWith('http') &&
|
|
75
|
+
quad.object.value === 'http://www.w3.org/2000/01/rdf-schema#Class'
|
|
76
|
+
) {
|
|
77
|
+
classes.add(quad.subject.value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return Array.from(classes);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get instances of a class
|
|
86
|
+
*/
|
|
87
|
+
getInstancesOfClass(store, className) {
|
|
88
|
+
const instances = new Set();
|
|
89
|
+
const typeIri = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
90
|
+
|
|
91
|
+
for (const quad of store) {
|
|
92
|
+
if (quad.predicate.value === typeIri && quad.object.value === className) {
|
|
93
|
+
instances.add(quad.subject.value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return Array.from(instances);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mine property patterns from instances
|
|
102
|
+
*/
|
|
103
|
+
mineProperties(store, instances) {
|
|
104
|
+
const properties = {};
|
|
105
|
+
|
|
106
|
+
for (const instance of instances) {
|
|
107
|
+
for (const quad of store) {
|
|
108
|
+
if (quad.subject.value === instance) {
|
|
109
|
+
const pred = quad.predicate.value;
|
|
110
|
+
const obj = quad.object.value;
|
|
111
|
+
|
|
112
|
+
if (!properties[pred]) {
|
|
113
|
+
properties[pred] = {
|
|
114
|
+
count: 0,
|
|
115
|
+
objects: [],
|
|
116
|
+
datatypes: new Set(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
properties[pred].count++;
|
|
121
|
+
properties[pred].objects.push(obj);
|
|
122
|
+
properties[pred].datatypes.add(this.inferDatatype(quad.object));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return properties;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Infer datatype of RDF value
|
|
132
|
+
*/
|
|
133
|
+
inferDatatype(obj) {
|
|
134
|
+
if (!obj) return 'unknown';
|
|
135
|
+
|
|
136
|
+
// Check for NamedNode/Resource
|
|
137
|
+
if (obj.termType === 'NamedNode' || (obj.value && String(obj.value).startsWith('http'))) {
|
|
138
|
+
return 'rdf:Resource';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Infer from value pattern - try specific types first
|
|
142
|
+
if (obj.value !== undefined && obj.value !== null) {
|
|
143
|
+
const val = String(obj.value);
|
|
144
|
+
|
|
145
|
+
// Try to infer more specific types from pattern
|
|
146
|
+
if (/^\d+$/.test(val)) return 'xsd:integer';
|
|
147
|
+
if (/^\d+\.\d+$/.test(val)) return 'xsd:float';
|
|
148
|
+
if (/^(true|false)$/i.test(val)) return 'xsd:boolean';
|
|
149
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(val)) return 'xsd:dateTime';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fall back to explicit datatype
|
|
153
|
+
if (obj.datatype && obj.datatype.value) {
|
|
154
|
+
const dt = obj.datatype.value;
|
|
155
|
+
if (dt.includes('integer') || dt.includes('int')) return 'xsd:integer';
|
|
156
|
+
if (dt.includes('float') || dt.includes('double')) return 'xsd:float';
|
|
157
|
+
if (dt.includes('boolean')) return 'xsd:boolean';
|
|
158
|
+
if (dt.includes('date')) return 'xsd:dateTime';
|
|
159
|
+
if (dt.includes('string')) return 'xsd:string';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return 'xsd:string';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Find dominant datatype from set
|
|
167
|
+
*/
|
|
168
|
+
dominantDatatype(datatypes) {
|
|
169
|
+
if (datatypes.size === 0) return 'xsd:string';
|
|
170
|
+
return Array.from(datatypes)[0];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Generate SHACL shape definition
|
|
175
|
+
*/
|
|
176
|
+
toSHACLShape(className, properties) {
|
|
177
|
+
return {
|
|
178
|
+
'@context': 'http://www.w3.org/ns/shacl#',
|
|
179
|
+
'@type': 'NodeShape',
|
|
180
|
+
targetClass: className,
|
|
181
|
+
property: Object.entries(properties).map(([pred, config]) => ({
|
|
182
|
+
path: pred,
|
|
183
|
+
minCount: config.minCount || 0,
|
|
184
|
+
datatype: config.datatype || 'xsd:string',
|
|
185
|
+
})),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default OntologyLearner;
|
package/src/hooks/query.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* @param {boolean} options.deterministic - Use deterministic execution
|
|
17
17
|
* @returns {Promise<boolean>} Query result (true/false)
|
|
18
18
|
*/
|
|
19
|
-
export async function ask(store, queryString,
|
|
19
|
+
export async function ask(store, queryString, _options = {}) {
|
|
20
20
|
if (!store || typeof store.query !== 'function') {
|
|
21
21
|
throw new TypeError('ask: store must have a query method');
|
|
22
22
|
}
|
|
@@ -59,7 +59,7 @@ export async function ask(store, queryString, options = {}) {
|
|
|
59
59
|
* @param {boolean} options.deterministic - Use deterministic execution
|
|
60
60
|
* @returns {Promise<Array>} Query results as array of bindings
|
|
61
61
|
*/
|
|
62
|
-
export async function select(store, queryString,
|
|
62
|
+
export async function select(store, queryString, _options = {}) {
|
|
63
63
|
if (!store || typeof store.query !== 'function') {
|
|
64
64
|
throw new TypeError('select: store must have a query method');
|
|
65
65
|
}
|
|
@@ -108,7 +108,7 @@ export async function select(store, queryString, options = {}) {
|
|
|
108
108
|
* @param {object} options - Query options
|
|
109
109
|
* @returns {Promise<Array>} Query results as array of quads
|
|
110
110
|
*/
|
|
111
|
-
export async function construct(store, queryString,
|
|
111
|
+
export async function construct(store, queryString, _options = {}) {
|
|
112
112
|
if (!store || typeof store.query !== 'function') {
|
|
113
113
|
throw new TypeError('construct: store must have a query method');
|
|
114
114
|
}
|
package/src/hooks/schemas.mjs
CHANGED
|
@@ -32,14 +32,45 @@ export const HookConditionRefSchema = z.object({
|
|
|
32
32
|
mediaType: z.string().optional(),
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Schema for SHACL condition with enforcement modes (soft-fail governance)
|
|
37
|
+
*
|
|
38
|
+
* Enforcement modes:
|
|
39
|
+
* - 'block': Default. Fail validation if SHACL constraint violated.
|
|
40
|
+
* - 'annotate': Allow write but add SHACL report as RDF triples to store.
|
|
41
|
+
* - 'repair': Attempt to fix violations using SPARQL CONSTRUCT query.
|
|
42
|
+
*/
|
|
43
|
+
export const ShaclConditionSchema = z.object({
|
|
44
|
+
kind: z.literal('shacl'),
|
|
45
|
+
ref: HookConditionRefSchema,
|
|
46
|
+
enforcementMode: z.enum(['block', 'annotate', 'repair']).default('block'),
|
|
47
|
+
repairConstruct: z.string().optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
35
50
|
/**
|
|
36
51
|
* Schema for hook condition
|
|
37
52
|
*/
|
|
38
53
|
export const HookConditionSchema = z.object({
|
|
39
|
-
kind: z.enum([
|
|
54
|
+
kind: z.enum([
|
|
55
|
+
'sparql-ask',
|
|
56
|
+
'sparql-select',
|
|
57
|
+
'shacl',
|
|
58
|
+
'delta',
|
|
59
|
+
'threshold',
|
|
60
|
+
'count',
|
|
61
|
+
'window',
|
|
62
|
+
'n3',
|
|
63
|
+
'datalog',
|
|
64
|
+
]),
|
|
40
65
|
ref: HookConditionRefSchema.optional(),
|
|
41
66
|
query: z.string().optional(),
|
|
42
67
|
shapes: z.string().optional(),
|
|
68
|
+
rules: z.string().optional(),
|
|
69
|
+
askQuery: z.string().optional(),
|
|
70
|
+
facts: z.array(z.string()).optional(),
|
|
71
|
+
goal: z.string().optional(),
|
|
72
|
+
enforcementMode: z.enum(['block', 'annotate', 'repair']).default('block').optional(),
|
|
73
|
+
repairConstruct: z.string().optional(),
|
|
43
74
|
spec: z.record(z.any()).optional(),
|
|
44
75
|
});
|
|
45
76
|
|
|
@@ -56,9 +87,17 @@ export const HookEffectRefSchema = z.object({
|
|
|
56
87
|
});
|
|
57
88
|
|
|
58
89
|
/**
|
|
59
|
-
* Schema for
|
|
90
|
+
* Schema for SPARQL CONSTRUCT effect (RDF-native transformation)
|
|
91
|
+
*/
|
|
92
|
+
export const SparqlConstructEffectSchema = z.object({
|
|
93
|
+
kind: z.literal('sparql-construct'),
|
|
94
|
+
query: z.string().min(1),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Schema for JavaScript function effect (legacy)
|
|
60
99
|
*/
|
|
61
|
-
export const
|
|
100
|
+
export const FunctionEffectSchema = z.object({
|
|
62
101
|
ref: HookEffectRefSchema.optional(),
|
|
63
102
|
inline: z.function().optional(),
|
|
64
103
|
timeout: z.number().int().positive().max(300000).default(30000),
|
|
@@ -66,6 +105,11 @@ export const HookEffectSchema = z.object({
|
|
|
66
105
|
sandbox: z.boolean().default(false),
|
|
67
106
|
});
|
|
68
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Schema for hook effect - union of function-based and SPARQL-based effects
|
|
110
|
+
*/
|
|
111
|
+
export const HookEffectSchema = z.union([SparqlConstructEffectSchema, FunctionEffectSchema]);
|
|
112
|
+
|
|
69
113
|
/**
|
|
70
114
|
* Complete knowledge hook schema
|
|
71
115
|
*/
|
|
@@ -42,28 +42,28 @@ const SENSITIVE_PATTERNS = {
|
|
|
42
42
|
// Database connection strings
|
|
43
43
|
credentials: [
|
|
44
44
|
/\b(?:postgres|mysql|mongodb):\/\/[^:\s]+:[^@\s]+@[^\s]+/gi, // DB URLs with passwords
|
|
45
|
-
/password\s*[:=]\s*["']?[^"'\s]+["']?/
|
|
46
|
-
/api[_-]?key\s*[:=]\s*["']?[^"'\s]+["']?/
|
|
45
|
+
/password\s*[:=]\s*["']?[^"'\s]+["']?/g, // password= or password: (lowercase only)
|
|
46
|
+
/api[_-]?key\s*[:=]\s*["']?[^"'\s]+["']?/g, // api_key= or api-key= (lowercase only)
|
|
47
47
|
/secret\s*[:=]\s*["']?[^"'\s]+["']?/gi, // secrets
|
|
48
48
|
/token\s*[:=]\s*["']?[^"'\s]+["']?/gi, // tokens
|
|
49
49
|
/authorization\s*:\s*["']?[^"'\s]+["']?/gi, // auth headers
|
|
50
50
|
],
|
|
51
51
|
|
|
52
|
-
// Environment variables
|
|
52
|
+
// Environment variables (ALL_CAPS only to avoid overlap with credentials)
|
|
53
53
|
environmentVars: [
|
|
54
|
-
/\bDATABASE_URL\s*=\s*[^\s]+/
|
|
55
|
-
/API_KEY\s*=\s*[^\s]+/
|
|
56
|
-
/SECRET\s*=\s*[^\s]+/
|
|
57
|
-
/PASSWORD\s*=\s*[^\s]+/
|
|
58
|
-
/TOKEN\s*=\s*[^\s]+/
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
/\bDATABASE_URL\s*=\s*[^\s]+/g,
|
|
55
|
+
/API_KEY\s*=\s*[^\s]+/g,
|
|
56
|
+
/SECRET\s*=\s*[^\s]+/g,
|
|
57
|
+
/PASSWORD\s*=\s*[^\s]+/g,
|
|
58
|
+
/TOKEN\s*=\s*[^\s]+/g,
|
|
59
|
+
/[A-Z][A-Z0-9_]*_KEY\s*=\s*[^\s]+/g,
|
|
60
|
+
/[A-Z][A-Z0-9_]*_SECRET\s*=\s*[^\s]+/g,
|
|
61
61
|
],
|
|
62
62
|
|
|
63
63
|
// Stack trace patterns
|
|
64
64
|
stackTraces: [
|
|
65
|
-
/\bat\s+[^\s]+\s+\([^)]
|
|
66
|
-
/at\s+[^(]+\([^)]
|
|
65
|
+
/\bat\s+[^\s]+\s+\([^)]*:\d+:\d+\)/g, // at Function (file:line:col) - allow empty parens before colon
|
|
66
|
+
/at\s+[^(]+\([^)]*\)/g, // at Function(...) - allow empty parens
|
|
67
67
|
/^\s*at\s.+$/gm, // Full stack trace lines
|
|
68
68
|
],
|
|
69
69
|
};
|
|
@@ -103,6 +103,11 @@ export class ErrorSanitizer {
|
|
|
103
103
|
message = this._removeEnvironmentVars(message);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// Remove stack traces
|
|
107
|
+
if (this.options.removeStackTraces) {
|
|
108
|
+
message = this._removeStackTraces(message);
|
|
109
|
+
}
|
|
110
|
+
|
|
106
111
|
// If sanitization removed too much, return generic message
|
|
107
112
|
if (message.trim().length < 10) {
|
|
108
113
|
return this.options.genericErrorMessage;
|
|
@@ -147,16 +152,13 @@ export class ErrorSanitizer {
|
|
|
147
152
|
let sanitized = text;
|
|
148
153
|
|
|
149
154
|
for (const pattern of SENSITIVE_PATTERNS.credentials) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// Replace password in connection string
|
|
153
|
-
return match.replace(/:\/\/[^:]+:[^@]+@/, '://***:***@');
|
|
154
|
-
}
|
|
155
|
-
// Replace entire credential assignment
|
|
156
|
-
return match.split(/[:=]/)[0] + '=***';
|
|
157
|
-
});
|
|
155
|
+
// Completely remove matched credential patterns
|
|
156
|
+
sanitized = sanitized.replace(pattern, '');
|
|
158
157
|
}
|
|
159
158
|
|
|
159
|
+
// Clean up extra whitespace left by removed patterns
|
|
160
|
+
sanitized = sanitized.replace(/\s+/g, ' ').trim();
|
|
161
|
+
|
|
160
162
|
return sanitized;
|
|
161
163
|
}
|
|
162
164
|
|
|
@@ -170,7 +172,7 @@ export class ErrorSanitizer {
|
|
|
170
172
|
let sanitized = text;
|
|
171
173
|
|
|
172
174
|
for (const pattern of SENSITIVE_PATTERNS.filePaths) {
|
|
173
|
-
sanitized = sanitized.replace(pattern, '
|
|
175
|
+
sanitized = sanitized.replace(pattern, '');
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
return sanitized;
|
|
@@ -186,11 +188,31 @@ export class ErrorSanitizer {
|
|
|
186
188
|
let sanitized = text;
|
|
187
189
|
|
|
188
190
|
for (const pattern of SENSITIVE_PATTERNS.environmentVars) {
|
|
189
|
-
sanitized = sanitized.replace(pattern,
|
|
190
|
-
return match.split('=')[0] + '=***';
|
|
191
|
-
});
|
|
191
|
+
sanitized = sanitized.replace(pattern, '');
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// Clean up extra whitespace left by removed patterns
|
|
195
|
+
sanitized = sanitized.replace(/\s+/g, ' ').trim();
|
|
196
|
+
|
|
197
|
+
return sanitized;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Remove stack trace patterns from text
|
|
202
|
+
* @param {string} text - Text to sanitize
|
|
203
|
+
* @returns {string} Sanitized text
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
_removeStackTraces(text) {
|
|
207
|
+
let sanitized = text;
|
|
208
|
+
|
|
209
|
+
for (const pattern of SENSITIVE_PATTERNS.stackTraces) {
|
|
210
|
+
sanitized = sanitized.replace(pattern, '');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Clean up extra whitespace left by removed patterns
|
|
214
|
+
sanitized = sanitized.replace(/\s+/g, ' ').trim();
|
|
215
|
+
|
|
194
216
|
return sanitized;
|
|
195
217
|
}
|
|
196
218
|
|