eyeleng 1.1.1 → 1.2.0

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/eyeleng.js CHANGED
@@ -17,7 +17,7 @@
17
17
  queryProgram,
18
18
  queryRunOptions,
19
19
  formatTriples,
20
- formatTrace,
20
+ formatProof,
21
21
  formatBindings,
22
22
  toJSON,
23
23
  resultTriples,
@@ -41,15 +41,28 @@
41
41
 
42
42
  const VERSION = readPackageVersion();
43
43
 
44
- function help() {
44
+ function legacyHelp() {
45
45
  return `eyeleng ${VERSION}\n\nA dependency-free JavaScript implementation experiment for the SHACL 1.2 Rules draft, including SRL and RDF Rules syntax front-ends.\n\nUsage:\n eyeleng [options] [file ...]\n\nOptions:\n --all Print the full closure, including input facts\n --json Print JSON instead of compact triples/bindings\n --trace Print derivation trace to stderr, or include it in JSON\n --stats Print iteration and triple counts to stderr\n --check Parse and analyze only; do not run rules\n --strict Treat static warnings as errors, including recursive term generation\n --deps Print rule dependency edges during --check\n --query TEXT Run a raw SRL body pattern over the closure or backward planner\n --query-file FILE Read a raw SRL body pattern from a file\n --query-mode MODE Use auto, forward, or backward query planning (default auto)\n --hybrid Force aggressive hybrid orientation for function-like rules\n --no-hybrid Disable automatic hybrid forward/backward execution\n --max-iterations N Stop after N fixpoint iterations within a recursive layer\n --no-imports Parse IMPORTS/owl:imports but do not load imported rule sets\n --rdf-messages Parse input as an RDF Message Log\n --include-message-facts Include payload facts while parsing RDF Message Logs\n --syntax MODE Use srl, rdf, or auto syntax detection (default auto)\n --ruleset TERM In RDF syntax, run only the selected srl:RuleSet\n --version Print version\n -h, --help Print this help\n\nWith no file arguments, eyeleng reads from stdin.\n`;
46
46
  }
47
47
 
48
+ function help() {
49
+ return legacyHelp().replace(
50
+ ' --trace Print derivation trace to stderr, or include it in JSON',
51
+ ' --prove Print proof explanations',
52
+ ).replace(
53
+ 'Usage:\n eyeleng [options] [file ...]',
54
+ 'Usage: eyeleng [options] [file-or-url.n3|- ...]',
55
+ ).replace(
56
+ 'With no file arguments, eyeleng reads from stdin.',
57
+ 'With no input arguments, eyeleng prints this help. Use - to read from stdin.',
58
+ );
59
+ }
60
+
48
61
  function parseArgs(argv) {
49
62
  const options = {
50
63
  all: false,
51
64
  json: false,
52
- trace: false,
65
+ prove: false,
53
66
  stats: false,
54
67
  check: false,
55
68
  strict: false,
@@ -70,7 +83,7 @@
70
83
  const arg = argv[i];
71
84
  if (arg === '--all') options.all = true;
72
85
  else if (arg === '--json') options.json = true;
73
- else if (arg === '--trace') options.trace = true;
86
+ else if (arg === '--prove') options.prove = true;
74
87
  else if (arg === '--stats') options.stats = true;
75
88
  else if (arg === '--check') options.check = true;
76
89
  else if (arg === '--strict') options.strict = true;
@@ -111,6 +124,8 @@
111
124
  options.version = true;
112
125
  } else if (arg === '-h' || arg === '--help') {
113
126
  options.help = true;
127
+ } else if (arg === '-') {
128
+ files.push(arg);
114
129
  } else if (arg.startsWith('-')) {
115
130
  throw new Error(`Unknown option ${arg}`);
116
131
  } else {
@@ -121,13 +136,26 @@
121
136
  return { options, files };
122
137
  }
123
138
 
124
- function readInput(files) {
125
- if (files.length === 0) return { source: fs.readFileSync(0, 'utf8'), filename: '<stdin>', baseIRI: null };
126
- if (files.length === 1) {
127
- const filename = path.resolve(files[0]);
128
- return { source: fs.readFileSync(filename, 'utf8'), filename, baseIRI: pathToFileURL(filename).href };
139
+ async function readInput(files, fetchImpl = globalThis.fetch) {
140
+ const inputs = [];
141
+ let readStdin = false;
142
+ for (const spec of files) {
143
+ if (spec === '-') {
144
+ if (readStdin) throw new Error('Standard input (-) may only be specified once');
145
+ readStdin = true;
146
+ inputs.push({ source: fs.readFileSync(0, 'utf8'), filename: '<stdin>', baseIRI: null });
147
+ } else if (/^https?:\/\//i.test(spec)) {
148
+ if (typeof fetchImpl !== 'function') throw new Error(`Cannot fetch URL ${spec}: fetch is unavailable`);
149
+ const response = await fetchImpl(spec);
150
+ if (!response.ok) throw new Error(`Cannot fetch URL ${spec}: HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`);
151
+ inputs.push({ source: await response.text(), filename: spec, baseIRI: spec });
152
+ } else {
153
+ const filename = path.resolve(spec);
154
+ inputs.push({ source: fs.readFileSync(filename, 'utf8'), filename, baseIRI: pathToFileURL(filename).href });
155
+ }
129
156
  }
130
- return { source: files.map((file) => fs.readFileSync(file, 'utf8')).join('\n'), filename: '<input>', baseIRI: null };
157
+ if (inputs.length === 1) return inputs[0];
158
+ return { source: inputs.map((input) => input.source).join('\n'), filename: '<input>', baseIRI: null };
131
159
  }
132
160
 
133
161
  function createFileImportResolver() {
@@ -172,7 +200,7 @@
172
200
  return /^https?:/.test(name) ? compactIRI(name, prefixes) : name;
173
201
  }
174
202
 
175
- function main(argv = process.argv.slice(2), io = process) {
203
+ async function main(argv = process.argv.slice(2), io = process) {
176
204
  try {
177
205
  const { options, files } = parseArgs(argv);
178
206
  if (options.help) {
@@ -183,7 +211,11 @@
183
211
  io.stdout.write(`${VERSION}\n`);
184
212
  return 0;
185
213
  }
186
- const input = readInput(files);
214
+ if (files.length === 0) {
215
+ io.stdout.write(help());
216
+ return 0;
217
+ }
218
+ const input = await readInput(files);
187
219
  const compiled = compile(input.source, {
188
220
  filename: input.filename,
189
221
  baseIRI: input.baseIRI,
@@ -245,15 +277,18 @@
245
277
  }
246
278
 
247
279
  if (options.json) {
248
- io.stdout.write(`${JSON.stringify(toJSON(result, { all: options.all, trace: options.trace, analysis: options.deps }), null, 2)}\n`);
280
+ io.stdout.write(`${JSON.stringify(toJSON(result, { all: options.all, proof: options.prove, analysis: options.deps }), null, 2)}\n`);
249
281
  } else if (result.query) {
250
282
  const out = formatBindings(result.query.bindings, result.prefixes, result.query.select);
251
283
  if (out) io.stdout.write(`${out}\n`);
252
284
  } else {
253
- if (options.trace && result.trace.length > 0) io.stderr.write(`${formatTrace(result.trace, result.prefixes)}\n`);
254
285
  const triples = resultTriples(result, compiled.program, options);
255
286
  const out = formatTriples(triples, result.prefixes);
256
287
  if (out) io.stdout.write(`${out}\n`);
288
+ if (options.prove) {
289
+ const proof = formatProof(result.trace, result.prefixes);
290
+ if (proof) io.stdout.write(`${out ? '\n' : ''}${proof}\n`);
291
+ }
257
292
  }
258
293
 
259
294
  if (options.stats) {
@@ -271,9 +306,9 @@
271
306
  }
272
307
  }
273
308
 
274
- if (require.main === module) process.exitCode = main();
309
+ if (require.main === module) main().then((code) => { process.exitCode = code; });
275
310
 
276
- module.exports = { main, parseArgs, help, VERSION, createFileImportResolver };
311
+ module.exports = { main, parseArgs, readInput, help, VERSION, createFileImportResolver };
277
312
 
278
313
  },
279
314
  "src/api.js": function (require, module, exports) {
@@ -284,7 +319,7 @@
284
319
  const { parseRdfMessageLog, looksLikeRdfMessageLog } = require('./rdfMessages.js');
285
320
  const { evaluate } = require('./engine.js');
286
321
  const { analyze } = require('./analyze.js');
287
- const { formatTriples, sortTriples, toJSON, formatTrace, formatBindings } = require('./format.js');
322
+ const { formatTriples, sortTriples, toJSON, formatTrace, formatProof, formatBindings } = require('./format.js');
288
323
  const { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery } = require('./query.js');
289
324
  const { resultTriples } = require('./output.js');
290
325
 
@@ -407,6 +442,7 @@
407
442
  sortTriples,
408
443
  toJSON,
409
444
  formatTrace,
445
+ formatProof,
410
446
  resultTriples,
411
447
  };
412
448
 
@@ -710,7 +746,7 @@
710
746
  }
711
747
 
712
748
  parseTripleStatement(options = {}) {
713
- const subjectNode = this.parseGraphNode(options);
749
+ const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });
714
750
  const triples = [...subjectNode.triples];
715
751
  triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));
716
752
  return triples;
@@ -724,7 +760,7 @@
724
760
  if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;
725
761
  const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);
726
762
  do {
727
- const objectNode = this.parseGraphNode(options);
763
+ const objectNode = this.parseGraphNode({ ...options, position: 'object' });
728
764
  triples.push(...objectNode.triples);
729
765
  const baseTriple = { s: subject, p: predicate, o: objectNode.term };
730
766
  triples.push(baseTriple);
@@ -791,6 +827,7 @@
791
827
  currentReifier = this.parseOptionalReifier(options);
792
828
  triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });
793
829
  } else if (this.matchValue('{|')) {
830
+ if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');
794
831
  const annotationSubject = currentReifier || this.freshGraphNode(options);
795
832
  triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });
796
833
  triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));
@@ -840,7 +877,7 @@
840
877
  }
841
878
 
842
879
  parseVerbTerm(options = {}) {
843
- const term = this.parseTerm(options);
880
+ const term = this.parseTerm({ ...options, position: 'predicate' });
844
881
  if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');
845
882
  return term;
846
883
  }
@@ -963,7 +1000,10 @@
963
1000
  if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);
964
1001
  if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);
965
1002
  if (token.type === 'word') {
966
- if (token.value === 'a') return iri(RDF_TYPE);
1003
+ if (token.value === 'a') {
1004
+ if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);
1005
+ return iri(RDF_TYPE);
1006
+ }
967
1007
  if (token.value === 'true') return literal(true, XSD_BOOLEAN);
968
1008
  if (token.value === 'false') return literal(false, XSD_BOOLEAN);
969
1009
  if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));
@@ -997,7 +1037,13 @@
997
1037
  return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
998
1038
  }
999
1039
  if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {
1000
- const tag = this.advance().value.slice(1).toLowerCase();
1040
+ const tagToken = this.advance();
1041
+ const rawTag = tagToken.value.slice(1);
1042
+ const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;
1043
+ if (direction && direction !== 'ltr' && direction !== 'rtl') {
1044
+ throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);
1045
+ }
1046
+ const tag = rawTag.toLowerCase();
1001
1047
  const [lang, langDir = null] = tag.split('--');
1002
1048
  return literal(token.value, null, lang, langDir);
1003
1049
  }
@@ -2776,6 +2822,7 @@
2776
2822
  const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
2777
2823
  const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
2778
2824
  const implicitStatementNodes = new Set();
2825
+ function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }
2779
2826
 
2780
2827
  function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
2781
2828
  function peek(offset = 0) { return tokens[i + offset]; }
@@ -2926,7 +2973,7 @@
2926
2973
  expect('>>');
2927
2974
  const node = reifier || freshBlank();
2928
2975
  out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
2929
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
2976
+ implicitStatementNodes.add(implicitStatementNodeKey(node));
2930
2977
  return node;
2931
2978
  }
2932
2979
 
@@ -2955,7 +3002,7 @@
2955
3002
  if (accept(']')) return node;
2956
3003
  parsePredicateObjectList(node, out, graph);
2957
3004
  expect(']');
2958
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
3005
+ if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));
2959
3006
  return node;
2960
3007
  }
2961
3008
 
@@ -3033,9 +3080,8 @@
3033
3080
  if (options3.requireDot) expect('.'); else accept('.');
3034
3081
  return;
3035
3082
  }
3036
- if (peek()?.type === '<<' && peek(1)?.type === '(') throw new Error('Triple term cannot be used as a subject');
3037
- const subject = parseTerm(out, graph, { noLiteral: true, noA: true });
3038
- if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
3083
+ const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });
3084
+ if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {
3039
3085
  if (options3.requireDot) expect('.'); else accept('.');
3040
3086
  return;
3041
3087
  }
@@ -4522,6 +4568,7 @@
4522
4568
  rule: rule.name || `rule#${ruleIndex + 1}`,
4523
4569
  triple,
4524
4570
  binding,
4571
+ uses: proofUses(rule.body, binding),
4525
4572
  });
4526
4573
  }
4527
4574
  }
@@ -4532,6 +4579,17 @@
4532
4579
  return { applications, added };
4533
4580
  }
4534
4581
 
4582
+ function proofUses(body, binding) {
4583
+ return body
4584
+ .filter((clause) => clause.type === 'triple')
4585
+ .map((clause) => ({
4586
+ s: instantiateTerm(clause.triple.s, binding),
4587
+ p: instantiateTerm(clause.triple.p, binding),
4588
+ o: instantiateTerm(clause.triple.o, binding),
4589
+ }))
4590
+ .filter((triple) => ![triple.s, triple.p, triple.o].some((term) => term && term.type === 'var'));
4591
+ }
4592
+
4535
4593
  function prepareBodyContext(program, store, context) {
4536
4594
  if (!context.hybridBackwardPredicates || context.hybridBackwardPredicates.size === 0) return context;
4537
4595
  return {
@@ -6322,6 +6380,35 @@
6322
6380
  return trace.map((entry) => `#${entry.iteration} ${entry.rule} => ${formatTriple(entry.triple, prefixes)}`).join('\n');
6323
6381
  }
6324
6382
 
6383
+ function formatProof(trace, prefixes = {}) {
6384
+ if (!trace.length) return '';
6385
+ const lines = ['@prefix pe: <https://eyereasoner.github.io/pe#> .', ''];
6386
+ for (const entry of trace) {
6387
+ const conclusion = formatTriple(entry.triple, prefixes);
6388
+ lines.push(`{ ${conclusion} } pe:why {`);
6389
+ lines.push(` { ${conclusion} }`);
6390
+ lines.push(` pe:by [ pe:rule ${quoteString(entry.rule)} ]${proofDetails(entry, prefixes)} .`);
6391
+ lines.push('}.', '');
6392
+ }
6393
+ return lines.join('\n').trimEnd();
6394
+ }
6395
+
6396
+ function proofDetails(entry, prefixes) {
6397
+ const details = [];
6398
+ const bindings = Object.entries(entry.binding || {}).sort(([a], [b]) => a.localeCompare(b));
6399
+ if (bindings.length > 0) {
6400
+ details.push(`\n pe:binding ${bindings.map(([name, value]) => `[ pe:var ${quoteString(name)}; pe:value ${formatTerm(value, prefixes)} ]`).join(', ')}`);
6401
+ }
6402
+ if (entry.uses && entry.uses.length > 0) {
6403
+ details.push(`\n pe:uses ${entry.uses.map((triple) => `{ ${formatTriple(triple, prefixes)} }`).join(', ')}`);
6404
+ }
6405
+ return details.length ? `;${details.join(';')}` : '';
6406
+ }
6407
+
6408
+ function quoteString(value) {
6409
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
6410
+ }
6411
+
6325
6412
  function formatBindings(bindings, prefixes = {}, select = null) {
6326
6413
  const columns = select && select.length > 0 ? select : inferColumns(bindings);
6327
6414
  return bindings
@@ -6353,7 +6440,7 @@
6353
6440
  prefixes: result.prefixes,
6354
6441
  diagnostics: result.diagnostics || [],
6355
6442
  triples: sortTriples(triples, result.prefixes).map(jsonSafeTriple),
6356
- trace: options.trace ? result.trace : undefined,
6443
+ proof: options.proof ? result.trace : undefined,
6357
6444
  };
6358
6445
  if (result.query) json.query = jsonSafeValue(result.query);
6359
6446
  if (result.analysis && options.analysis) json.analysis = result.analysis;
@@ -6382,7 +6469,7 @@
6382
6469
  return value;
6383
6470
  }
6384
6471
 
6385
- module.exports = { sortTriples, formatTriples, formatTrace, formatBindings, formatBinding, toJSON };
6472
+ module.exports = { sortTriples, formatTriples, formatTrace, formatProof, formatBindings, formatBinding, toJSON };
6386
6473
 
6387
6474
  },
6388
6475
  "src/query.js": function (require, module, exports) {
@@ -6534,5 +6621,7 @@
6534
6621
  __modules[id](localRequire, module, module.exports);
6535
6622
  return module.exports;
6536
6623
  }
6537
- process.exitCode = __require("src/cli.js").main(process.argv.slice(2));
6624
+ Promise.resolve(__require("src/cli.js").main(process.argv.slice(2)))
6625
+ .then((code) => { process.exitCode = code; })
6626
+ .catch((error) => { console.error(error); process.exitCode = 1; });
6538
6627
  }());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeleng",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "The EYE Logic Engine: a JavaScript implementation of SHACL Rules, including SRL and RDF Rules syntax front-ends.",
5
5
  "keywords": [
6
6
  "SRL",
package/playground.html CHANGED
@@ -438,7 +438,7 @@
438
438
  <div class="checks" aria-label="Reasoning options">
439
439
  <label class="toggle"><input id="all-output" type="checkbox" /> Print full closure (--all)</label>
440
440
  <label class="toggle"><input id="json-output" type="checkbox" /> JSON output (--json)</label>
441
- <label class="toggle"><input id="trace-output" type="checkbox" /> Include derivation trace (--trace)</label>
441
+ <label class="toggle"><input id="trace-output" type="checkbox" /> Include proof explanations (--prove)</label>
442
442
  <label class="toggle"><input id="strict-mode" type="checkbox" /> Treat warnings as errors (--strict)</label>
443
443
  <label class="toggle"><input id="check-only" type="checkbox" /> Check only, do not run rules</label>
444
444
  <label class="toggle"><input id="show-analysis" type="checkbox" /> Include analysis in JSON</label>
@@ -946,7 +946,7 @@
946
946
  if (els.json.checked) {
947
947
  text = JSON.stringify(eyeleng.toJSON(result, {
948
948
  all: els.all.checked,
949
- trace: els.trace.checked,
949
+ proof: els.trace.checked,
950
950
  analysis: els.analysis.checked,
951
951
  }), null, 2);
952
952
  } else if (result.query) {
@@ -955,8 +955,8 @@
955
955
  const triples = els.all.checked ? result.closure : result.inferred;
956
956
  text = eyeleng.formatTriples(triples, result.prefixes);
957
957
  if (els.trace.checked && result.trace.length > 0) {
958
- const trace = eyeleng.formatTrace(result.trace, result.prefixes);
959
- text = (text ? text + '\n\n' : '') + '# Trace\n' + trace;
958
+ const proof = eyeleng.formatProof(result.trace, result.prefixes);
959
+ text = (text ? text + '\n\n' : '') + proof;
960
960
  }
961
961
  }
962
962
  if (!text) text = '(no output)';