@unrdf/hooks 26.4.3 → 26.4.7
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 +566 -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/hook-chains/package.json +2 -2
- package/examples/hooks-marketplace.mjs +261 -0
- package/examples/n3-reasoning-example.mjs +279 -0
- package/examples/policy-hooks/README.md +5 -9
- 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/policy-hooks/package.json +2 -2
- package/examples/shacl-repair-example.mjs +191 -0
- package/examples/window-condition-example.mjs +285 -0
- package/package.json +27 -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 +17 -9
- package/src/hooks/condition-evaluator.mjs +681 -77
- package/src/hooks/define-hook.mjs +23 -23
- package/src/hooks/effect-executor.mjs +618 -0
- package/src/hooks/effect-sandbox.mjs +19 -9
- package/src/hooks/file-resolver.mjs +155 -1
- package/src/hooks/hook-chain-compiler.mjs +10 -1
- package/src/hooks/hook-executor.mjs +102 -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 +464 -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
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AtomVM/Erlang Bridge for Knowledge Hooks
|
|
3
|
+
* @module hooks/atomvm-bridge
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Bridge for executing knowledge hooks from Erlang/BEAM processes via AtomVM.
|
|
7
|
+
* Enables hooks to be registered, evaluated, and executed from distributed systems.
|
|
8
|
+
*
|
|
9
|
+
* Usage from Erlang:
|
|
10
|
+
* ```erlang
|
|
11
|
+
* % Via HTTP gateway:
|
|
12
|
+
* {ok, HookId} = atomvm_bridge:register_hook(#{
|
|
13
|
+
* <<"name">> => <<"compliance">>,
|
|
14
|
+
* <<"condition">> => #{<<"kind">> => <<"sparql-ask">>}
|
|
15
|
+
* }),
|
|
16
|
+
* {ok, Result} = atomvm_bridge:evaluate_condition(Condition).
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { evaluateCondition, KnowledgeHookEngine } from './index.mjs';
|
|
21
|
+
import { validateKnowledgeHook } from './schemas.mjs';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* HooksBridge - AtomVM/Erlang integration for Knowledge Hooks
|
|
25
|
+
*
|
|
26
|
+
* Provides a stateful API for managing hooks across distributed systems.
|
|
27
|
+
* Each bridge instance maintains its own registry and execution context.
|
|
28
|
+
*
|
|
29
|
+
* @class HooksBridge
|
|
30
|
+
*/
|
|
31
|
+
export class HooksBridge {
|
|
32
|
+
/**
|
|
33
|
+
* Create a new HooksBridge instance.
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} store - RDF store (oxigraph)
|
|
36
|
+
* @param {Object} [options] - Bridge options
|
|
37
|
+
* @param {boolean} [options.persistReceipts=true] - Persist receipt chain
|
|
38
|
+
* @param {number} [options.maxHooks=1000] - Max hooks in registry
|
|
39
|
+
* @param {number} [options.maxReceiptHistory=10000] - Max receipts to keep
|
|
40
|
+
*/
|
|
41
|
+
constructor(store, options = {}) {
|
|
42
|
+
this.store = store;
|
|
43
|
+
this.options = {
|
|
44
|
+
persistReceipts: options.persistReceipts ?? true,
|
|
45
|
+
maxHooks: options.maxHooks ?? 1000,
|
|
46
|
+
maxReceiptHistory: options.maxReceiptHistory ?? 10000,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
this.hookRegistry = new Map(); // hookId -> hook definition
|
|
50
|
+
this.receiptChain = []; // Array of receipts in order
|
|
51
|
+
this.engine = new KnowledgeHookEngine(store);
|
|
52
|
+
this.nextHookId = 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Register a hook definition (typically from Erlang).
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} hookDef - Hook definition
|
|
59
|
+
* @param {string} hookDef.name - Hook name
|
|
60
|
+
* @param {Object} hookDef.condition - Condition (9 kinds)
|
|
61
|
+
* @param {Array} [hookDef.effects] - Effects
|
|
62
|
+
* @param {Object} [hookDef.metadata] - Custom metadata
|
|
63
|
+
* @returns {Promise<string>} Hook ID
|
|
64
|
+
*
|
|
65
|
+
* @throws {Error} If hook definition invalid or registry full
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const hookId = await bridge.registerHook({
|
|
69
|
+
* name: 'erlang-check',
|
|
70
|
+
* condition: { kind: 'sparql-ask', query: 'ASK { ?s ?p ?o }' },
|
|
71
|
+
* effects: [{ kind: 'sparql-construct', query: '...' }]
|
|
72
|
+
* });
|
|
73
|
+
*/
|
|
74
|
+
async registerHook(hookDef) {
|
|
75
|
+
// Validate hook definition
|
|
76
|
+
const validation = validateKnowledgeHook(hookDef);
|
|
77
|
+
if (!validation.success) {
|
|
78
|
+
throw new Error(`Invalid hook definition: ${validation.error.message}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check registry size
|
|
82
|
+
if (this.hookRegistry.size >= this.options.maxHooks) {
|
|
83
|
+
throw new Error(`Hook registry full (max ${this.options.maxHooks})`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Assign ID and store
|
|
87
|
+
const hookId = String(this.nextHookId++);
|
|
88
|
+
this.hookRegistry.set(hookId, hookDef);
|
|
89
|
+
|
|
90
|
+
return hookId;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get a registered hook by ID.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} hookId - Hook ID from registerHook()
|
|
97
|
+
* @returns {Object|null} Hook definition or null if not found
|
|
98
|
+
*/
|
|
99
|
+
getHook(hookId) {
|
|
100
|
+
return this.hookRegistry.get(hookId) ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Unregister a hook by ID.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} hookId - Hook ID to remove
|
|
107
|
+
* @returns {boolean} True if hook was removed, false if not found
|
|
108
|
+
*/
|
|
109
|
+
unregisterHook(hookId) {
|
|
110
|
+
return this.hookRegistry.delete(hookId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* List all registered hooks.
|
|
115
|
+
*
|
|
116
|
+
* @returns {Array<{id: string, name: string}>} Array of hook metadata
|
|
117
|
+
*/
|
|
118
|
+
listHooks() {
|
|
119
|
+
return Array.from(this.hookRegistry.entries()).map(([id, def]) => ({
|
|
120
|
+
id,
|
|
121
|
+
name: def.name,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Evaluate a condition against the store (typically from Erlang).
|
|
127
|
+
*
|
|
128
|
+
* @param {Object} condition - Condition definition (9 kinds)
|
|
129
|
+
* @param {string} [condition.kind] - Condition kind (sparql-ask, shacl, etc.)
|
|
130
|
+
* @returns {Promise<boolean>} Condition evaluation result
|
|
131
|
+
*
|
|
132
|
+
* @throws {Error} If condition invalid or evaluation fails
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* // SPARQL ASK condition
|
|
136
|
+
* const result = await bridge.evaluateCondition({
|
|
137
|
+
* kind: 'sparql-ask',
|
|
138
|
+
* query: 'ASK { ?s a ex:Person }'
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* // Datalog condition
|
|
142
|
+
* const result = await bridge.evaluateCondition({
|
|
143
|
+
* kind: 'datalog',
|
|
144
|
+
* facts: ['user(alice)', 'admin(alice)'],
|
|
145
|
+
* rules: ['allowed(X) :- admin(X)'],
|
|
146
|
+
* goal: 'allowed(alice)'
|
|
147
|
+
* });
|
|
148
|
+
*/
|
|
149
|
+
async evaluateCondition(condition) {
|
|
150
|
+
if (!condition || typeof condition !== 'object') {
|
|
151
|
+
throw new Error('Condition must be a valid object');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!condition.kind) {
|
|
155
|
+
throw new Error('Condition must have a "kind" property (9 kinds supported)');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return evaluateCondition(condition, this.store);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Execute hooks with receipt chaining (typically from Erlang).
|
|
163
|
+
*
|
|
164
|
+
* @param {Object} context - Execution context
|
|
165
|
+
* @param {string} context.nodeId - Application identifier
|
|
166
|
+
* @param {bigint} context.t_ns - Timestamp in nanoseconds
|
|
167
|
+
* @param {string} [context.previousReceiptHash] - Link to prior operation
|
|
168
|
+
* @param {Array<string>} hookIds - Hook IDs to execute (from registerHook)
|
|
169
|
+
* @returns {Promise<Object>} Execution result with receipt
|
|
170
|
+
*
|
|
171
|
+
* @throws {Error} If hooks not found or execution fails
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* const result = await bridge.executeHooks(
|
|
175
|
+
* {
|
|
176
|
+
* nodeId: 'erlang-app',
|
|
177
|
+
* t_ns: BigInt(Date.now() * 1000000),
|
|
178
|
+
* previousReceiptHash: 'prior-hash'
|
|
179
|
+
* },
|
|
180
|
+
* ['hook1', 'hook2']
|
|
181
|
+
* );
|
|
182
|
+
*
|
|
183
|
+
* console.log('Receipt:', result.receipt.receiptHash);
|
|
184
|
+
* console.log('Delta:', result.receipt.delta.adds.length, 'additions');
|
|
185
|
+
*/
|
|
186
|
+
async executeHooks(context, hookIds) {
|
|
187
|
+
if (!context || !context.nodeId) {
|
|
188
|
+
throw new Error('Context must have nodeId property');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!Array.isArray(hookIds)) {
|
|
192
|
+
throw new Error('hookIds must be an array');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Resolve hook definitions
|
|
196
|
+
const hooks = [];
|
|
197
|
+
for (const hookId of hookIds) {
|
|
198
|
+
const hook = this.getHook(hookId);
|
|
199
|
+
if (!hook) {
|
|
200
|
+
throw new Error(`Hook not found: ${hookId}`);
|
|
201
|
+
}
|
|
202
|
+
hooks.push(hook);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Execute via engine (provides receipt chaining)
|
|
206
|
+
const result = await this.engine.execute(context, hooks);
|
|
207
|
+
|
|
208
|
+
// Persist receipt if enabled
|
|
209
|
+
if (this.options.persistReceipts && result.receipt) {
|
|
210
|
+
this.receiptChain.push(result.receipt);
|
|
211
|
+
|
|
212
|
+
// Prune old receipts if necessary
|
|
213
|
+
if (this.receiptChain.length > this.options.maxReceiptHistory) {
|
|
214
|
+
this.receiptChain = this.receiptChain.slice(-this.options.maxReceiptHistory);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get entire receipt chain (for audit trail).
|
|
223
|
+
*
|
|
224
|
+
* @returns {Array<Object>} Receipts in execution order
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* const chain = bridge.getReceiptChain();
|
|
228
|
+
* chain.forEach((r, i) => {
|
|
229
|
+
* console.log(`Step ${i}: ${r.receiptHash}`);
|
|
230
|
+
* console.log(` Previous: ${r.previousReceiptHash}`);
|
|
231
|
+
* console.log(` Delta: +${r.delta.adds.length} -${r.delta.deletes.length}`);
|
|
232
|
+
* });
|
|
233
|
+
*/
|
|
234
|
+
getReceiptChain() {
|
|
235
|
+
return [...this.receiptChain];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Verify receipt chain integrity (all previousReceiptHash links are valid).
|
|
240
|
+
*
|
|
241
|
+
* @returns {Object} Verification result
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* const result = bridge.verifyReceiptChain();
|
|
245
|
+
* if (result.valid) {
|
|
246
|
+
* console.log('✓ Chain verified: 42 receipts, unbroken links');
|
|
247
|
+
* } else {
|
|
248
|
+
* console.log('✗ Chain broken at step:', result.brokenAt);
|
|
249
|
+
* }
|
|
250
|
+
*/
|
|
251
|
+
verifyReceiptChain() {
|
|
252
|
+
if (this.receiptChain.length === 0) {
|
|
253
|
+
return { valid: true, length: 0 };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (let i = 1; i < this.receiptChain.length; i++) {
|
|
257
|
+
const current = this.receiptChain[i];
|
|
258
|
+
const previous = this.receiptChain[i - 1];
|
|
259
|
+
|
|
260
|
+
if (current.previousReceiptHash !== previous.receiptHash) {
|
|
261
|
+
return {
|
|
262
|
+
valid: false,
|
|
263
|
+
brokenAt: i,
|
|
264
|
+
expected: previous.receiptHash,
|
|
265
|
+
got: current.previousReceiptHash,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
valid: true,
|
|
272
|
+
length: this.receiptChain.length,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Clear receipt history (does not affect registered hooks).
|
|
278
|
+
*
|
|
279
|
+
* @returns {number} Number of receipts cleared
|
|
280
|
+
*/
|
|
281
|
+
clearReceiptChain() {
|
|
282
|
+
const count = this.receiptChain.length;
|
|
283
|
+
this.receiptChain = [];
|
|
284
|
+
return count;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get bridge statistics.
|
|
289
|
+
*
|
|
290
|
+
* @returns {Object} Stats object
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* const stats = bridge.getStats();
|
|
294
|
+
* console.log('Hooks registered:', stats.hookCount);
|
|
295
|
+
* console.log('Receipt chain length:', stats.receiptCount);
|
|
296
|
+
* console.log('Memory usage:', stats.memoryBytes);
|
|
297
|
+
*/
|
|
298
|
+
getStats() {
|
|
299
|
+
return {
|
|
300
|
+
hookCount: this.hookRegistry.size,
|
|
301
|
+
nextHookId: this.nextHookId,
|
|
302
|
+
receiptCount: this.receiptChain.length,
|
|
303
|
+
maxHooks: this.options.maxHooks,
|
|
304
|
+
maxReceiptHistory: this.options.maxReceiptHistory,
|
|
305
|
+
memoryBytes: JSON.stringify({
|
|
306
|
+
hooks: Array.from(this.hookRegistry.values()),
|
|
307
|
+
receipts: this.receiptChain,
|
|
308
|
+
}).length,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Create a HooksBridge instance.
|
|
315
|
+
*
|
|
316
|
+
* @param {Object} store - RDF store
|
|
317
|
+
* @param {Object} [options] - Bridge options
|
|
318
|
+
* @returns {HooksBridge} New bridge instance
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* import { createHooksBridge } from '@unrdf/hooks/atomvm';
|
|
322
|
+
* import { createStore } from '@unrdf/oxigraph';
|
|
323
|
+
*
|
|
324
|
+
* const store = createStore();
|
|
325
|
+
* const bridge = createHooksBridge(store);
|
|
326
|
+
*
|
|
327
|
+
* const hookId = await bridge.registerHook({...});
|
|
328
|
+
* const result = await bridge.executeHooks({...}, [hookId]);
|
|
329
|
+
*/
|
|
330
|
+
export function createHooksBridge(store, options) {
|
|
331
|
+
return new HooksBridge(store, options);
|
|
332
|
+
}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { defineHook } from './define-hook.mjs';
|
|
7
|
-
import { dataFactory } from '../../../oxigraph/src/index.mjs';
|
|
8
7
|
import { quadPool } from './quad-pool.mjs';
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -152,14 +151,17 @@ export const normalizeLanguageTag = defineHook({
|
|
|
152
151
|
return quad;
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
//
|
|
154
|
+
// Explicitly copy quad properties (spread doesn't copy prototype getters)
|
|
156
155
|
return {
|
|
157
|
-
|
|
156
|
+
subject: quad.subject,
|
|
157
|
+
predicate: quad.predicate,
|
|
158
158
|
object: {
|
|
159
|
-
|
|
159
|
+
termType: quad.object.termType,
|
|
160
160
|
value: quad.object.value,
|
|
161
|
+
datatype: quad.object.datatype,
|
|
161
162
|
language: quad.object.language.toLowerCase(),
|
|
162
163
|
},
|
|
164
|
+
graph: quad.graph,
|
|
163
165
|
};
|
|
164
166
|
},
|
|
165
167
|
metadata: {
|
|
@@ -178,13 +180,17 @@ export const trimLiterals = defineHook({
|
|
|
178
180
|
return quad;
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
//
|
|
183
|
+
// Explicitly copy quad properties (spread doesn't copy prototype getters)
|
|
182
184
|
return {
|
|
183
|
-
|
|
185
|
+
subject: quad.subject,
|
|
186
|
+
predicate: quad.predicate,
|
|
184
187
|
object: {
|
|
185
|
-
|
|
188
|
+
termType: quad.object.termType,
|
|
186
189
|
value: quad.object.value.trim(),
|
|
190
|
+
datatype: quad.object.datatype,
|
|
191
|
+
language: quad.object.language,
|
|
187
192
|
},
|
|
193
|
+
graph: quad.graph,
|
|
188
194
|
};
|
|
189
195
|
},
|
|
190
196
|
metadata: {
|
|
@@ -250,7 +256,8 @@ export const normalizeLanguageTagPooled = defineHook({
|
|
|
250
256
|
quad.subject,
|
|
251
257
|
quad.predicate,
|
|
252
258
|
{
|
|
253
|
-
|
|
259
|
+
value: quad.object.value,
|
|
260
|
+
datatype: quad.object.datatype,
|
|
254
261
|
language: quad.object.language.toLowerCase(),
|
|
255
262
|
},
|
|
256
263
|
quad.graph
|
|
@@ -281,8 +288,9 @@ export const trimLiteralsPooled = defineHook({
|
|
|
281
288
|
quad.subject,
|
|
282
289
|
quad.predicate,
|
|
283
290
|
{
|
|
284
|
-
...quad.object,
|
|
285
291
|
value: trimmed,
|
|
292
|
+
datatype: quad.object.datatype,
|
|
293
|
+
language: quad.object.language,
|
|
286
294
|
},
|
|
287
295
|
quad.graph
|
|
288
296
|
);
|