eyeling 1.29.2 → 1.29.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.
@@ -41,6 +41,8 @@ const {
41
41
  PrefixEnv,
42
42
  literalParts,
43
43
  copyQuotedGraphMetadata,
44
+ isInternalBlankVarName,
45
+ internalBlankVarSuffix,
44
46
  } = require('./prelude');
45
47
 
46
48
  const { decodeN3StringEscapes } = require('./lexer');
@@ -143,6 +145,127 @@ function __assertBuiltinHandlerResult(iri, out) {
143
145
  }
144
146
  }
145
147
 
148
+ function collectVarNamesInTerm(t, acc) {
149
+ if (t instanceof Var) {
150
+ acc.add(t.name);
151
+ return;
152
+ }
153
+ if (t instanceof ListTerm) {
154
+ for (const e of t.elems) collectVarNamesInTerm(e, acc);
155
+ return;
156
+ }
157
+ if (t instanceof OpenListTerm) {
158
+ for (const e of t.prefix) collectVarNamesInTerm(e, acc);
159
+ acc.add(t.tailVar);
160
+ return;
161
+ }
162
+ if (t instanceof GraphTerm) {
163
+ for (const tr of t.triples) collectVarNamesInTriple(tr, acc);
164
+ }
165
+ }
166
+
167
+ function collectVarNamesInTriple(tr, acc) {
168
+ collectVarNamesInTerm(tr.s, acc);
169
+ collectVarNamesInTerm(tr.p, acc);
170
+ collectVarNamesInTerm(tr.o, acc);
171
+ }
172
+
173
+ function cloneSubstWithMappedKeys(subst, keyMap, externalizeTerm) {
174
+ const out = Object.create(null);
175
+ for (const [key, val] of Object.entries(subst || {})) {
176
+ out[keyMap.get(key) || key] = externalizeTerm ? externalizeTerm(val) : val;
177
+ }
178
+ return out;
179
+ }
180
+
181
+ function hasInternalBlankVarInSubst(subst) {
182
+ for (const key of Object.keys(subst || {})) {
183
+ if (isInternalBlankVarName(key)) return true;
184
+ }
185
+ return false;
186
+ }
187
+
188
+ function makeCustomBuiltinPublicBridge(goal, subst) {
189
+ const used = new Set(Object.keys(subst || {}).filter((name) => !isInternalBlankVarName(name)));
190
+ collectVarNamesInTriple(goal, used);
191
+ for (const name of Array.from(used)) {
192
+ if (isInternalBlankVarName(name)) used.delete(name);
193
+ }
194
+
195
+ const internalNames = new Set();
196
+ collectVarNamesInTriple(goal, internalNames);
197
+ for (const key of Object.keys(subst || {})) internalNames.add(key);
198
+ for (const name of Array.from(internalNames)) {
199
+ if (!isInternalBlankVarName(name)) internalNames.delete(name);
200
+ }
201
+
202
+ if (internalNames.size === 0 && !hasInternalBlankVarInSubst(subst)) return null;
203
+
204
+ const internalToPublic = new Map();
205
+ const publicToInternal = new Map();
206
+
207
+ function allocatePublicName(internalName) {
208
+ const suffix = internalBlankVarSuffix(internalName) || String(internalToPublic.size + 1);
209
+ const base = /^[$A-Za-z_][0-9A-Za-z_]*$/u.test(`_b${suffix}`) ? `_b${suffix}` : `_b${internalToPublic.size + 1}`;
210
+ let name = base;
211
+ let n = 1;
212
+ while (used.has(name) || publicToInternal.has(name)) {
213
+ n += 1;
214
+ name = `${base}_${n}`;
215
+ }
216
+ used.add(name);
217
+ internalToPublic.set(internalName, name);
218
+ publicToInternal.set(name, internalName);
219
+ return name;
220
+ }
221
+
222
+ for (const name of internalNames) allocatePublicName(name);
223
+
224
+ function externalName(name) {
225
+ if (!isInternalBlankVarName(name)) return name;
226
+ return internalToPublic.get(name) || allocatePublicName(name);
227
+ }
228
+
229
+ function externalizeTerm(t) {
230
+ if (t instanceof Var) return new Var(externalName(t.name));
231
+ if (t instanceof ListTerm) return new ListTerm(t.elems.map(externalizeTerm));
232
+ if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(externalizeTerm), externalName(t.tailVar));
233
+ if (t instanceof GraphTerm) {
234
+ const triples = t.triples.map((tr) => new Triple(externalizeTerm(tr.s), externalizeTerm(tr.p), externalizeTerm(tr.o)));
235
+ return copyQuotedGraphMetadata(t, new GraphTerm(triples));
236
+ }
237
+ return t;
238
+ }
239
+
240
+ function internalizeTerm(t) {
241
+ if (t instanceof Var) return new Var(publicToInternal.get(t.name) || t.name);
242
+ if (t instanceof ListTerm) return new ListTerm(t.elems.map(internalizeTerm));
243
+ if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(internalizeTerm), publicToInternal.get(t.tailVar) || t.tailVar);
244
+ if (t instanceof GraphTerm) {
245
+ const triples = t.triples.map((tr) => new Triple(internalizeTerm(tr.s), internalizeTerm(tr.p), internalizeTerm(tr.o)));
246
+ return copyQuotedGraphMetadata(t, new GraphTerm(triples));
247
+ }
248
+ return t;
249
+ }
250
+
251
+ function internalizeDelta(delta) {
252
+ const out = Object.create(null);
253
+ for (const [key, val] of Object.entries(delta || {})) {
254
+ out[publicToInternal.get(key) || key] = internalizeTerm(val);
255
+ }
256
+ return out;
257
+ }
258
+
259
+ return {
260
+ goal: new Triple(externalizeTerm(goal.s), externalizeTerm(goal.p), externalizeTerm(goal.o)),
261
+ subst: cloneSubstWithMappedKeys(subst, internalToPublic, externalizeTerm),
262
+ internalizeResults(out) {
263
+ if (out == null) return out;
264
+ return out.map(internalizeDelta);
265
+ },
266
+ };
267
+ }
268
+
146
269
  function apiTermToN3(term, prefixes = PrefixEnv.newDefault()) {
147
270
  return termToN3(term, prefixes || PrefixEnv.newDefault());
148
271
  }
@@ -242,10 +365,11 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
242
365
  const handler = __customBuiltinHandlers.get(pv);
243
366
  if (typeof handler !== 'function') return null;
244
367
 
368
+ const bridge = makeCustomBuiltinPublicBridge(goal, subst);
245
369
  const ctx = {
246
370
  iri: pv,
247
- goal,
248
- subst,
371
+ goal: bridge ? bridge.goal : goal,
372
+ subst: bridge ? bridge.subst : subst,
249
373
  facts,
250
374
  backRules,
251
375
  depth,
@@ -255,7 +379,8 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
255
379
  };
256
380
 
257
381
  try {
258
- const out = handler(ctx);
382
+ const raw = handler(ctx);
383
+ const out = bridge ? bridge.internalizeResults(raw) : raw;
259
384
  if (out == null) return [];
260
385
  __assertBuiltinHandlerResult(pv, out);
261
386
  return out;
@@ -10995,7 +11120,7 @@ async function runStoreBacked(input, store, opts = {}) {
10995
11120
  }
10996
11121
 
10997
11122
  let queryTriples = [];
10998
- const queryDerived = [];
11123
+ let queryDerived = [];
10999
11124
  if (qrules.length) {
11000
11125
  for (const r of qrules) {
11001
11126
  const solutions = await __proveGoalsAgainstStore(r.premise || [], __emptySubst(), store, brules, triples, 0, varGen, {});
@@ -15468,6 +15593,21 @@ const DT_NS = 'https://eyereasoner.github.io/eyeling/datatype#';
15468
15593
  const SKOLEM_NS = 'https://eyereasoner.github.io/.well-known/genid/';
15469
15594
  const RDF_JSON_DT = RDF_NS + 'JSON';
15470
15595
 
15596
+ // Private rule-lifting variable prefix used for blank nodes that appear in
15597
+ // rule-body patterns. The prefix is intentionally not spellable in N3 input,
15598
+ // but public extension APIs should present these variables with stable,
15599
+ // ordinary names and translate them back internally.
15600
+ const INTERNAL_BLANK_VAR_PREFIX = '\uE000eyeling_b';
15601
+
15602
+ function isInternalBlankVarName(name) {
15603
+ return typeof name === 'string' && name.startsWith(INTERNAL_BLANK_VAR_PREFIX);
15604
+ }
15605
+
15606
+ function internalBlankVarSuffix(name) {
15607
+ if (!isInternalBlankVarName(name)) return '';
15608
+ return name.slice(INTERNAL_BLANK_VAR_PREFIX.length);
15609
+ }
15610
+
15471
15611
  function parseUriReferenceForResolution(uri) {
15472
15612
  // RFC 3986 Appendix B-style component parser, with the scheme tightened to
15473
15613
  // the RFC scheme grammar. Capturing delimiter presence matters: `?` with an
@@ -16171,6 +16311,9 @@ module.exports = {
16171
16311
  DT_NS,
16172
16312
  SKOLEM_NS,
16173
16313
  RDF_JSON_DT,
16314
+ INTERNAL_BLANK_VAR_PREFIX,
16315
+ isInternalBlankVarName,
16316
+ internalBlankVarSuffix,
16174
16317
  resolveIriRef,
16175
16318
  literalParts,
16176
16319
  normalizeLiteralForTid,
@@ -17645,6 +17788,7 @@ const {
17645
17788
  GraphTerm,
17646
17789
  Triple,
17647
17790
  copyQuotedGraphMetadata,
17791
+ INTERNAL_BLANK_VAR_PREFIX,
17648
17792
  } = require('./prelude');
17649
17793
 
17650
17794
  function liftBlankRuleVars(premise, conclusion) {
@@ -17660,10 +17804,8 @@ function liftBlankRuleVars(premise, conclusion) {
17660
17804
  // These variables are implementation details used to correlate one blank
17661
17805
  // node across the lifted rule body. Their names must not be spellable by
17662
17806
  // user input; otherwise a user variable such as ?_b1 can capture the
17663
- // lifted blank node and leak it into the rule head. U+E000 is a private-use
17664
- // code point and is not accepted by the lexer as part of a variable name.
17665
- const INTERNAL_BLANK_VAR_PREFIX = '\uE000eyeling_b';
17666
-
17807
+ // lifted blank node and leak it into the rule head. The public custom
17808
+ // builtin API maps these names back to ordinary-looking ?_bN variables.
17667
17809
  function blankToVar(label) {
17668
17810
  let name = mapping[label];
17669
17811
  if (name === undefined) {
package/eyeling.js CHANGED
@@ -41,6 +41,8 @@ const {
41
41
  PrefixEnv,
42
42
  literalParts,
43
43
  copyQuotedGraphMetadata,
44
+ isInternalBlankVarName,
45
+ internalBlankVarSuffix,
44
46
  } = require('./prelude');
45
47
 
46
48
  const { decodeN3StringEscapes } = require('./lexer');
@@ -143,6 +145,127 @@ function __assertBuiltinHandlerResult(iri, out) {
143
145
  }
144
146
  }
145
147
 
148
+ function collectVarNamesInTerm(t, acc) {
149
+ if (t instanceof Var) {
150
+ acc.add(t.name);
151
+ return;
152
+ }
153
+ if (t instanceof ListTerm) {
154
+ for (const e of t.elems) collectVarNamesInTerm(e, acc);
155
+ return;
156
+ }
157
+ if (t instanceof OpenListTerm) {
158
+ for (const e of t.prefix) collectVarNamesInTerm(e, acc);
159
+ acc.add(t.tailVar);
160
+ return;
161
+ }
162
+ if (t instanceof GraphTerm) {
163
+ for (const tr of t.triples) collectVarNamesInTriple(tr, acc);
164
+ }
165
+ }
166
+
167
+ function collectVarNamesInTriple(tr, acc) {
168
+ collectVarNamesInTerm(tr.s, acc);
169
+ collectVarNamesInTerm(tr.p, acc);
170
+ collectVarNamesInTerm(tr.o, acc);
171
+ }
172
+
173
+ function cloneSubstWithMappedKeys(subst, keyMap, externalizeTerm) {
174
+ const out = Object.create(null);
175
+ for (const [key, val] of Object.entries(subst || {})) {
176
+ out[keyMap.get(key) || key] = externalizeTerm ? externalizeTerm(val) : val;
177
+ }
178
+ return out;
179
+ }
180
+
181
+ function hasInternalBlankVarInSubst(subst) {
182
+ for (const key of Object.keys(subst || {})) {
183
+ if (isInternalBlankVarName(key)) return true;
184
+ }
185
+ return false;
186
+ }
187
+
188
+ function makeCustomBuiltinPublicBridge(goal, subst) {
189
+ const used = new Set(Object.keys(subst || {}).filter((name) => !isInternalBlankVarName(name)));
190
+ collectVarNamesInTriple(goal, used);
191
+ for (const name of Array.from(used)) {
192
+ if (isInternalBlankVarName(name)) used.delete(name);
193
+ }
194
+
195
+ const internalNames = new Set();
196
+ collectVarNamesInTriple(goal, internalNames);
197
+ for (const key of Object.keys(subst || {})) internalNames.add(key);
198
+ for (const name of Array.from(internalNames)) {
199
+ if (!isInternalBlankVarName(name)) internalNames.delete(name);
200
+ }
201
+
202
+ if (internalNames.size === 0 && !hasInternalBlankVarInSubst(subst)) return null;
203
+
204
+ const internalToPublic = new Map();
205
+ const publicToInternal = new Map();
206
+
207
+ function allocatePublicName(internalName) {
208
+ const suffix = internalBlankVarSuffix(internalName) || String(internalToPublic.size + 1);
209
+ const base = /^[$A-Za-z_][0-9A-Za-z_]*$/u.test(`_b${suffix}`) ? `_b${suffix}` : `_b${internalToPublic.size + 1}`;
210
+ let name = base;
211
+ let n = 1;
212
+ while (used.has(name) || publicToInternal.has(name)) {
213
+ n += 1;
214
+ name = `${base}_${n}`;
215
+ }
216
+ used.add(name);
217
+ internalToPublic.set(internalName, name);
218
+ publicToInternal.set(name, internalName);
219
+ return name;
220
+ }
221
+
222
+ for (const name of internalNames) allocatePublicName(name);
223
+
224
+ function externalName(name) {
225
+ if (!isInternalBlankVarName(name)) return name;
226
+ return internalToPublic.get(name) || allocatePublicName(name);
227
+ }
228
+
229
+ function externalizeTerm(t) {
230
+ if (t instanceof Var) return new Var(externalName(t.name));
231
+ if (t instanceof ListTerm) return new ListTerm(t.elems.map(externalizeTerm));
232
+ if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(externalizeTerm), externalName(t.tailVar));
233
+ if (t instanceof GraphTerm) {
234
+ const triples = t.triples.map((tr) => new Triple(externalizeTerm(tr.s), externalizeTerm(tr.p), externalizeTerm(tr.o)));
235
+ return copyQuotedGraphMetadata(t, new GraphTerm(triples));
236
+ }
237
+ return t;
238
+ }
239
+
240
+ function internalizeTerm(t) {
241
+ if (t instanceof Var) return new Var(publicToInternal.get(t.name) || t.name);
242
+ if (t instanceof ListTerm) return new ListTerm(t.elems.map(internalizeTerm));
243
+ if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(internalizeTerm), publicToInternal.get(t.tailVar) || t.tailVar);
244
+ if (t instanceof GraphTerm) {
245
+ const triples = t.triples.map((tr) => new Triple(internalizeTerm(tr.s), internalizeTerm(tr.p), internalizeTerm(tr.o)));
246
+ return copyQuotedGraphMetadata(t, new GraphTerm(triples));
247
+ }
248
+ return t;
249
+ }
250
+
251
+ function internalizeDelta(delta) {
252
+ const out = Object.create(null);
253
+ for (const [key, val] of Object.entries(delta || {})) {
254
+ out[publicToInternal.get(key) || key] = internalizeTerm(val);
255
+ }
256
+ return out;
257
+ }
258
+
259
+ return {
260
+ goal: new Triple(externalizeTerm(goal.s), externalizeTerm(goal.p), externalizeTerm(goal.o)),
261
+ subst: cloneSubstWithMappedKeys(subst, internalToPublic, externalizeTerm),
262
+ internalizeResults(out) {
263
+ if (out == null) return out;
264
+ return out.map(internalizeDelta);
265
+ },
266
+ };
267
+ }
268
+
146
269
  function apiTermToN3(term, prefixes = PrefixEnv.newDefault()) {
147
270
  return termToN3(term, prefixes || PrefixEnv.newDefault());
148
271
  }
@@ -242,10 +365,11 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
242
365
  const handler = __customBuiltinHandlers.get(pv);
243
366
  if (typeof handler !== 'function') return null;
244
367
 
368
+ const bridge = makeCustomBuiltinPublicBridge(goal, subst);
245
369
  const ctx = {
246
370
  iri: pv,
247
- goal,
248
- subst,
371
+ goal: bridge ? bridge.goal : goal,
372
+ subst: bridge ? bridge.subst : subst,
249
373
  facts,
250
374
  backRules,
251
375
  depth,
@@ -255,7 +379,8 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
255
379
  };
256
380
 
257
381
  try {
258
- const out = handler(ctx);
382
+ const raw = handler(ctx);
383
+ const out = bridge ? bridge.internalizeResults(raw) : raw;
259
384
  if (out == null) return [];
260
385
  __assertBuiltinHandlerResult(pv, out);
261
386
  return out;
@@ -10995,7 +11120,7 @@ async function runStoreBacked(input, store, opts = {}) {
10995
11120
  }
10996
11121
 
10997
11122
  let queryTriples = [];
10998
- const queryDerived = [];
11123
+ let queryDerived = [];
10999
11124
  if (qrules.length) {
11000
11125
  for (const r of qrules) {
11001
11126
  const solutions = await __proveGoalsAgainstStore(r.premise || [], __emptySubst(), store, brules, triples, 0, varGen, {});
@@ -15468,6 +15593,21 @@ const DT_NS = 'https://eyereasoner.github.io/eyeling/datatype#';
15468
15593
  const SKOLEM_NS = 'https://eyereasoner.github.io/.well-known/genid/';
15469
15594
  const RDF_JSON_DT = RDF_NS + 'JSON';
15470
15595
 
15596
+ // Private rule-lifting variable prefix used for blank nodes that appear in
15597
+ // rule-body patterns. The prefix is intentionally not spellable in N3 input,
15598
+ // but public extension APIs should present these variables with stable,
15599
+ // ordinary names and translate them back internally.
15600
+ const INTERNAL_BLANK_VAR_PREFIX = '\uE000eyeling_b';
15601
+
15602
+ function isInternalBlankVarName(name) {
15603
+ return typeof name === 'string' && name.startsWith(INTERNAL_BLANK_VAR_PREFIX);
15604
+ }
15605
+
15606
+ function internalBlankVarSuffix(name) {
15607
+ if (!isInternalBlankVarName(name)) return '';
15608
+ return name.slice(INTERNAL_BLANK_VAR_PREFIX.length);
15609
+ }
15610
+
15471
15611
  function parseUriReferenceForResolution(uri) {
15472
15612
  // RFC 3986 Appendix B-style component parser, with the scheme tightened to
15473
15613
  // the RFC scheme grammar. Capturing delimiter presence matters: `?` with an
@@ -16171,6 +16311,9 @@ module.exports = {
16171
16311
  DT_NS,
16172
16312
  SKOLEM_NS,
16173
16313
  RDF_JSON_DT,
16314
+ INTERNAL_BLANK_VAR_PREFIX,
16315
+ isInternalBlankVarName,
16316
+ internalBlankVarSuffix,
16174
16317
  resolveIriRef,
16175
16318
  literalParts,
16176
16319
  normalizeLiteralForTid,
@@ -17645,6 +17788,7 @@ const {
17645
17788
  GraphTerm,
17646
17789
  Triple,
17647
17790
  copyQuotedGraphMetadata,
17791
+ INTERNAL_BLANK_VAR_PREFIX,
17648
17792
  } = require('./prelude');
17649
17793
 
17650
17794
  function liftBlankRuleVars(premise, conclusion) {
@@ -17660,10 +17804,8 @@ function liftBlankRuleVars(premise, conclusion) {
17660
17804
  // These variables are implementation details used to correlate one blank
17661
17805
  // node across the lifted rule body. Their names must not be spellable by
17662
17806
  // user input; otherwise a user variable such as ?_b1 can capture the
17663
- // lifted blank node and leak it into the rule head. U+E000 is a private-use
17664
- // code point and is not accepted by the lexer as part of a variable name.
17665
- const INTERNAL_BLANK_VAR_PREFIX = '\uE000eyeling_b';
17666
-
17807
+ // lifted blank node and leak it into the rule head. The public custom
17808
+ // builtin API maps these names back to ordinary-looking ?_bN variables.
17667
17809
  function blankToVar(label) {
17668
17810
  let name = mapping[label];
17669
17811
  if (name === undefined) {
package/lib/builtins.js CHANGED
@@ -30,6 +30,8 @@ const {
30
30
  PrefixEnv,
31
31
  literalParts,
32
32
  copyQuotedGraphMetadata,
33
+ isInternalBlankVarName,
34
+ internalBlankVarSuffix,
33
35
  } = require('./prelude');
34
36
 
35
37
  const { decodeN3StringEscapes } = require('./lexer');
@@ -132,6 +134,127 @@ function __assertBuiltinHandlerResult(iri, out) {
132
134
  }
133
135
  }
134
136
 
137
+ function collectVarNamesInTerm(t, acc) {
138
+ if (t instanceof Var) {
139
+ acc.add(t.name);
140
+ return;
141
+ }
142
+ if (t instanceof ListTerm) {
143
+ for (const e of t.elems) collectVarNamesInTerm(e, acc);
144
+ return;
145
+ }
146
+ if (t instanceof OpenListTerm) {
147
+ for (const e of t.prefix) collectVarNamesInTerm(e, acc);
148
+ acc.add(t.tailVar);
149
+ return;
150
+ }
151
+ if (t instanceof GraphTerm) {
152
+ for (const tr of t.triples) collectVarNamesInTriple(tr, acc);
153
+ }
154
+ }
155
+
156
+ function collectVarNamesInTriple(tr, acc) {
157
+ collectVarNamesInTerm(tr.s, acc);
158
+ collectVarNamesInTerm(tr.p, acc);
159
+ collectVarNamesInTerm(tr.o, acc);
160
+ }
161
+
162
+ function cloneSubstWithMappedKeys(subst, keyMap, externalizeTerm) {
163
+ const out = Object.create(null);
164
+ for (const [key, val] of Object.entries(subst || {})) {
165
+ out[keyMap.get(key) || key] = externalizeTerm ? externalizeTerm(val) : val;
166
+ }
167
+ return out;
168
+ }
169
+
170
+ function hasInternalBlankVarInSubst(subst) {
171
+ for (const key of Object.keys(subst || {})) {
172
+ if (isInternalBlankVarName(key)) return true;
173
+ }
174
+ return false;
175
+ }
176
+
177
+ function makeCustomBuiltinPublicBridge(goal, subst) {
178
+ const used = new Set(Object.keys(subst || {}).filter((name) => !isInternalBlankVarName(name)));
179
+ collectVarNamesInTriple(goal, used);
180
+ for (const name of Array.from(used)) {
181
+ if (isInternalBlankVarName(name)) used.delete(name);
182
+ }
183
+
184
+ const internalNames = new Set();
185
+ collectVarNamesInTriple(goal, internalNames);
186
+ for (const key of Object.keys(subst || {})) internalNames.add(key);
187
+ for (const name of Array.from(internalNames)) {
188
+ if (!isInternalBlankVarName(name)) internalNames.delete(name);
189
+ }
190
+
191
+ if (internalNames.size === 0 && !hasInternalBlankVarInSubst(subst)) return null;
192
+
193
+ const internalToPublic = new Map();
194
+ const publicToInternal = new Map();
195
+
196
+ function allocatePublicName(internalName) {
197
+ const suffix = internalBlankVarSuffix(internalName) || String(internalToPublic.size + 1);
198
+ const base = /^[$A-Za-z_][0-9A-Za-z_]*$/u.test(`_b${suffix}`) ? `_b${suffix}` : `_b${internalToPublic.size + 1}`;
199
+ let name = base;
200
+ let n = 1;
201
+ while (used.has(name) || publicToInternal.has(name)) {
202
+ n += 1;
203
+ name = `${base}_${n}`;
204
+ }
205
+ used.add(name);
206
+ internalToPublic.set(internalName, name);
207
+ publicToInternal.set(name, internalName);
208
+ return name;
209
+ }
210
+
211
+ for (const name of internalNames) allocatePublicName(name);
212
+
213
+ function externalName(name) {
214
+ if (!isInternalBlankVarName(name)) return name;
215
+ return internalToPublic.get(name) || allocatePublicName(name);
216
+ }
217
+
218
+ function externalizeTerm(t) {
219
+ if (t instanceof Var) return new Var(externalName(t.name));
220
+ if (t instanceof ListTerm) return new ListTerm(t.elems.map(externalizeTerm));
221
+ if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(externalizeTerm), externalName(t.tailVar));
222
+ if (t instanceof GraphTerm) {
223
+ const triples = t.triples.map((tr) => new Triple(externalizeTerm(tr.s), externalizeTerm(tr.p), externalizeTerm(tr.o)));
224
+ return copyQuotedGraphMetadata(t, new GraphTerm(triples));
225
+ }
226
+ return t;
227
+ }
228
+
229
+ function internalizeTerm(t) {
230
+ if (t instanceof Var) return new Var(publicToInternal.get(t.name) || t.name);
231
+ if (t instanceof ListTerm) return new ListTerm(t.elems.map(internalizeTerm));
232
+ if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(internalizeTerm), publicToInternal.get(t.tailVar) || t.tailVar);
233
+ if (t instanceof GraphTerm) {
234
+ const triples = t.triples.map((tr) => new Triple(internalizeTerm(tr.s), internalizeTerm(tr.p), internalizeTerm(tr.o)));
235
+ return copyQuotedGraphMetadata(t, new GraphTerm(triples));
236
+ }
237
+ return t;
238
+ }
239
+
240
+ function internalizeDelta(delta) {
241
+ const out = Object.create(null);
242
+ for (const [key, val] of Object.entries(delta || {})) {
243
+ out[publicToInternal.get(key) || key] = internalizeTerm(val);
244
+ }
245
+ return out;
246
+ }
247
+
248
+ return {
249
+ goal: new Triple(externalizeTerm(goal.s), externalizeTerm(goal.p), externalizeTerm(goal.o)),
250
+ subst: cloneSubstWithMappedKeys(subst, internalToPublic, externalizeTerm),
251
+ internalizeResults(out) {
252
+ if (out == null) return out;
253
+ return out.map(internalizeDelta);
254
+ },
255
+ };
256
+ }
257
+
135
258
  function apiTermToN3(term, prefixes = PrefixEnv.newDefault()) {
136
259
  return termToN3(term, prefixes || PrefixEnv.newDefault());
137
260
  }
@@ -231,10 +354,11 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
231
354
  const handler = __customBuiltinHandlers.get(pv);
232
355
  if (typeof handler !== 'function') return null;
233
356
 
357
+ const bridge = makeCustomBuiltinPublicBridge(goal, subst);
234
358
  const ctx = {
235
359
  iri: pv,
236
- goal,
237
- subst,
360
+ goal: bridge ? bridge.goal : goal,
361
+ subst: bridge ? bridge.subst : subst,
238
362
  facts,
239
363
  backRules,
240
364
  depth,
@@ -244,7 +368,8 @@ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGe
244
368
  };
245
369
 
246
370
  try {
247
- const out = handler(ctx);
371
+ const raw = handler(ctx);
372
+ const out = bridge ? bridge.internalizeResults(raw) : raw;
248
373
  if (out == null) return [];
249
374
  __assertBuiltinHandlerResult(pv, out);
250
375
  return out;
package/lib/engine.js CHANGED
@@ -3930,7 +3930,7 @@ async function runStoreBacked(input, store, opts = {}) {
3930
3930
  }
3931
3931
 
3932
3932
  let queryTriples = [];
3933
- const queryDerived = [];
3933
+ let queryDerived = [];
3934
3934
  if (qrules.length) {
3935
3935
  for (const r of qrules) {
3936
3936
  const solutions = await __proveGoalsAgainstStore(r.premise || [], __emptySubst(), store, brules, triples, 0, varGen, {});
package/lib/prelude.js CHANGED
@@ -25,6 +25,21 @@ const DT_NS = 'https://eyereasoner.github.io/eyeling/datatype#';
25
25
  const SKOLEM_NS = 'https://eyereasoner.github.io/.well-known/genid/';
26
26
  const RDF_JSON_DT = RDF_NS + 'JSON';
27
27
 
28
+ // Private rule-lifting variable prefix used for blank nodes that appear in
29
+ // rule-body patterns. The prefix is intentionally not spellable in N3 input,
30
+ // but public extension APIs should present these variables with stable,
31
+ // ordinary names and translate them back internally.
32
+ const INTERNAL_BLANK_VAR_PREFIX = '\uE000eyeling_b';
33
+
34
+ function isInternalBlankVarName(name) {
35
+ return typeof name === 'string' && name.startsWith(INTERNAL_BLANK_VAR_PREFIX);
36
+ }
37
+
38
+ function internalBlankVarSuffix(name) {
39
+ if (!isInternalBlankVarName(name)) return '';
40
+ return name.slice(INTERNAL_BLANK_VAR_PREFIX.length);
41
+ }
42
+
28
43
  function parseUriReferenceForResolution(uri) {
29
44
  // RFC 3986 Appendix B-style component parser, with the scheme tightened to
30
45
  // the RFC scheme grammar. Capturing delimiter presence matters: `?` with an
@@ -728,6 +743,9 @@ module.exports = {
728
743
  DT_NS,
729
744
  SKOLEM_NS,
730
745
  RDF_JSON_DT,
746
+ INTERNAL_BLANK_VAR_PREFIX,
747
+ isInternalBlankVarName,
748
+ internalBlankVarSuffix,
731
749
  resolveIriRef,
732
750
  literalParts,
733
751
  normalizeLiteralForTid,
package/lib/rules.js CHANGED
@@ -17,6 +17,7 @@ const {
17
17
  GraphTerm,
18
18
  Triple,
19
19
  copyQuotedGraphMetadata,
20
+ INTERNAL_BLANK_VAR_PREFIX,
20
21
  } = require('./prelude');
21
22
 
22
23
  function liftBlankRuleVars(premise, conclusion) {
@@ -32,10 +33,8 @@ function liftBlankRuleVars(premise, conclusion) {
32
33
  // These variables are implementation details used to correlate one blank
33
34
  // node across the lifted rule body. Their names must not be spellable by
34
35
  // user input; otherwise a user variable such as ?_b1 can capture the
35
- // lifted blank node and leak it into the rule head. U+E000 is a private-use
36
- // code point and is not accepted by the lexer as part of a variable name.
37
- const INTERNAL_BLANK_VAR_PREFIX = '\uE000eyeling_b';
38
-
36
+ // lifted blank node and leak it into the rule head. The public custom
37
+ // builtin API maps these names back to ordinary-looking ?_bN variables.
39
38
  function blankToVar(label) {
40
39
  let name = mapping[label];
41
40
  if (name === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.29.2",
3
+ "version": "1.29.3",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -7,6 +7,7 @@ const { detail, failResult, info, pass } = require('./report');
7
7
  const builtins = require('../lib/builtins');
8
8
  require('../lib/engine');
9
9
  const { reason } = require('../index');
10
+ const { reasonStream } = require('../lib/engine');
10
11
 
11
12
  const expectedApiKeys = [
12
13
  'registerBuiltin',
@@ -218,6 +219,41 @@ const cases = [
218
219
  assert.match(out, /:canonical :midnightRollover "2027-01-01T00:00:00Z"\^\^xsd:dateTime \./);
219
220
  },
220
221
  },
222
+ {
223
+ name: 'custom builtin API hides internal blank-node variable prefix',
224
+ run() {
225
+ const iri = 'http://example.org/custom#format';
226
+ builtins.unregisterBuiltin(iri);
227
+ builtins.registerBuiltin(iri, ({ goal, subst, api }) => {
228
+ const formatted = api.termToN3(goal.s);
229
+ assert.doesNotMatch(formatted, /\uE000eyeling_b/);
230
+ assert.match(formatted, /\?_b1/);
231
+ const next = api.unifyTerm(goal.o, api.internLiteral(JSON.stringify(formatted)), subst);
232
+ return next === null ? [] : [next];
233
+ });
234
+
235
+ try {
236
+ const out = reasonStream(`
237
+ @prefix : <http://example.org/> .
238
+ @prefix cb: <http://example.org/custom#> .
239
+
240
+ {
241
+ { [] a ?class } cb:format ?format .
242
+ }
243
+ =>
244
+ {
245
+ :result :is ?format .
246
+ } .
247
+ `, { proof: false, includeInputFactsInClosure: false }).closureN3;
248
+ assert.doesNotMatch(out, /\uE000eyeling_b/);
249
+ assert.match(out, /:result :is/);
250
+ assert.match(out, /\?_b1/);
251
+ assert.match(out, /\?class/);
252
+ } finally {
253
+ builtins.unregisterBuiltin(iri);
254
+ }
255
+ },
256
+ },
221
257
 
222
258
  ];
223
259