@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.
Files changed (59) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +566 -53
  3. package/examples/atomvm-fibo-hooks-demo.mjs +323 -0
  4. package/examples/delta-monitoring-example.mjs +213 -0
  5. package/examples/fibo-jtbd-governance.mjs +388 -0
  6. package/examples/hook-chains/node_modules/.bin/jiti +0 -0
  7. package/examples/hook-chains/node_modules/.bin/msw +0 -0
  8. package/examples/hook-chains/node_modules/.bin/terser +0 -0
  9. package/examples/hook-chains/node_modules/.bin/tsc +0 -0
  10. package/examples/hook-chains/node_modules/.bin/tsserver +0 -0
  11. package/examples/hook-chains/node_modules/.bin/tsx +0 -0
  12. package/examples/hook-chains/node_modules/.bin/validate-hooks +0 -0
  13. package/examples/hook-chains/node_modules/.bin/vite +0 -0
  14. package/examples/hook-chains/node_modules/.bin/vitest +0 -0
  15. package/examples/hook-chains/node_modules/.bin/yaml +0 -0
  16. package/examples/hook-chains/package.json +2 -2
  17. package/examples/hooks-marketplace.mjs +261 -0
  18. package/examples/n3-reasoning-example.mjs +279 -0
  19. package/examples/policy-hooks/README.md +5 -9
  20. package/examples/policy-hooks/node_modules/.bin/jiti +0 -0
  21. package/examples/policy-hooks/node_modules/.bin/msw +0 -0
  22. package/examples/policy-hooks/node_modules/.bin/terser +0 -0
  23. package/examples/policy-hooks/node_modules/.bin/tsc +0 -0
  24. package/examples/policy-hooks/node_modules/.bin/tsserver +0 -0
  25. package/examples/policy-hooks/node_modules/.bin/tsx +0 -0
  26. package/examples/policy-hooks/node_modules/.bin/validate-hooks +0 -0
  27. package/examples/policy-hooks/node_modules/.bin/vite +0 -0
  28. package/examples/policy-hooks/node_modules/.bin/vitest +0 -0
  29. package/examples/policy-hooks/node_modules/.bin/yaml +0 -0
  30. package/examples/policy-hooks/package.json +2 -2
  31. package/examples/shacl-repair-example.mjs +191 -0
  32. package/examples/window-condition-example.mjs +285 -0
  33. package/package.json +27 -23
  34. package/src/atomvm.mjs +9 -0
  35. package/src/define.mjs +114 -0
  36. package/src/executor.mjs +23 -0
  37. package/src/hooks/atomvm-bridge.mjs +332 -0
  38. package/src/hooks/builtin-hooks.mjs +17 -9
  39. package/src/hooks/condition-evaluator.mjs +681 -77
  40. package/src/hooks/define-hook.mjs +23 -23
  41. package/src/hooks/effect-executor.mjs +618 -0
  42. package/src/hooks/effect-sandbox.mjs +19 -9
  43. package/src/hooks/file-resolver.mjs +155 -1
  44. package/src/hooks/hook-chain-compiler.mjs +10 -1
  45. package/src/hooks/hook-executor.mjs +102 -73
  46. package/src/hooks/knowledge-hook-engine.mjs +133 -7
  47. package/src/hooks/ontology-learner.mjs +190 -0
  48. package/src/hooks/query.mjs +3 -3
  49. package/src/hooks/schemas.mjs +47 -3
  50. package/src/hooks/security/error-sanitizer.mjs +46 -24
  51. package/src/hooks/self-play-autonomics.mjs +464 -0
  52. package/src/hooks/telemetry.mjs +32 -9
  53. package/src/hooks/validate.mjs +100 -33
  54. package/src/index.mjs +2 -0
  55. package/src/lib/admit-hook.mjs +615 -0
  56. package/src/policy-compiler.mjs +12 -2
  57. package/dist/index.d.mts +0 -1738
  58. package/dist/index.d.ts +0 -1738
  59. 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
- // Create new quad with lowercase language tag
154
+ // Explicitly copy quad properties (spread doesn't copy prototype getters)
156
155
  return {
157
- ...quad,
156
+ subject: quad.subject,
157
+ predicate: quad.predicate,
158
158
  object: {
159
- ...quad.object,
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
- // Create new quad with trimmed literal
183
+ // Explicitly copy quad properties (spread doesn't copy prototype getters)
182
184
  return {
183
- ...quad,
185
+ subject: quad.subject,
186
+ predicate: quad.predicate,
184
187
  object: {
185
- ...quad.object,
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
- ...quad.object,
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
  );