eyeling 1.19.2 → 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
@@ -2087,20 +2087,18 @@ const out = reason({ builtinModules: ['./hello-builtin.js'] }, n3Text);
2087
2087
 
2088
2088
  ### 16.2.1 Stability rule for `--builtin`
2089
2089
 
2090
- Eyeling now treats the custom-builtin boundary as a **versioned API contract** rather than an informal convenience layer.
2090
+ Eyeling keeps `--builtin` simple.
2091
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.
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
2093
 
2094
- The stable rule is:
2094
+ In practice, this means:
2095
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
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`
2102
2100
 
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`.
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.
2104
2102
 
2105
2103
  ### 16.3 What a builtin module may export
2106
2104
 
@@ -2167,7 +2165,7 @@ If none of those shapes match, Eyeling rejects the module with a descriptive err
2167
2165
 
2168
2166
  ### 16.4 The handler contract
2169
2167
 
2170
- Builtin handlers are called with an **exactly versioned** context object:
2168
+ Builtin handlers are called with a context object containing:
2171
2169
 
2172
2170
  - `iri` — the predicate IRI string
2173
2171
  - `goal` — the current triple goal
@@ -2179,16 +2177,14 @@ Builtin handlers are called with an **exactly versioned** context object:
2179
2177
  - `maxResults` — current result cap
2180
2178
  - `api` — the same registration/helper API used by modules
2181
2179
 
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**:
2180
+ A handler should return an **array of substitution objects**:
2185
2181
 
2186
2182
  - `[]` means failure / no solutions
2187
2183
  - `[{}]` means success with no new bindings
2188
2184
  - `[{ ...delta }]` means one successful continuation with bindings
2189
2185
  - multiple objects mean a generator builtin
2190
2186
 
2191
- Returning a non-array, `null` elements, or non-object delta elements is rejected by the runtime contract wrapper.
2187
+ Returning something else is rejected at runtime.
2192
2188
 
2193
2189
  In practice:
2194
2190
 
@@ -2201,13 +2197,9 @@ Custom builtin failures are wrapped so the predicate IRI appears in the thrown e
2201
2197
 
2202
2198
  ### 16.5 The helper API exposed to builtin modules
2203
2199
 
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()`
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.
2209
2201
 
2210
- The stable helper function set is:
2202
+ The current helper function set is:
2211
2203
 
2212
2204
  - `registerBuiltin`, `unregisterBuiltin`, `listBuiltinIris`
2213
2205
  - `internIri`, `internLiteral`, `literalParts`
@@ -2222,7 +2214,7 @@ The stable namespace bags are:
2222
2214
  - `terms`: `Literal`, `Iri`, `Var`, `Blank`, `ListTerm`, `OpenListTerm`, `GraphTerm`, `Triple`, `Rule`
2223
2215
  - `ns`: `RDF_NS`, `XSD_NS`, `CRYPTO_NS`, `MATH_NS`, `TIME_NS`, `LIST_NS`, `LOG_NS`, `STRING_NS`
2224
2216
 
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.
2217
+ The helper object is frozen and regression-tested so helper additions, removals, and renames do not slip in silently.
2226
2218
 
2227
2219
  That API keeps the extension boundary explicit: custom builtins get the operations they need without reaching into Eyeling’s private module graph.
2228
2220
 
package/eyeling.js CHANGED
@@ -9,130 +9,6 @@
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
- };
136
12
  __modules["lib/builtin-sudoku.js"] = function(require, module, exports){
137
13
  'use strict';
138
14
 
@@ -639,13 +515,6 @@ const { termToN3 } = require('./printing');
639
515
  const trace = require('./trace');
640
516
  const time = require('./time');
641
517
  const deref = require('./deref');
642
- const {
643
- CONTRACT: BUILTIN_CONTRACT,
644
- assertBuiltinApiShape,
645
- assertBuiltinCtxShape,
646
- assertBuiltinResultShape,
647
- deepFreezeBuiltinApi,
648
- } = require('./builtin-contract');
649
518
 
650
519
  let nodeCrypto = null;
651
520
  try {
@@ -690,18 +559,12 @@ function registerBuiltin(iri, handler) {
690
559
  throw new TypeError(`Custom builtin ${iri} must be registered with a function handler`);
691
560
  }
692
561
 
693
- const wrapped = function builtinContractWrapper(ctx) {
694
- assertBuiltinCtxShape(ctx);
562
+ const wrapped = function builtinResultWrapper(ctx) {
695
563
  const out = handler(ctx);
696
- assertBuiltinResultShape(out, iri);
564
+ __assertBuiltinHandlerResult(iri, out);
697
565
  return out;
698
566
  };
699
567
 
700
- Object.defineProperty(wrapped, '__builtinContractWrapped', {
701
- value: true,
702
- enumerable: false,
703
- });
704
-
705
568
  __customBuiltinHandlers.set(iri, wrapped);
706
569
  return wrapped;
707
570
  }
@@ -716,15 +579,28 @@ function listBuiltinIris() {
716
579
 
717
580
  let __builtinApiSingleton = null;
718
581
 
719
- function getBuiltinApiVersion() {
720
- return BUILTIN_CONTRACT.version;
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
+ }
721
598
  }
722
599
 
723
600
  function __buildBuiltinRegistrationApi() {
724
601
  if (__builtinApiSingleton) return __builtinApiSingleton;
725
602
 
726
603
  const api = {
727
- getBuiltinApiVersion,
728
604
  registerBuiltin,
729
605
  unregisterBuiltin,
730
606
  listBuiltinIris,
@@ -753,7 +629,7 @@ function __buildBuiltinRegistrationApi() {
753
629
  ns: { RDF_NS, XSD_NS, CRYPTO_NS, MATH_NS, TIME_NS, LIST_NS, LOG_NS, STRING_NS },
754
630
  };
755
631
 
756
- __builtinApiSingleton = deepFreezeBuiltinApi(assertBuiltinApiShape(api));
632
+ __builtinApiSingleton = __freezeBuiltinApi(api);
757
633
  return __builtinApiSingleton;
758
634
  }
759
635
 
@@ -831,7 +707,7 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
831
707
  try {
832
708
  const out = handler(ctx);
833
709
  if (out == null) return [];
834
- assertBuiltinResultShape(out, pv);
710
+ __assertBuiltinHandlerResult(pv, out);
835
711
  return out;
836
712
  } catch (err) {
837
713
  if (err && typeof err === 'object' && typeof err.message === 'string') {
@@ -4626,7 +4502,6 @@ module.exports = {
4626
4502
  registerBuiltinModule,
4627
4503
  loadBuiltinModule,
4628
4504
  listBuiltinIris,
4629
- getBuiltinApiVersion,
4630
4505
  __testBuildBuiltinApi: __buildBuiltinRegistrationApi,
4631
4506
  // shared helpers used by engine/explain
4632
4507
  parseBooleanLiteralInfo,
package/lib/builtins.js CHANGED
@@ -35,13 +35,6 @@ 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');
45
38
 
46
39
  let nodeCrypto = null;
47
40
  try {
@@ -86,18 +79,12 @@ function registerBuiltin(iri, handler) {
86
79
  throw new TypeError(`Custom builtin ${iri} must be registered with a function handler`);
87
80
  }
88
81
 
89
- const wrapped = function builtinContractWrapper(ctx) {
90
- assertBuiltinCtxShape(ctx);
82
+ const wrapped = function builtinResultWrapper(ctx) {
91
83
  const out = handler(ctx);
92
- assertBuiltinResultShape(out, iri);
84
+ __assertBuiltinHandlerResult(iri, out);
93
85
  return out;
94
86
  };
95
87
 
96
- Object.defineProperty(wrapped, '__builtinContractWrapped', {
97
- value: true,
98
- enumerable: false,
99
- });
100
-
101
88
  __customBuiltinHandlers.set(iri, wrapped);
102
89
  return wrapped;
103
90
  }
@@ -112,15 +99,28 @@ function listBuiltinIris() {
112
99
 
113
100
  let __builtinApiSingleton = null;
114
101
 
115
- function getBuiltinApiVersion() {
116
- return BUILTIN_CONTRACT.version;
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
+ }
117
118
  }
118
119
 
119
120
  function __buildBuiltinRegistrationApi() {
120
121
  if (__builtinApiSingleton) return __builtinApiSingleton;
121
122
 
122
123
  const api = {
123
- getBuiltinApiVersion,
124
124
  registerBuiltin,
125
125
  unregisterBuiltin,
126
126
  listBuiltinIris,
@@ -149,7 +149,7 @@ function __buildBuiltinRegistrationApi() {
149
149
  ns: { RDF_NS, XSD_NS, CRYPTO_NS, MATH_NS, TIME_NS, LIST_NS, LOG_NS, STRING_NS },
150
150
  };
151
151
 
152
- __builtinApiSingleton = deepFreezeBuiltinApi(assertBuiltinApiShape(api));
152
+ __builtinApiSingleton = __freezeBuiltinApi(api);
153
153
  return __builtinApiSingleton;
154
154
  }
155
155
 
@@ -227,7 +227,7 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
227
227
  try {
228
228
  const out = handler(ctx);
229
229
  if (out == null) return [];
230
- assertBuiltinResultShape(out, pv);
230
+ __assertBuiltinHandlerResult(pv, out);
231
231
  return out;
232
232
  } catch (err) {
233
233
  if (err && typeof err === 'object' && typeof err.message === 'string') {
@@ -4022,7 +4022,6 @@ module.exports = {
4022
4022
  registerBuiltinModule,
4023
4023
  loadBuiltinModule,
4024
4024
  listBuiltinIris,
4025
- getBuiltinApiVersion,
4026
4025
  __testBuildBuiltinApi: __buildBuiltinRegistrationApi,
4027
4026
  // shared helpers used by engine/explain
4028
4027
  parseBooleanLiteralInfo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.19.2",
3
+ "version": "1.19.3",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -20,36 +20,65 @@ function fail(msg) {
20
20
 
21
21
  const fixtures = path.join(__dirname, 'fixtures', 'builtins');
22
22
  const builtins = require('../lib/builtins');
23
- const { CONTRACT } = require('../lib/builtin-contract');
24
23
  require('../lib/engine');
25
24
 
26
- const expectedApiKeys = [...Object.keys(CONTRACT.api.functions), ...Object.keys(CONTRACT.api.namespaces)].sort();
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();
27
53
 
28
- const expectedFunctionArities = Object.fromEntries(
29
- Object.entries(CONTRACT.api.functions).map(([name, spec]) => [name, spec]),
30
- );
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();
31
66
 
32
67
  const cases = [
33
68
  {
34
- name: 'builtin API exact helper surface is stable',
69
+ name: 'builtin helper API stays stable and frozen',
35
70
  run() {
36
71
  const api = builtins.__testBuildBuiltinApi();
37
72
  assert.deepEqual(Object.keys(api).sort(), expectedApiKeys);
38
73
  assert.equal(Object.isFrozen(api), true);
39
74
  assert.equal(Object.isFrozen(api.terms), true);
40
75
  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);
76
+ assert.deepEqual(Object.keys(api.terms).sort(), expectedTermsKeys);
77
+ assert.deepEqual(Object.keys(api.ns).sort(), expectedNsKeys);
49
78
  },
50
79
  },
51
80
  {
52
- name: 'registerBuiltinModule accepts all declared module export forms',
81
+ name: 'registerBuiltinModule accepts supported module export forms',
53
82
  run() {
54
83
  assert.doesNotThrow(() => builtins.registerBuiltinModule(require(path.join(fixtures, 'ok-map.js')), 'ok-map'));
55
84
  assert.doesNotThrow(() =>
@@ -73,33 +102,13 @@ const cases = [
73
102
  },
74
103
  },
75
104
  {
76
- name: 'registered builtin handlers must return substitution-delta arrays',
105
+ name: 'registered builtin handlers must return substitution arrays',
77
106
  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]);
107
+ const wrapped = builtins.registerBuiltin('http://example.org/test#shape-check', () => ({ nope: true }));
99
108
  assert.throws(
100
109
  () =>
101
110
  wrapped({
102
- iri: 'http://example.org/test#ctx-shape',
111
+ iri: 'http://example.org/test#shape-check',
103
112
  goal: {},
104
113
  subst: {},
105
114
  facts: [],
@@ -108,9 +117,8 @@ const cases = [
108
117
  varGen: 0,
109
118
  maxResults: 1,
110
119
  api: builtins.__testBuildBuiltinApi(),
111
- extra: true,
112
120
  }),
113
- /Builtin handler ctx keys changed|Builtin handler ctx shape changed/,
121
+ /must return an array of substitution deltas/,
114
122
  );
115
123
  },
116
124
  },
@@ -1,121 +0,0 @@
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
- };