eyeling 1.19.1 → 1.19.2

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,26 @@ 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 now treats the custom-builtin boundary as a **versioned API contract** rather than an informal convenience layer.
2091
+
2092
+ That matters for LLM-generated builtin modules: the model is free to decide **when** a builtin should be used, but it is **not** free to rename helpers, invent new helper names, change handler context fields, or change the expected return shape.
2093
+
2094
+ The stable rule is:
2095
+
2096
+ - builtin module loading accepts only the declared export forms
2097
+ - the helper API exposed by `__buildBuiltinRegistrationApi()` has an **exact key set**
2098
+ - helper names and helper arities are treated as public contract
2099
+ - builtin handlers receive an **exact** context object shape
2100
+ - builtin handlers must return an **array of substitution-delta objects**
2101
+ - any add/remove/rename of these contract elements is a **breaking change** and should bump the builtin API version
2102
+
2103
+ In code, this contract lives in `lib/builtin-contract.js`, is enforced at runtime by `lib/builtins.js`, and is locked by `test/builtin-contract.test.js`.
2104
+
2088
2105
  ### 16.3 What a builtin module may export
2089
2106
 
2090
- Eyeling accepts three module shapes.
2107
+ Eyeling accepts these stable module shapes.
2091
2108
 
2092
2109
  #### A function export
2093
2110
 
@@ -2124,23 +2141,54 @@ module.exports = {
2124
2141
  };
2125
2142
  ```
2126
2143
 
2144
+ #### An object with `.builtins`
2145
+
2146
+ ```js
2147
+ module.exports = {
2148
+ builtins: {
2149
+ 'http://example.org/custom#ok': ({ subst }) => [subst],
2150
+ },
2151
+ };
2152
+ ```
2153
+
2154
+ #### An object with `.default` as a plain object map
2155
+
2156
+ This is mainly an ESM/transpiler compatibility form.
2157
+
2158
+ ```js
2159
+ module.exports = {
2160
+ default: {
2161
+ 'http://example.org/custom#ok': ({ subst }) => [subst],
2162
+ },
2163
+ };
2164
+ ```
2165
+
2127
2166
  If none of those shapes match, Eyeling rejects the module with a descriptive error.
2128
2167
 
2129
2168
  ### 16.4 The handler contract
2130
2169
 
2131
- Builtin handlers are called with a context object like:
2170
+ Builtin handlers are called with an **exactly versioned** context object:
2132
2171
 
2133
2172
  - `iri` — the predicate IRI string
2134
2173
  - `goal` — the current triple goal
2135
2174
  - `subst` — the current substitution
2136
- - `facts`, `backRules`, `depth`, `varGen`, `maxResults`
2175
+ - `facts` the active fact store
2176
+ - `backRules` — the backward-rule set
2177
+ - `depth` — current proof depth
2178
+ - `varGen` — the variable generator state
2179
+ - `maxResults` — current result cap
2137
2180
  - `api` — the same registration/helper API used by modules
2138
2181
 
2139
- A handler returns **an array of substitutions**:
2182
+ The exact key set is part of the contract; adding, removing, or renaming a context field is a breaking change.
2183
+
2184
+ A handler returns **an array of substitution-delta objects**:
2140
2185
 
2141
2186
  - `[]` means failure / no solutions
2142
- - `[subst2]` means one successful continuation
2143
- - multiple substitutions mean a generator builtin
2187
+ - `[{}]` means success with no new bindings
2188
+ - `[{ ...delta }]` means one successful continuation with bindings
2189
+ - multiple objects mean a generator builtin
2190
+
2191
+ Returning a non-array, `null` elements, or non-object delta elements is rejected by the runtime contract wrapper.
2144
2192
 
2145
2193
  In practice:
2146
2194
 
@@ -2153,13 +2201,28 @@ Custom builtin failures are wrapped so the predicate IRI appears in the thrown e
2153
2201
 
2154
2202
  ### 16.5 The helper API exposed to builtin modules
2155
2203
 
2156
- Builtin modules do not need to import internal engine files directly. Eyeling passes a helper API into module registration, including:
2204
+ Builtin modules do not need to import internal engine files directly. Eyeling passes a helper API into module registration, and that helper surface is now treated as an **exact public contract**.
2205
+
2206
+ The current builtin API version is exposed as:
2207
+
2208
+ - `getBuiltinApiVersion()`
2209
+
2210
+ The stable helper function set is:
2211
+
2212
+ - `registerBuiltin`, `unregisterBuiltin`, `listBuiltinIris`
2213
+ - `internIri`, `internLiteral`, `literalParts`
2214
+ - `termToJsString`, `termToJsStringDecoded`, `termToN3`, `iriValue`
2215
+ - `unifyTerm`, `applySubstTerm`, `applySubstTriple`, `proveGoals`, `isGroundTerm`
2216
+ - `computeConclusionFromFormula`, `skolemIriFromGroundTerm`
2217
+ - `parseBooleanLiteralInfo`, `parseNumericLiteralInfo`, `parseXsdDecimalToBigIntScale`, `pow10n`
2218
+ - `normalizeLiteralForFastKey`, `literalsEquivalentAsXsdString`, `materializeRdfLists`
2219
+
2220
+ The stable namespace bags are:
2221
+
2222
+ - `terms`: `Literal`, `Iri`, `Var`, `Blank`, `ListTerm`, `OpenListTerm`, `GraphTerm`, `Triple`, `Rule`
2223
+ - `ns`: `RDF_NS`, `XSD_NS`, `CRYPTO_NS`, `MATH_NS`, `TIME_NS`, `LIST_NS`, `LOG_NS`, `STRING_NS`
2157
2224
 
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`
2225
+ The contract is intentionally strict: if a helper is added, removed, renamed, or its callable shape changes, the builtin contract tests fail until the change is made explicit and the version is bumped.
2163
2226
 
2164
2227
  That API keeps the extension boundary explicit: custom builtins get the operations they need without reaching into Eyeling’s private module graph.
2165
2228
 
package/eyeling.js CHANGED
@@ -9,6 +9,130 @@
9
9
  const __cache = Object.create(null);
10
10
 
11
11
  // ---- bundled modules ----
12
+ __modules["lib/builtin-contract.js"] = function(require, module, exports){
13
+ 'use strict';
14
+
15
+ const CONTRACT = {
16
+ version: 1,
17
+ moduleExportForms: [
18
+ 'function',
19
+ 'object.register',
20
+ 'object.builtins',
21
+ 'plain-object-map',
22
+ 'object.default-object-map',
23
+ ],
24
+ api: {
25
+ functions: {
26
+ getBuiltinApiVersion: { arity: 0, required: true },
27
+ registerBuiltin: { arity: 2, required: true },
28
+ unregisterBuiltin: { arity: 1, required: true },
29
+ listBuiltinIris: { arity: 0, required: true },
30
+ internIri: { arity: 1, required: true },
31
+ internLiteral: { arity: 1, required: true },
32
+ literalParts: { arity: 1, required: true },
33
+ termToJsString: { arity: 1, required: true },
34
+ termToJsStringDecoded: { arity: 1, required: true },
35
+ termToN3: { arity: 2, required: true },
36
+ iriValue: { arity: 1, required: true },
37
+ unifyTerm: { arity: 3, required: true },
38
+ applySubstTerm: { arity: 2, required: true },
39
+ applySubstTriple: { arity: 2, required: true },
40
+ proveGoals: { arity: 9, required: true },
41
+ isGroundTerm: { arity: 1, required: true },
42
+ computeConclusionFromFormula: { arity: 1, required: true },
43
+ skolemIriFromGroundTerm: { arity: 1, required: true },
44
+ parseBooleanLiteralInfo: { arity: 1, required: true },
45
+ parseNumericLiteralInfo: { arity: 1, required: true },
46
+ parseXsdDecimalToBigIntScale: { arity: 1, required: true },
47
+ pow10n: { arity: 1, required: true },
48
+ normalizeLiteralForFastKey: { arity: 1, required: true },
49
+ literalsEquivalentAsXsdString: { arity: 2, required: true },
50
+ materializeRdfLists: { arity: 3, required: true },
51
+ },
52
+ namespaces: {
53
+ terms: ['Literal', 'Iri', 'Var', 'Blank', 'ListTerm', 'OpenListTerm', 'GraphTerm', 'Triple', 'Rule'],
54
+ ns: ['RDF_NS', 'XSD_NS', 'CRYPTO_NS', 'MATH_NS', 'TIME_NS', 'LIST_NS', 'LOG_NS', 'STRING_NS'],
55
+ },
56
+ },
57
+ handler: {
58
+ ctxKeys: ['iri', 'goal', 'subst', 'facts', 'backRules', 'depth', 'varGen', 'maxResults', 'api'],
59
+ return: 'array-of-substitution-deltas',
60
+ },
61
+ };
62
+
63
+ const EXACT_API_KEYS = new Set([...Object.keys(CONTRACT.api.functions), ...Object.keys(CONTRACT.api.namespaces)]);
64
+
65
+ function assertExactKeys(obj, expectedKeys, label) {
66
+ const got = Object.keys(obj).sort();
67
+ const exp = Array.from(expectedKeys).sort();
68
+ if (got.length !== exp.length || got.some((k, i) => k !== exp[i])) {
69
+ throw new Error(`${label} keys changed. expected: ${exp.join(', ')}; got: ${got.join(', ')}`);
70
+ }
71
+ }
72
+
73
+ function assertFunctionArity(name, fn, spec) {
74
+ if (typeof fn !== 'function') throw new TypeError(`Builtin API member ${name} must be a function`);
75
+ if (Number.isInteger(spec.arity) && fn.length !== spec.arity) {
76
+ throw new Error(`Builtin API member ${name} arity changed: expected ${spec.arity}, got ${fn.length}`);
77
+ }
78
+ if (Number.isInteger(spec.arityMin) && fn.length < spec.arityMin) {
79
+ throw new Error(`Builtin API member ${name} arity too small: expected >= ${spec.arityMin}, got ${fn.length}`);
80
+ }
81
+ }
82
+
83
+ function deepFreezeBuiltinApi(api) {
84
+ Object.freeze(api.terms);
85
+ Object.freeze(api.ns);
86
+ return Object.freeze(api);
87
+ }
88
+
89
+ function assertBuiltinApiShape(api) {
90
+ assertExactKeys(api, EXACT_API_KEYS, 'Builtin registration API');
91
+
92
+ for (const [name, spec] of Object.entries(CONTRACT.api.functions)) {
93
+ assertFunctionArity(name, api[name], spec);
94
+ }
95
+
96
+ for (const name of CONTRACT.api.namespaces.terms) {
97
+ if (!api.terms || typeof api.terms[name] !== 'function') {
98
+ throw new TypeError(`Builtin API terms.${name} missing or invalid`);
99
+ }
100
+ }
101
+
102
+ for (const name of CONTRACT.api.namespaces.ns) {
103
+ if (!api.ns || typeof api.ns[name] !== 'string') {
104
+ throw new TypeError(`Builtin API ns.${name} missing or invalid`);
105
+ }
106
+ }
107
+
108
+ return api;
109
+ }
110
+
111
+ function assertBuiltinCtxShape(ctx) {
112
+ assertExactKeys(ctx, new Set(CONTRACT.handler.ctxKeys), 'Builtin handler ctx');
113
+ }
114
+
115
+ function assertBuiltinResultShape(out, iri) {
116
+ if (out == null) return;
117
+ if (!Array.isArray(out)) {
118
+ throw new TypeError(`Custom builtin ${iri} must return an array of substitution deltas`);
119
+ }
120
+ for (const delta of out) {
121
+ if (delta === null || typeof delta !== 'object' || Array.isArray(delta)) {
122
+ throw new TypeError(`Custom builtin ${iri} returned a non-object substitution delta`);
123
+ }
124
+ }
125
+ }
126
+
127
+ module.exports = {
128
+ CONTRACT,
129
+ assertBuiltinApiShape,
130
+ assertBuiltinCtxShape,
131
+ assertBuiltinResultShape,
132
+ deepFreezeBuiltinApi,
133
+ };
134
+
135
+ };
12
136
  __modules["lib/builtin-sudoku.js"] = function(require, module, exports){
13
137
  'use strict';
14
138
 
@@ -515,6 +639,13 @@ const { termToN3 } = require('./printing');
515
639
  const trace = require('./trace');
516
640
  const time = require('./time');
517
641
  const deref = require('./deref');
642
+ const {
643
+ CONTRACT: BUILTIN_CONTRACT,
644
+ assertBuiltinApiShape,
645
+ assertBuiltinCtxShape,
646
+ assertBuiltinResultShape,
647
+ deepFreezeBuiltinApi,
648
+ } = require('./builtin-contract');
518
649
 
519
650
  let nodeCrypto = null;
520
651
  try {
@@ -558,8 +689,21 @@ function registerBuiltin(iri, handler) {
558
689
  if (typeof handler !== 'function') {
559
690
  throw new TypeError(`Custom builtin ${iri} must be registered with a function handler`);
560
691
  }
561
- __customBuiltinHandlers.set(iri, handler);
562
- return handler;
692
+
693
+ const wrapped = function builtinContractWrapper(ctx) {
694
+ assertBuiltinCtxShape(ctx);
695
+ const out = handler(ctx);
696
+ assertBuiltinResultShape(out, iri);
697
+ return out;
698
+ };
699
+
700
+ Object.defineProperty(wrapped, '__builtinContractWrapped', {
701
+ value: true,
702
+ enumerable: false,
703
+ });
704
+
705
+ __customBuiltinHandlers.set(iri, wrapped);
706
+ return wrapped;
563
707
  }
564
708
 
565
709
  function unregisterBuiltin(iri) {
@@ -570,8 +714,17 @@ function listBuiltinIris() {
570
714
  return Array.from(__customBuiltinHandlers.keys()).sort();
571
715
  }
572
716
 
717
+ let __builtinApiSingleton = null;
718
+
719
+ function getBuiltinApiVersion() {
720
+ return BUILTIN_CONTRACT.version;
721
+ }
722
+
573
723
  function __buildBuiltinRegistrationApi() {
574
- return {
724
+ if (__builtinApiSingleton) return __builtinApiSingleton;
725
+
726
+ const api = {
727
+ getBuiltinApiVersion,
575
728
  registerBuiltin,
576
729
  unregisterBuiltin,
577
730
  listBuiltinIris,
@@ -599,6 +752,9 @@ function __buildBuiltinRegistrationApi() {
599
752
  terms: { Literal, Iri, Var, Blank, ListTerm, OpenListTerm, GraphTerm, Triple, Rule },
600
753
  ns: { RDF_NS, XSD_NS, CRYPTO_NS, MATH_NS, TIME_NS, LIST_NS, LOG_NS, STRING_NS },
601
754
  };
755
+
756
+ __builtinApiSingleton = deepFreezeBuiltinApi(assertBuiltinApiShape(api));
757
+ return __builtinApiSingleton;
602
758
  }
603
759
 
604
760
  function registerBuiltinModule(mod, origin = '<builtin-module>') {
@@ -675,9 +831,7 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
675
831
  try {
676
832
  const out = handler(ctx);
677
833
  if (out == null) return [];
678
- if (!Array.isArray(out)) {
679
- throw new TypeError(`Custom builtin ${pv} must return an array of substitution deltas`);
680
- }
834
+ assertBuiltinResultShape(out, pv);
681
835
  return out;
682
836
  } catch (err) {
683
837
  if (err && typeof err === 'object' && typeof err.message === 'string') {
@@ -4472,6 +4626,8 @@ module.exports = {
4472
4626
  registerBuiltinModule,
4473
4627
  loadBuiltinModule,
4474
4628
  listBuiltinIris,
4629
+ getBuiltinApiVersion,
4630
+ __testBuildBuiltinApi: __buildBuiltinRegistrationApi,
4475
4631
  // shared helpers used by engine/explain
4476
4632
  parseBooleanLiteralInfo,
4477
4633
  parseNumericLiteralInfo,
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ const CONTRACT = {
4
+ version: 1,
5
+ moduleExportForms: [
6
+ 'function',
7
+ 'object.register',
8
+ 'object.builtins',
9
+ 'plain-object-map',
10
+ 'object.default-object-map',
11
+ ],
12
+ api: {
13
+ functions: {
14
+ getBuiltinApiVersion: { arity: 0, required: true },
15
+ registerBuiltin: { arity: 2, required: true },
16
+ unregisterBuiltin: { arity: 1, required: true },
17
+ listBuiltinIris: { arity: 0, required: true },
18
+ internIri: { arity: 1, required: true },
19
+ internLiteral: { arity: 1, required: true },
20
+ literalParts: { arity: 1, required: true },
21
+ termToJsString: { arity: 1, required: true },
22
+ termToJsStringDecoded: { arity: 1, required: true },
23
+ termToN3: { arity: 2, required: true },
24
+ iriValue: { arity: 1, required: true },
25
+ unifyTerm: { arity: 3, required: true },
26
+ applySubstTerm: { arity: 2, required: true },
27
+ applySubstTriple: { arity: 2, required: true },
28
+ proveGoals: { arity: 9, required: true },
29
+ isGroundTerm: { arity: 1, required: true },
30
+ computeConclusionFromFormula: { arity: 1, required: true },
31
+ skolemIriFromGroundTerm: { arity: 1, required: true },
32
+ parseBooleanLiteralInfo: { arity: 1, required: true },
33
+ parseNumericLiteralInfo: { arity: 1, required: true },
34
+ parseXsdDecimalToBigIntScale: { arity: 1, required: true },
35
+ pow10n: { arity: 1, required: true },
36
+ normalizeLiteralForFastKey: { arity: 1, required: true },
37
+ literalsEquivalentAsXsdString: { arity: 2, required: true },
38
+ materializeRdfLists: { arity: 3, required: true },
39
+ },
40
+ namespaces: {
41
+ terms: ['Literal', 'Iri', 'Var', 'Blank', 'ListTerm', 'OpenListTerm', 'GraphTerm', 'Triple', 'Rule'],
42
+ ns: ['RDF_NS', 'XSD_NS', 'CRYPTO_NS', 'MATH_NS', 'TIME_NS', 'LIST_NS', 'LOG_NS', 'STRING_NS'],
43
+ },
44
+ },
45
+ handler: {
46
+ ctxKeys: ['iri', 'goal', 'subst', 'facts', 'backRules', 'depth', 'varGen', 'maxResults', 'api'],
47
+ return: 'array-of-substitution-deltas',
48
+ },
49
+ };
50
+
51
+ const EXACT_API_KEYS = new Set([...Object.keys(CONTRACT.api.functions), ...Object.keys(CONTRACT.api.namespaces)]);
52
+
53
+ function assertExactKeys(obj, expectedKeys, label) {
54
+ const got = Object.keys(obj).sort();
55
+ const exp = Array.from(expectedKeys).sort();
56
+ if (got.length !== exp.length || got.some((k, i) => k !== exp[i])) {
57
+ throw new Error(`${label} keys changed. expected: ${exp.join(', ')}; got: ${got.join(', ')}`);
58
+ }
59
+ }
60
+
61
+ function assertFunctionArity(name, fn, spec) {
62
+ if (typeof fn !== 'function') throw new TypeError(`Builtin API member ${name} must be a function`);
63
+ if (Number.isInteger(spec.arity) && fn.length !== spec.arity) {
64
+ throw new Error(`Builtin API member ${name} arity changed: expected ${spec.arity}, got ${fn.length}`);
65
+ }
66
+ if (Number.isInteger(spec.arityMin) && fn.length < spec.arityMin) {
67
+ throw new Error(`Builtin API member ${name} arity too small: expected >= ${spec.arityMin}, got ${fn.length}`);
68
+ }
69
+ }
70
+
71
+ function deepFreezeBuiltinApi(api) {
72
+ Object.freeze(api.terms);
73
+ Object.freeze(api.ns);
74
+ return Object.freeze(api);
75
+ }
76
+
77
+ function assertBuiltinApiShape(api) {
78
+ assertExactKeys(api, EXACT_API_KEYS, 'Builtin registration API');
79
+
80
+ for (const [name, spec] of Object.entries(CONTRACT.api.functions)) {
81
+ assertFunctionArity(name, api[name], spec);
82
+ }
83
+
84
+ for (const name of CONTRACT.api.namespaces.terms) {
85
+ if (!api.terms || typeof api.terms[name] !== 'function') {
86
+ throw new TypeError(`Builtin API terms.${name} missing or invalid`);
87
+ }
88
+ }
89
+
90
+ for (const name of CONTRACT.api.namespaces.ns) {
91
+ if (!api.ns || typeof api.ns[name] !== 'string') {
92
+ throw new TypeError(`Builtin API ns.${name} missing or invalid`);
93
+ }
94
+ }
95
+
96
+ return api;
97
+ }
98
+
99
+ function assertBuiltinCtxShape(ctx) {
100
+ assertExactKeys(ctx, new Set(CONTRACT.handler.ctxKeys), 'Builtin handler ctx');
101
+ }
102
+
103
+ function assertBuiltinResultShape(out, iri) {
104
+ if (out == null) return;
105
+ if (!Array.isArray(out)) {
106
+ throw new TypeError(`Custom builtin ${iri} must return an array of substitution deltas`);
107
+ }
108
+ for (const delta of out) {
109
+ if (delta === null || typeof delta !== 'object' || Array.isArray(delta)) {
110
+ throw new TypeError(`Custom builtin ${iri} returned a non-object substitution delta`);
111
+ }
112
+ }
113
+ }
114
+
115
+ module.exports = {
116
+ CONTRACT,
117
+ assertBuiltinApiShape,
118
+ assertBuiltinCtxShape,
119
+ assertBuiltinResultShape,
120
+ deepFreezeBuiltinApi,
121
+ };
package/lib/builtins.js CHANGED
@@ -35,6 +35,13 @@ const { termToN3 } = require('./printing');
35
35
  const trace = require('./trace');
36
36
  const time = require('./time');
37
37
  const deref = require('./deref');
38
+ const {
39
+ CONTRACT: BUILTIN_CONTRACT,
40
+ assertBuiltinApiShape,
41
+ assertBuiltinCtxShape,
42
+ assertBuiltinResultShape,
43
+ deepFreezeBuiltinApi,
44
+ } = require('./builtin-contract');
38
45
 
39
46
  let nodeCrypto = null;
40
47
  try {
@@ -78,8 +85,21 @@ function registerBuiltin(iri, handler) {
78
85
  if (typeof handler !== 'function') {
79
86
  throw new TypeError(`Custom builtin ${iri} must be registered with a function handler`);
80
87
  }
81
- __customBuiltinHandlers.set(iri, handler);
82
- return handler;
88
+
89
+ const wrapped = function builtinContractWrapper(ctx) {
90
+ assertBuiltinCtxShape(ctx);
91
+ const out = handler(ctx);
92
+ assertBuiltinResultShape(out, iri);
93
+ return out;
94
+ };
95
+
96
+ Object.defineProperty(wrapped, '__builtinContractWrapped', {
97
+ value: true,
98
+ enumerable: false,
99
+ });
100
+
101
+ __customBuiltinHandlers.set(iri, wrapped);
102
+ return wrapped;
83
103
  }
84
104
 
85
105
  function unregisterBuiltin(iri) {
@@ -90,8 +110,17 @@ function listBuiltinIris() {
90
110
  return Array.from(__customBuiltinHandlers.keys()).sort();
91
111
  }
92
112
 
113
+ let __builtinApiSingleton = null;
114
+
115
+ function getBuiltinApiVersion() {
116
+ return BUILTIN_CONTRACT.version;
117
+ }
118
+
93
119
  function __buildBuiltinRegistrationApi() {
94
- return {
120
+ if (__builtinApiSingleton) return __builtinApiSingleton;
121
+
122
+ const api = {
123
+ getBuiltinApiVersion,
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 = deepFreezeBuiltinApi(assertBuiltinApiShape(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
+ assertBuiltinResultShape(out, pv);
201
231
  return out;
202
232
  } catch (err) {
203
233
  if (err && typeof err === 'object' && typeof err.message === 'string') {
@@ -3992,6 +4022,8 @@ module.exports = {
3992
4022
  registerBuiltinModule,
3993
4023
  loadBuiltinModule,
3994
4024
  listBuiltinIris,
4025
+ getBuiltinApiVersion,
4026
+ __testBuildBuiltinApi: __buildBuiltinRegistrationApi,
3995
4027
  // shared helpers used by engine/explain
3996
4028
  parseBooleanLiteralInfo,
3997
4029
  parseNumericLiteralInfo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.19.1",
3
+ "version": "1.19.2",
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,147 @@
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
+ const { CONTRACT } = require('../lib/builtin-contract');
24
+ require('../lib/engine');
25
+
26
+ const expectedApiKeys = [...Object.keys(CONTRACT.api.functions), ...Object.keys(CONTRACT.api.namespaces)].sort();
27
+
28
+ const expectedFunctionArities = Object.fromEntries(
29
+ Object.entries(CONTRACT.api.functions).map(([name, spec]) => [name, spec]),
30
+ );
31
+
32
+ const cases = [
33
+ {
34
+ name: 'builtin API exact helper surface is stable',
35
+ run() {
36
+ const api = builtins.__testBuildBuiltinApi();
37
+ assert.deepEqual(Object.keys(api).sort(), expectedApiKeys);
38
+ assert.equal(Object.isFrozen(api), true);
39
+ assert.equal(Object.isFrozen(api.terms), true);
40
+ assert.equal(Object.isFrozen(api.ns), true);
41
+ for (const [name, spec] of Object.entries(expectedFunctionArities)) {
42
+ assert.equal(typeof api[name], 'function', `${name} must be a function`);
43
+ if (Number.isInteger(spec.arity)) assert.equal(api[name].length, spec.arity, `${name} arity drifted`);
44
+ if (Number.isInteger(spec.arityMin)) assert.ok(api[name].length >= spec.arityMin, `${name} arity drifted`);
45
+ }
46
+ assert.deepEqual(Object.keys(api.terms).sort(), CONTRACT.api.namespaces.terms.slice().sort());
47
+ assert.deepEqual(Object.keys(api.ns).sort(), CONTRACT.api.namespaces.ns.slice().sort());
48
+ assert.equal(api.getBuiltinApiVersion(), CONTRACT.version);
49
+ },
50
+ },
51
+ {
52
+ name: 'registerBuiltinModule accepts all declared module export forms',
53
+ run() {
54
+ assert.doesNotThrow(() => builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-map.js')), 'ok-map'));
55
+ assert.doesNotThrow(() =>
56
+ builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-register.js')), 'ok-register'),
57
+ );
58
+ assert.doesNotThrow(() =>
59
+ builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-builtins.js')), 'ok-builtins'),
60
+ );
61
+ assert.doesNotThrow(() =>
62
+ builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-default-map.js')), 'ok-default-map'),
63
+ );
64
+ },
65
+ },
66
+ {
67
+ name: 'registerBuiltinModule rejects unsupported module exports',
68
+ run() {
69
+ assert.throws(
70
+ () => builtins.registerBuiltinModule(require(path.join(fixtures, 'bad-export.js')), 'bad-export'),
71
+ /must export a function, a \{ register\(\) \} object, or an object mapping predicate IRIs to handlers/,
72
+ );
73
+ },
74
+ },
75
+ {
76
+ name: 'registered builtin handlers must return substitution-delta arrays',
77
+ run() {
78
+ builtins.registerBuiltinModule(require(path.join(fixtures, 'bad-return.js')), 'bad-return');
79
+ assert.throws(() => {
80
+ const h = builtins.registerBuiltin('http://example.org/test#shape-check', () => ({ nope: true }));
81
+ h({
82
+ iri: 'http://example.org/test#shape-check',
83
+ goal: {},
84
+ subst: {},
85
+ facts: [],
86
+ backRules: [],
87
+ depth: 0,
88
+ varGen: 0,
89
+ maxResults: 1,
90
+ api: builtins.__testBuildBuiltinApi(),
91
+ });
92
+ }, /must return an array of substitution deltas/);
93
+ },
94
+ },
95
+ {
96
+ name: 'registered builtin handlers receive the exact stable ctx shape',
97
+ run() {
98
+ const wrapped = builtins.registerBuiltin('http://example.org/test#ctx-shape', ({ subst }) => [subst]);
99
+ assert.throws(
100
+ () =>
101
+ wrapped({
102
+ iri: 'http://example.org/test#ctx-shape',
103
+ goal: {},
104
+ subst: {},
105
+ facts: [],
106
+ backRules: [],
107
+ depth: 0,
108
+ varGen: 0,
109
+ maxResults: 1,
110
+ api: builtins.__testBuildBuiltinApi(),
111
+ extra: true,
112
+ }),
113
+ /Builtin handler ctx keys changed|Builtin handler ctx shape changed/,
114
+ );
115
+ },
116
+ },
117
+ ];
118
+
119
+ let passed = 0;
120
+ let failed = 0;
121
+
122
+ (function main() {
123
+ const suiteStart = Date.now();
124
+ info(`Running ${cases.length} builtin contract tests`);
125
+
126
+ for (const tc of cases) {
127
+ const start = Date.now();
128
+ try {
129
+ tc.run();
130
+ ok(`${tc.name} ${C.dim}(${Date.now() - start} ms)${C.n}`);
131
+ passed++;
132
+ } catch (e) {
133
+ fail(`${tc.name} ${C.dim}(${Date.now() - start} ms)${C.n}`);
134
+ fail(e && e.stack ? e.stack : String(e));
135
+ failed++;
136
+ }
137
+ }
138
+
139
+ console.log('');
140
+ console.log(`${C.y}==${C.n} Total elapsed: ${Date.now() - suiteStart} ms`);
141
+ if (failed === 0) {
142
+ ok(`All builtin contract tests passed (${passed}/${cases.length})`);
143
+ process.exit(0);
144
+ }
145
+ fail(`Some builtin contract tests failed (${passed}/${cases.length})`);
146
+ process.exit(1);
147
+ })();
@@ -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
+ };