eyeling 1.19.1 → 1.19.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.
package/HANDBOOK.md CHANGED
@@ -2085,9 +2085,24 @@ const { reason } = require('eyeling');
2085
2085
  const out = reason({ builtinModules: ['./hello-builtin.js'] }, n3Text);
2086
2086
  ```
2087
2087
 
2088
+ ### 16.2.1 Stability rule for `--builtin`
2089
+
2090
+ Eyeling keeps `--builtin` simple.
2091
+
2092
+ There is one small helper API passed into builtin modules. That helper object is frozen, its key set is regression-tested, and builtin modules must use one of the documented export forms.
2093
+
2094
+ In practice, this means:
2095
+
2096
+ - builtin module loading accepts only the documented export forms
2097
+ - the helper API exposed by `__buildBuiltinRegistrationApi()` has a fixed key set
2098
+ - builtin handlers should return an array of substitution objects
2099
+ - accidental helper drift is caught by `test/builtin-contract.test.js`
2100
+
2101
+ This is only meant to stop silent breakage. It is **not** a promise that Eyeling can never change the builtin API. If the helper surface ever needs to change, that change should be deliberate, documented, and called out in release notes.
2102
+
2088
2103
  ### 16.3 What a builtin module may export
2089
2104
 
2090
- Eyeling accepts three module shapes.
2105
+ Eyeling accepts these stable module shapes.
2091
2106
 
2092
2107
  #### A function export
2093
2108
 
@@ -2124,23 +2139,52 @@ module.exports = {
2124
2139
  };
2125
2140
  ```
2126
2141
 
2142
+ #### An object with `.builtins`
2143
+
2144
+ ```js
2145
+ module.exports = {
2146
+ builtins: {
2147
+ 'http://example.org/custom#ok': ({ subst }) => [subst],
2148
+ },
2149
+ };
2150
+ ```
2151
+
2152
+ #### An object with `.default` as a plain object map
2153
+
2154
+ This is mainly an ESM/transpiler compatibility form.
2155
+
2156
+ ```js
2157
+ module.exports = {
2158
+ default: {
2159
+ 'http://example.org/custom#ok': ({ subst }) => [subst],
2160
+ },
2161
+ };
2162
+ ```
2163
+
2127
2164
  If none of those shapes match, Eyeling rejects the module with a descriptive error.
2128
2165
 
2129
2166
  ### 16.4 The handler contract
2130
2167
 
2131
- Builtin handlers are called with a context object like:
2168
+ Builtin handlers are called with a context object containing:
2132
2169
 
2133
2170
  - `iri` — the predicate IRI string
2134
2171
  - `goal` — the current triple goal
2135
2172
  - `subst` — the current substitution
2136
- - `facts`, `backRules`, `depth`, `varGen`, `maxResults`
2173
+ - `facts` the active fact store
2174
+ - `backRules` — the backward-rule set
2175
+ - `depth` — current proof depth
2176
+ - `varGen` — the variable generator state
2177
+ - `maxResults` — current result cap
2137
2178
  - `api` — the same registration/helper API used by modules
2138
2179
 
2139
- A handler returns **an array of substitutions**:
2180
+ A handler should return an **array of substitution objects**:
2140
2181
 
2141
2182
  - `[]` means failure / no solutions
2142
- - `[subst2]` means one successful continuation
2143
- - multiple substitutions mean a generator builtin
2183
+ - `[{}]` means success with no new bindings
2184
+ - `[{ ...delta }]` means one successful continuation with bindings
2185
+ - multiple objects mean a generator builtin
2186
+
2187
+ Returning something else is rejected at runtime.
2144
2188
 
2145
2189
  In practice:
2146
2190
 
@@ -2153,13 +2197,24 @@ Custom builtin failures are wrapped so the predicate IRI appears in the thrown e
2153
2197
 
2154
2198
  ### 16.5 The helper API exposed to builtin modules
2155
2199
 
2156
- Builtin modules do not need to import internal engine files directly. Eyeling passes a helper API into module registration, including:
2200
+ Builtin modules do not need to import internal engine files directly. Eyeling passes a helper API into module registration, and that helper surface is kept intentionally small.
2201
+
2202
+ The current helper function set is:
2203
+
2204
+ - `registerBuiltin`, `unregisterBuiltin`, `listBuiltinIris`
2205
+ - `internIri`, `internLiteral`, `literalParts`
2206
+ - `termToJsString`, `termToJsStringDecoded`, `termToN3`, `iriValue`
2207
+ - `unifyTerm`, `applySubstTerm`, `applySubstTriple`, `proveGoals`, `isGroundTerm`
2208
+ - `computeConclusionFromFormula`, `skolemIriFromGroundTerm`
2209
+ - `parseBooleanLiteralInfo`, `parseNumericLiteralInfo`, `parseXsdDecimalToBigIntScale`, `pow10n`
2210
+ - `normalizeLiteralForFastKey`, `literalsEquivalentAsXsdString`, `materializeRdfLists`
2211
+
2212
+ The stable namespace bags are:
2213
+
2214
+ - `terms`: `Literal`, `Iri`, `Var`, `Blank`, `ListTerm`, `OpenListTerm`, `GraphTerm`, `Triple`, `Rule`
2215
+ - `ns`: `RDF_NS`, `XSD_NS`, `CRYPTO_NS`, `MATH_NS`, `TIME_NS`, `LIST_NS`, `LOG_NS`, `STRING_NS`
2157
2216
 
2158
- - registration helpers: `registerBuiltin`, `unregisterBuiltin`, `listBuiltinIris`
2159
- - term constructors via `terms` (`Literal`, `Iri`, `Var`, `Blank`, `ListTerm`, `GraphTerm`, `Triple`, `Rule`, ...)
2160
- - literal/term helpers such as `internLiteral`, `internIri`, `literalParts`, `termToN3`, `termToJsString`
2161
- - reasoning helpers such as `unifyTerm`, `applySubstTerm`, `applySubstTriple`, `proveGoals`
2162
- - namespace constants via `ns`
2217
+ The helper object is frozen and regression-tested so helper additions, removals, and renames do not slip in silently.
2163
2218
 
2164
2219
  That API keeps the extension boundary explicit: custom builtins get the operations they need without reaching into Eyeling’s private module graph.
2165
2220
 
package/eyeling.js CHANGED
@@ -558,8 +558,15 @@ function registerBuiltin(iri, handler) {
558
558
  if (typeof handler !== 'function') {
559
559
  throw new TypeError(`Custom builtin ${iri} must be registered with a function handler`);
560
560
  }
561
- __customBuiltinHandlers.set(iri, handler);
562
- return handler;
561
+
562
+ const wrapped = function builtinResultWrapper(ctx) {
563
+ const out = handler(ctx);
564
+ __assertBuiltinHandlerResult(iri, out);
565
+ return out;
566
+ };
567
+
568
+ __customBuiltinHandlers.set(iri, wrapped);
569
+ return wrapped;
563
570
  }
564
571
 
565
572
  function unregisterBuiltin(iri) {
@@ -570,8 +577,30 @@ function listBuiltinIris() {
570
577
  return Array.from(__customBuiltinHandlers.keys()).sort();
571
578
  }
572
579
 
580
+ let __builtinApiSingleton = null;
581
+
582
+ function __freezeBuiltinApi(api) {
583
+ Object.freeze(api.terms);
584
+ Object.freeze(api.ns);
585
+ return Object.freeze(api);
586
+ }
587
+
588
+ function __assertBuiltinHandlerResult(iri, out) {
589
+ if (out == null) return;
590
+ if (!Array.isArray(out)) {
591
+ throw new TypeError(`Custom builtin ${iri} must return an array of substitution deltas`);
592
+ }
593
+ for (const delta of out) {
594
+ if (!delta || typeof delta !== 'object' || Array.isArray(delta)) {
595
+ throw new TypeError(`Custom builtin ${iri} must return an array of substitution deltas`);
596
+ }
597
+ }
598
+ }
599
+
573
600
  function __buildBuiltinRegistrationApi() {
574
- return {
601
+ if (__builtinApiSingleton) return __builtinApiSingleton;
602
+
603
+ const api = {
575
604
  registerBuiltin,
576
605
  unregisterBuiltin,
577
606
  listBuiltinIris,
@@ -599,6 +628,9 @@ function __buildBuiltinRegistrationApi() {
599
628
  terms: { Literal, Iri, Var, Blank, ListTerm, OpenListTerm, GraphTerm, Triple, Rule },
600
629
  ns: { RDF_NS, XSD_NS, CRYPTO_NS, MATH_NS, TIME_NS, LIST_NS, LOG_NS, STRING_NS },
601
630
  };
631
+
632
+ __builtinApiSingleton = __freezeBuiltinApi(api);
633
+ return __builtinApiSingleton;
602
634
  }
603
635
 
604
636
  function registerBuiltinModule(mod, origin = '<builtin-module>') {
@@ -675,9 +707,7 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
675
707
  try {
676
708
  const out = handler(ctx);
677
709
  if (out == null) return [];
678
- if (!Array.isArray(out)) {
679
- throw new TypeError(`Custom builtin ${pv} must return an array of substitution deltas`);
680
- }
710
+ __assertBuiltinHandlerResult(pv, out);
681
711
  return out;
682
712
  } catch (err) {
683
713
  if (err && typeof err === 'object' && typeof err.message === 'string') {
@@ -4472,6 +4502,7 @@ module.exports = {
4472
4502
  registerBuiltinModule,
4473
4503
  loadBuiltinModule,
4474
4504
  listBuiltinIris,
4505
+ __testBuildBuiltinApi: __buildBuiltinRegistrationApi,
4475
4506
  // shared helpers used by engine/explain
4476
4507
  parseBooleanLiteralInfo,
4477
4508
  parseNumericLiteralInfo,
package/lib/builtins.js CHANGED
@@ -78,8 +78,15 @@ function registerBuiltin(iri, handler) {
78
78
  if (typeof handler !== 'function') {
79
79
  throw new TypeError(`Custom builtin ${iri} must be registered with a function handler`);
80
80
  }
81
- __customBuiltinHandlers.set(iri, handler);
82
- return handler;
81
+
82
+ const wrapped = function builtinResultWrapper(ctx) {
83
+ const out = handler(ctx);
84
+ __assertBuiltinHandlerResult(iri, out);
85
+ return out;
86
+ };
87
+
88
+ __customBuiltinHandlers.set(iri, wrapped);
89
+ return wrapped;
83
90
  }
84
91
 
85
92
  function unregisterBuiltin(iri) {
@@ -90,8 +97,30 @@ function listBuiltinIris() {
90
97
  return Array.from(__customBuiltinHandlers.keys()).sort();
91
98
  }
92
99
 
100
+ let __builtinApiSingleton = null;
101
+
102
+ function __freezeBuiltinApi(api) {
103
+ Object.freeze(api.terms);
104
+ Object.freeze(api.ns);
105
+ return Object.freeze(api);
106
+ }
107
+
108
+ function __assertBuiltinHandlerResult(iri, out) {
109
+ if (out == null) return;
110
+ if (!Array.isArray(out)) {
111
+ throw new TypeError(`Custom builtin ${iri} must return an array of substitution deltas`);
112
+ }
113
+ for (const delta of out) {
114
+ if (!delta || typeof delta !== 'object' || Array.isArray(delta)) {
115
+ throw new TypeError(`Custom builtin ${iri} must return an array of substitution deltas`);
116
+ }
117
+ }
118
+ }
119
+
93
120
  function __buildBuiltinRegistrationApi() {
94
- return {
121
+ if (__builtinApiSingleton) return __builtinApiSingleton;
122
+
123
+ const api = {
95
124
  registerBuiltin,
96
125
  unregisterBuiltin,
97
126
  listBuiltinIris,
@@ -119,6 +148,9 @@ function __buildBuiltinRegistrationApi() {
119
148
  terms: { Literal, Iri, Var, Blank, ListTerm, OpenListTerm, GraphTerm, Triple, Rule },
120
149
  ns: { RDF_NS, XSD_NS, CRYPTO_NS, MATH_NS, TIME_NS, LIST_NS, LOG_NS, STRING_NS },
121
150
  };
151
+
152
+ __builtinApiSingleton = __freezeBuiltinApi(api);
153
+ return __builtinApiSingleton;
122
154
  }
123
155
 
124
156
  function registerBuiltinModule(mod, origin = '<builtin-module>') {
@@ -195,9 +227,7 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
195
227
  try {
196
228
  const out = handler(ctx);
197
229
  if (out == null) return [];
198
- if (!Array.isArray(out)) {
199
- throw new TypeError(`Custom builtin ${pv} must return an array of substitution deltas`);
200
- }
230
+ __assertBuiltinHandlerResult(pv, out);
201
231
  return out;
202
232
  } catch (err) {
203
233
  if (err && typeof err === 'object' && typeof err.message === 'string') {
@@ -3992,6 +4022,7 @@ module.exports = {
3992
4022
  registerBuiltinModule,
3993
4023
  loadBuiltinModule,
3994
4024
  listBuiltinIris,
4025
+ __testBuildBuiltinApi: __buildBuiltinRegistrationApi,
3995
4026
  // shared helpers used by engine/explain
3996
4027
  parseBooleanLiteralInfo,
3997
4028
  parseNumericLiteralInfo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.19.1",
3
+ "version": "1.19.3",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -40,12 +40,13 @@
40
40
  "build": "node tools/bundle.js",
41
41
  "test:packlist": "node test/packlist.test.js",
42
42
  "test:api": "node test/api.test.js",
43
+ "test:builtin-contract": "node test/builtin-contract.test.js",
43
44
  "test:n3gen": "node test/n3gen.test.js",
44
45
  "test:examples": "node test/examples.test.js",
45
46
  "test:manifest": "node test/manifest.test.js",
46
47
  "test:playground": "node test/playground.test.js",
47
48
  "test:package": "node test/package.test.js",
48
- "test:all": "npm run test:api && npm run test:n3gen && npm run test:examples && npm run test:manifest && npm run test:playground",
49
+ "test:all": "npm run test:api && npm run test:builtin-contract && npm run test:n3gen && npm run test:examples && npm run test:manifest && npm run test:playground",
49
50
  "pretest": "npm run build && npm run test:packlist",
50
51
  "test": "npm run test:all",
51
52
  "posttest": "npm run test:package",
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const path = require('node:path');
5
+
6
+ const TTY = process.stdout.isTTY;
7
+ const C = TTY
8
+ ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
9
+ : { g: '', r: '', y: '', dim: '', n: '' };
10
+
11
+ function ok(msg) {
12
+ console.log(`${C.g}OK ${C.n} ${msg}`);
13
+ }
14
+ function info(msg) {
15
+ console.log(`${C.y}==${C.n} ${msg}`);
16
+ }
17
+ function fail(msg) {
18
+ console.error(`${C.r}FAIL${C.n} ${msg}`);
19
+ }
20
+
21
+ const fixtures = path.join(__dirname, 'fixtures', 'builtins');
22
+ const builtins = require('../lib/builtins');
23
+ require('../lib/engine');
24
+
25
+ const expectedApiKeys = [
26
+ 'registerBuiltin',
27
+ 'unregisterBuiltin',
28
+ 'listBuiltinIris',
29
+ 'internIri',
30
+ 'internLiteral',
31
+ 'literalParts',
32
+ 'termToJsString',
33
+ 'termToJsStringDecoded',
34
+ 'termToN3',
35
+ 'iriValue',
36
+ 'unifyTerm',
37
+ 'applySubstTerm',
38
+ 'applySubstTriple',
39
+ 'proveGoals',
40
+ 'isGroundTerm',
41
+ 'computeConclusionFromFormula',
42
+ 'skolemIriFromGroundTerm',
43
+ 'parseBooleanLiteralInfo',
44
+ 'parseNumericLiteralInfo',
45
+ 'parseXsdDecimalToBigIntScale',
46
+ 'pow10n',
47
+ 'normalizeLiteralForFastKey',
48
+ 'literalsEquivalentAsXsdString',
49
+ 'materializeRdfLists',
50
+ 'terms',
51
+ 'ns',
52
+ ].sort();
53
+
54
+ const expectedTermsKeys = [
55
+ 'Literal',
56
+ 'Iri',
57
+ 'Var',
58
+ 'Blank',
59
+ 'ListTerm',
60
+ 'OpenListTerm',
61
+ 'GraphTerm',
62
+ 'Triple',
63
+ 'Rule',
64
+ ].sort();
65
+ const expectedNsKeys = ['RDF_NS', 'XSD_NS', 'CRYPTO_NS', 'MATH_NS', 'TIME_NS', 'LIST_NS', 'LOG_NS', 'STRING_NS'].sort();
66
+
67
+ const cases = [
68
+ {
69
+ name: 'builtin helper API stays stable and frozen',
70
+ run() {
71
+ const api = builtins.__testBuildBuiltinApi();
72
+ assert.deepEqual(Object.keys(api).sort(), expectedApiKeys);
73
+ assert.equal(Object.isFrozen(api), true);
74
+ assert.equal(Object.isFrozen(api.terms), true);
75
+ assert.equal(Object.isFrozen(api.ns), true);
76
+ assert.deepEqual(Object.keys(api.terms).sort(), expectedTermsKeys);
77
+ assert.deepEqual(Object.keys(api.ns).sort(), expectedNsKeys);
78
+ },
79
+ },
80
+ {
81
+ name: 'registerBuiltinModule accepts supported module export forms',
82
+ run() {
83
+ assert.doesNotThrow(() => builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-map.js')), 'ok-map'));
84
+ assert.doesNotThrow(() =>
85
+ builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-register.js')), 'ok-register'),
86
+ );
87
+ assert.doesNotThrow(() =>
88
+ builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-builtins.js')), 'ok-builtins'),
89
+ );
90
+ assert.doesNotThrow(() =>
91
+ builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-default-map.js')), 'ok-default-map'),
92
+ );
93
+ },
94
+ },
95
+ {
96
+ name: 'registerBuiltinModule rejects unsupported module exports',
97
+ run() {
98
+ assert.throws(
99
+ () => builtins.registerBuiltinModule(require(path.join(fixtures, 'bad-export.js')), 'bad-export'),
100
+ /must export a function, a \{ register\(\) \} object, or an object mapping predicate IRIs to handlers/,
101
+ );
102
+ },
103
+ },
104
+ {
105
+ name: 'registered builtin handlers must return substitution arrays',
106
+ run() {
107
+ const wrapped = builtins.registerBuiltin('http://example.org/test#shape-check', () => ({ nope: true }));
108
+ assert.throws(
109
+ () =>
110
+ wrapped({
111
+ iri: 'http://example.org/test#shape-check',
112
+ goal: {},
113
+ subst: {},
114
+ facts: [],
115
+ backRules: [],
116
+ depth: 0,
117
+ varGen: 0,
118
+ maxResults: 1,
119
+ api: builtins.__testBuildBuiltinApi(),
120
+ }),
121
+ /must return an array of substitution deltas/,
122
+ );
123
+ },
124
+ },
125
+ ];
126
+
127
+ let passed = 0;
128
+ let failed = 0;
129
+
130
+ (function main() {
131
+ const suiteStart = Date.now();
132
+ info(`Running ${cases.length} builtin contract tests`);
133
+
134
+ for (const tc of cases) {
135
+ const start = Date.now();
136
+ try {
137
+ tc.run();
138
+ ok(`${tc.name} ${C.dim}(${Date.now() - start} ms)${C.n}`);
139
+ passed++;
140
+ } catch (e) {
141
+ fail(`${tc.name} ${C.dim}(${Date.now() - start} ms)${C.n}`);
142
+ fail(e && e.stack ? e.stack : String(e));
143
+ failed++;
144
+ }
145
+ }
146
+
147
+ console.log('');
148
+ console.log(`${C.y}==${C.n} Total elapsed: ${Date.now() - suiteStart} ms`);
149
+ if (failed === 0) {
150
+ ok(`All builtin contract tests passed (${passed}/${cases.length})`);
151
+ process.exit(0);
152
+ }
153
+ fail(`Some builtin contract tests failed (${passed}/${cases.length})`);
154
+ process.exit(1);
155
+ })();
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = 42;
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ 'http://example.org/test#bad-return': () => ({ nope: true }),
5
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ builtins: {
5
+ 'http://example.org/test#ok-builtins': ({ subst }) => [subst],
6
+ },
7
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ default: {
5
+ 'http://example.org/test#ok-default-map': ({ subst }) => [subst],
6
+ },
7
+ };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ 'http://example.org/test#ok': ({ subst }) => [subst],
5
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ register(api) {
5
+ api.registerBuiltin('http://example.org/test#ok-register', ({ subst }) => [subst]);
6
+ },
7
+ };