eyeling 1.23.1 → 1.23.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/lib/cli.js CHANGED
@@ -208,6 +208,8 @@ function main() {
208
208
  parseN3Text(text, {
209
209
  baseIri: __sourceLabelToBaseIri(sourceLabel),
210
210
  label: sourceLabel,
211
+ collectUsedPrefixes: true,
212
+ keepSourceArtifacts: false,
211
213
  }),
212
214
  );
213
215
  } catch (e) {
@@ -225,8 +227,6 @@ function main() {
225
227
  const frules = mergedDocument.frules;
226
228
  const brules = mergedDocument.brules;
227
229
  const qrules = mergedDocument.logQueryRules;
228
- const tokenSets = parsedSources.map((source) => ({ tokens: source.tokens, prefixes: source.prefixes }));
229
-
230
230
  if (showAst) {
231
231
  function astReplacer(unusedJsonKey, value) {
232
232
  if (value instanceof Set) return Array.from(value);
@@ -282,75 +282,6 @@ function main() {
282
282
  // In --stream mode we print prefixes *before* any derivations happen.
283
283
  // To keep the header small and stable, emit only prefixes that are actually
284
284
  // used (as QNames) in the *input* N3 program.
285
- function prefixesUsedInInputTokens(toks2, prefEnv) {
286
- const used = new Set();
287
-
288
- function maybeAddFromQName(name) {
289
- if (typeof name !== 'string') return;
290
- if (!name.includes(':')) return;
291
- if (name.startsWith('_:')) return; // blank node
292
-
293
- // Split only on the first ':'
294
- const idx = name.indexOf(':');
295
- const p = name.slice(0, idx); // may be '' for ":foo"
296
-
297
- // Ignore things like "http://..." unless that prefix is actually defined.
298
- if (!Object.prototype.hasOwnProperty.call(prefEnv.map, p)) return;
299
-
300
- used.add(p);
301
- }
302
-
303
- for (let i = 0; i < toks2.length; i++) {
304
- const t = toks2[i];
305
-
306
- // Skip @prefix ... .
307
- if (t.typ === 'AtPrefix') {
308
- while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
309
- continue;
310
- }
311
- // Skip @base ... .
312
- if (t.typ === 'AtBase') {
313
- while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
314
- continue;
315
- }
316
-
317
- // Skip SPARQL/Turtle PREFIX pfx: <iri>
318
- if (
319
- t.typ === 'Ident' &&
320
- typeof t.value === 'string' &&
321
- t.value.toLowerCase() === 'prefix' &&
322
- toks2[i + 1] &&
323
- toks2[i + 1].typ === 'Ident' &&
324
- typeof toks2[i + 1].value === 'string' &&
325
- toks2[i + 1].value.endsWith(':') &&
326
- toks2[i + 2] &&
327
- (toks2[i + 2].typ === 'IriRef' || toks2[i + 2].typ === 'Ident')
328
- ) {
329
- i += 2;
330
- continue;
331
- }
332
-
333
- // Skip SPARQL BASE <iri>
334
- if (
335
- t.typ === 'Ident' &&
336
- typeof t.value === 'string' &&
337
- t.value.toLowerCase() === 'base' &&
338
- toks2[i + 1] &&
339
- toks2[i + 1].typ === 'IriRef'
340
- ) {
341
- i += 1;
342
- continue;
343
- }
344
-
345
- // Count QNames in identifiers (including datatypes like xsd:integer).
346
- if (t.typ === 'Ident') {
347
- maybeAddFromQName(t.value);
348
- }
349
- }
350
-
351
- return used;
352
- }
353
-
354
285
  function restrictPrefixEnv(prefEnv, usedSet) {
355
286
  const m = {};
356
287
  for (const p of usedSet) {
@@ -368,10 +299,7 @@ function main() {
368
299
  const mayAutoRenderOutputStrings = programMayProduceOutputStrings(triples, frules, qrules);
369
300
 
370
301
  if (streamMode && !hasQueries && !mayAutoRenderOutputStrings) {
371
- const usedInInput = new Set();
372
- for (const source of tokenSets) {
373
- for (const pfx of prefixesUsedInInputTokens(source.tokens, source.prefixes)) usedInInput.add(pfx);
374
- }
302
+ const usedInInput = mergedDocument.usedPrefixes instanceof Set ? new Set(mergedDocument.usedPrefixes) : new Set();
375
303
  const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
376
304
 
377
305
  // Ensure log:trace uses the same compact prefix set as the output.
package/lib/engine.js CHANGED
@@ -610,10 +610,6 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
610
610
  termsEqualNoIntDecimal,
611
611
  });
612
612
 
613
- try {
614
- registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
615
- } catch (_) {}
616
-
617
613
  // Initialize proof/output helpers (implemented in lib/explain.js).
618
614
  const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
619
615
  applySubstTerm,
@@ -31,13 +31,101 @@ function emptyParsedDocument() {
31
31
  };
32
32
  }
33
33
 
34
+ function prefixesUsedInTokens(tokens, prefEnv) {
35
+ const used = new Set();
36
+ const toks = Array.isArray(tokens) ? tokens : [];
37
+ const prefixes = prefEnv && prefEnv.map ? prefEnv.map : {};
38
+
39
+ function maybeAddFromQName(name) {
40
+ if (typeof name !== 'string') return;
41
+ if (!name.includes(':')) return;
42
+ if (name.startsWith('_:')) return; // blank node
43
+
44
+ // Split only on the first ':'; the empty prefix is valid for ":foo".
45
+ const idx = name.indexOf(':');
46
+ const p = name.slice(0, idx);
47
+
48
+ // Ignore strings like "http://..." unless that prefix is actually defined.
49
+ if (!Object.prototype.hasOwnProperty.call(prefixes, p)) return;
50
+
51
+ used.add(p);
52
+ }
53
+
54
+ for (let i = 0; i < toks.length; i++) {
55
+ const t = toks[i];
56
+ if (!t) continue;
57
+
58
+ // Skip @prefix ... .
59
+ if (t.typ === 'AtPrefix') {
60
+ while (i < toks.length && toks[i].typ !== 'Dot' && toks[i].typ !== 'EOF') i++;
61
+ continue;
62
+ }
63
+
64
+ // Skip @base ... .
65
+ if (t.typ === 'AtBase') {
66
+ while (i < toks.length && toks[i].typ !== 'Dot' && toks[i].typ !== 'EOF') i++;
67
+ continue;
68
+ }
69
+
70
+ // Skip SPARQL/Turtle PREFIX pfx: <iri>
71
+ if (
72
+ t.typ === 'Ident' &&
73
+ typeof t.value === 'string' &&
74
+ t.value.toLowerCase() === 'prefix' &&
75
+ toks[i + 1] &&
76
+ toks[i + 1].typ === 'Ident' &&
77
+ typeof toks[i + 1].value === 'string' &&
78
+ toks[i + 1].value.endsWith(':') &&
79
+ toks[i + 2] &&
80
+ (toks[i + 2].typ === 'IriRef' || toks[i + 2].typ === 'Ident')
81
+ ) {
82
+ i += 2;
83
+ continue;
84
+ }
85
+
86
+ // Skip SPARQL BASE <iri>
87
+ if (
88
+ t.typ === 'Ident' &&
89
+ typeof t.value === 'string' &&
90
+ t.value.toLowerCase() === 'base' &&
91
+ toks[i + 1] &&
92
+ toks[i + 1].typ === 'IriRef'
93
+ ) {
94
+ i += 1;
95
+ continue;
96
+ }
97
+
98
+ // Count QNames in identifiers, including datatypes like xsd:integer.
99
+ if (t.typ === 'Ident') maybeAddFromQName(t.value);
100
+ }
101
+
102
+ return used;
103
+ }
104
+
34
105
  function parseN3Text(text, opts = {}) {
35
- const { baseIri = '', label = '<input>' } = opts || {};
106
+ const { baseIri = '', label = '<input>', keepSourceArtifacts = true, collectUsedPrefixes = false } = opts || {};
36
107
  const tokens = lex(text);
37
108
  const parser = new Parser(tokens);
38
109
  if (baseIri) parser.prefixes.setBase(baseIri);
39
110
  const [prefixes, triples, frules, brules, logQueryRules] = parser.parseDocument();
40
- return { prefixes, triples, frules, brules, logQueryRules, tokens, text, label };
111
+
112
+ const doc = { prefixes, triples, frules, brules, logQueryRules, label };
113
+
114
+ if (collectUsedPrefixes) {
115
+ Object.defineProperty(doc, 'usedPrefixes', {
116
+ value: prefixesUsedInTokens(tokens, prefixes),
117
+ enumerable: false,
118
+ writable: false,
119
+ configurable: true,
120
+ });
121
+ }
122
+
123
+ if (keepSourceArtifacts) {
124
+ doc.tokens = tokens;
125
+ doc.text = text;
126
+ }
127
+
128
+ return doc;
41
129
  }
42
130
 
43
131
  function sourceBlankPrefix(sourceIndex) {
@@ -96,16 +184,27 @@ function scopeBlankNodesInDocument(doc, sourceIndex) {
96
184
  return out;
97
185
  }
98
186
 
99
- return {
187
+ const out = {
100
188
  prefixes: doc.prefixes,
101
189
  triples: (doc.triples || []).map(cloneTriple),
102
190
  frules: (doc.frules || []).map(cloneRule),
103
191
  brules: (doc.brules || []).map(cloneRule),
104
192
  logQueryRules: (doc.logQueryRules || []).map(cloneRule),
105
- tokens: doc.tokens,
106
- text: doc.text,
107
193
  label: doc.label,
108
194
  };
195
+
196
+ if (doc.usedPrefixes instanceof Set) {
197
+ Object.defineProperty(out, 'usedPrefixes', {
198
+ value: new Set(doc.usedPrefixes),
199
+ enumerable: false,
200
+ writable: false,
201
+ configurable: true,
202
+ });
203
+ }
204
+ if (Object.prototype.hasOwnProperty.call(doc, 'tokens')) out.tokens = doc.tokens;
205
+ if (Object.prototype.hasOwnProperty.call(doc, 'text')) out.text = doc.text;
206
+
207
+ return out;
109
208
  }
110
209
 
111
210
  function mergePrefixEnvs(target, source) {
@@ -124,9 +223,10 @@ function mergePrefixEnvs(target, source) {
124
223
  function mergeParsedDocuments(docs, opts = {}) {
125
224
  const documents = Array.isArray(docs) ? docs : [];
126
225
  const scopeBlankNodes = typeof opts.scopeBlankNodes === 'boolean' ? opts.scopeBlankNodes : documents.length > 1;
226
+ const keepSources = !!opts.keepSources || !!opts.keepSourceArtifacts;
127
227
 
128
228
  const merged = emptyParsedDocument();
129
- const mergedSources = [];
229
+ const mergedSources = keepSources ? [] : null;
130
230
 
131
231
  for (let i = 0; i < documents.length; i++) {
132
232
  const originalDoc = documents[i] || emptyParsedDocument();
@@ -137,15 +237,30 @@ function mergeParsedDocuments(docs, opts = {}) {
137
237
  merged.frules.push(...(doc.frules || []));
138
238
  merged.brules.push(...(doc.brules || []));
139
239
  merged.logQueryRules.push(...(doc.logQueryRules || []));
140
- mergedSources.push(doc);
240
+
241
+ if (doc.usedPrefixes instanceof Set) {
242
+ if (!(merged.usedPrefixes instanceof Set)) {
243
+ Object.defineProperty(merged, 'usedPrefixes', {
244
+ value: new Set(),
245
+ enumerable: false,
246
+ writable: false,
247
+ configurable: true,
248
+ });
249
+ }
250
+ for (const pfx of doc.usedPrefixes) merged.usedPrefixes.add(pfx);
251
+ }
252
+
253
+ if (keepSources) mergedSources.push(doc);
141
254
  }
142
255
 
143
- Object.defineProperty(merged, 'sources', {
144
- value: mergedSources,
145
- enumerable: false,
146
- writable: false,
147
- configurable: true,
148
- });
256
+ if (keepSources) {
257
+ Object.defineProperty(merged, 'sources', {
258
+ value: mergedSources,
259
+ enumerable: false,
260
+ writable: false,
261
+ configurable: true,
262
+ });
263
+ }
149
264
 
150
265
  return merged;
151
266
  }
@@ -177,14 +292,17 @@ function parseN3SourceList(input, opts = {}) {
177
292
  if (!isN3SourceListInput(input)) return null;
178
293
  const sources = input.sources.map(normalizeN3SourceItem);
179
294
  const defaultBaseIri = typeof opts.baseIri === 'string' ? opts.baseIri : '';
180
- const parsed = sources.map((source, index) =>
295
+ const parsed = sources.map((source) =>
181
296
  parseN3Text(source.text, {
182
297
  label: source.label,
183
298
  baseIri: source.baseIri || (sources.length === 1 ? defaultBaseIri : ''),
299
+ collectUsedPrefixes: true,
300
+ keepSourceArtifacts: !!opts.keepSourceArtifacts,
184
301
  }),
185
302
  );
186
303
  return mergeParsedDocuments(parsed, {
187
304
  scopeBlankNodes: typeof input.scopeBlankNodes === 'boolean' ? input.scopeBlankNodes : parsed.length > 1,
305
+ keepSources: !!opts.keepSourceArtifacts,
188
306
  });
189
307
  }
190
308
 
@@ -193,6 +311,7 @@ module.exports = {
193
311
  parseN3Text,
194
312
  mergeParsedDocuments,
195
313
  scopeBlankNodesInDocument,
314
+ prefixesUsedInTokens,
196
315
  isN3SourceListInput,
197
316
  parseN3SourceList,
198
317
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.23.1",
3
+ "version": "1.23.3",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -96,6 +96,14 @@ function resolveExpectedPath(outputDir, inputFile) {
96
96
  return path.join(outputDir, candidates[0]);
97
97
  }
98
98
 
99
+ function resolveExampleBuiltinPath(root, inputFile) {
100
+ const stem = path.basename(inputFile, path.extname(inputFile));
101
+ const rel = path.join('examples', 'builtin', `${stem}.js`);
102
+ const abs = path.join(root, rel);
103
+ if (!fs.existsSync(abs)) return null;
104
+ return { abs, rel };
105
+ }
106
+
99
107
  function main() {
100
108
  const suiteStart = Date.now();
101
109
 
@@ -170,17 +178,32 @@ function main() {
170
178
  const tmpDir = mkTmpDir();
171
179
  const generatedPath = path.join(tmpDir, 'generated.n3');
172
180
 
173
- // Run eyeling on this file (cwd examplesDir so relative behavior matches old script)
181
+ // Run eyeling on this file. If examples/builtin/<stem>.js exists,
182
+ // load it for the matching examples/<stem>.n3 file. Builtin-backed examples
183
+ // run from the repository root so the command shape matches documented usage:
184
+ // node eyeling.js --builtin examples/builtin/foo.js examples/foo.n3
185
+ const builtin = resolveExampleBuiltinPath(root, file);
174
186
  const outFd = fs.openSync(generatedPath, 'w');
175
-
176
- const r = cp.spawnSync(nodePath, [eyelingJsPath, '-d', file], {
177
- cwd: examplesDir,
178
- stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
179
- maxBuffer: 200 * 1024 * 1024,
180
- encoding: 'utf8',
181
- });
182
-
183
- fs.closeSync(outFd);
187
+ let r;
188
+ try {
189
+ if (builtin) {
190
+ r = cp.spawnSync(nodePath, [eyelingJsPath, '-d', '--builtin', builtin.rel, path.join('examples', file)], {
191
+ cwd: root,
192
+ stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
193
+ maxBuffer: 200 * 1024 * 1024,
194
+ encoding: 'utf8',
195
+ });
196
+ } else {
197
+ r = cp.spawnSync(nodePath, [eyelingJsPath, '-d', file], {
198
+ cwd: examplesDir,
199
+ stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
200
+ maxBuffer: 200 * 1024 * 1024,
201
+ encoding: 'utf8',
202
+ });
203
+ }
204
+ } finally {
205
+ fs.closeSync(outFd);
206
+ }
184
207
 
185
208
  const rc = r.status == null ? 1 : r.status;
186
209