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/src/api.js CHANGED
@@ -5,7 +5,7 @@ const { parseRdfSyntax, parseRdfDocument, rdfDocumentToProgram, looksLikeRdfRule
5
5
  const { parseRdfMessageLog, looksLikeRdfMessageLog } = require('./rdfMessages.js');
6
6
  const { evaluate } = require('./engine.js');
7
7
  const { analyze } = require('./analyze.js');
8
- const { formatTriples, sortTriples, toJSON, formatTrace, formatBindings } = require('./format.js');
8
+ const { formatTriples, sortTriples, toJSON, formatTrace, formatProof, formatBindings } = require('./format.js');
9
9
  const { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery } = require('./query.js');
10
10
  const { resultTriples } = require('./output.js');
11
11
 
@@ -128,5 +128,6 @@ module.exports = {
128
128
  sortTriples,
129
129
  toJSON,
130
130
  formatTrace,
131
+ formatProof,
131
132
  resultTriples,
132
133
  };
package/src/cli.js CHANGED
@@ -11,7 +11,7 @@ const {
11
11
  queryProgram,
12
12
  queryRunOptions,
13
13
  formatTriples,
14
- formatTrace,
14
+ formatProof,
15
15
  formatBindings,
16
16
  toJSON,
17
17
  resultTriples,
@@ -35,15 +35,28 @@ function readPackageVersion() {
35
35
 
36
36
  const VERSION = readPackageVersion();
37
37
 
38
- function help() {
38
+ function legacyHelp() {
39
39
  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`;
40
40
  }
41
41
 
42
+ function help() {
43
+ return legacyHelp().replace(
44
+ ' --trace Print derivation trace to stderr, or include it in JSON',
45
+ ' --prove Print proof explanations',
46
+ ).replace(
47
+ 'Usage:\n eyeleng [options] [file ...]',
48
+ 'Usage: eyeleng [options] [file-or-url.n3|- ...]',
49
+ ).replace(
50
+ 'With no file arguments, eyeleng reads from stdin.',
51
+ 'With no input arguments, eyeleng prints this help. Use - to read from stdin.',
52
+ );
53
+ }
54
+
42
55
  function parseArgs(argv) {
43
56
  const options = {
44
57
  all: false,
45
58
  json: false,
46
- trace: false,
59
+ prove: false,
47
60
  stats: false,
48
61
  check: false,
49
62
  strict: false,
@@ -64,7 +77,7 @@ function parseArgs(argv) {
64
77
  const arg = argv[i];
65
78
  if (arg === '--all') options.all = true;
66
79
  else if (arg === '--json') options.json = true;
67
- else if (arg === '--trace') options.trace = true;
80
+ else if (arg === '--prove') options.prove = true;
68
81
  else if (arg === '--stats') options.stats = true;
69
82
  else if (arg === '--check') options.check = true;
70
83
  else if (arg === '--strict') options.strict = true;
@@ -105,6 +118,8 @@ function parseArgs(argv) {
105
118
  options.version = true;
106
119
  } else if (arg === '-h' || arg === '--help') {
107
120
  options.help = true;
121
+ } else if (arg === '-') {
122
+ files.push(arg);
108
123
  } else if (arg.startsWith('-')) {
109
124
  throw new Error(`Unknown option ${arg}`);
110
125
  } else {
@@ -115,13 +130,26 @@ function parseArgs(argv) {
115
130
  return { options, files };
116
131
  }
117
132
 
118
- function readInput(files) {
119
- if (files.length === 0) return { source: fs.readFileSync(0, 'utf8'), filename: '<stdin>', baseIRI: null };
120
- if (files.length === 1) {
121
- const filename = path.resolve(files[0]);
122
- return { source: fs.readFileSync(filename, 'utf8'), filename, baseIRI: pathToFileURL(filename).href };
133
+ async function readInput(files, fetchImpl = globalThis.fetch) {
134
+ const inputs = [];
135
+ let readStdin = false;
136
+ for (const spec of files) {
137
+ if (spec === '-') {
138
+ if (readStdin) throw new Error('Standard input (-) may only be specified once');
139
+ readStdin = true;
140
+ inputs.push({ source: fs.readFileSync(0, 'utf8'), filename: '<stdin>', baseIRI: null });
141
+ } else if (/^https?:\/\//i.test(spec)) {
142
+ if (typeof fetchImpl !== 'function') throw new Error(`Cannot fetch URL ${spec}: fetch is unavailable`);
143
+ const response = await fetchImpl(spec);
144
+ if (!response.ok) throw new Error(`Cannot fetch URL ${spec}: HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`);
145
+ inputs.push({ source: await response.text(), filename: spec, baseIRI: spec });
146
+ } else {
147
+ const filename = path.resolve(spec);
148
+ inputs.push({ source: fs.readFileSync(filename, 'utf8'), filename, baseIRI: pathToFileURL(filename).href });
149
+ }
123
150
  }
124
- return { source: files.map((file) => fs.readFileSync(file, 'utf8')).join('\n'), filename: '<input>', baseIRI: null };
151
+ if (inputs.length === 1) return inputs[0];
152
+ return { source: inputs.map((input) => input.source).join('\n'), filename: '<input>', baseIRI: null };
125
153
  }
126
154
 
127
155
  function createFileImportResolver() {
@@ -166,7 +194,7 @@ function formatRuleName(name, prefixes = {}) {
166
194
  return /^https?:/.test(name) ? compactIRI(name, prefixes) : name;
167
195
  }
168
196
 
169
- function main(argv = process.argv.slice(2), io = process) {
197
+ async function main(argv = process.argv.slice(2), io = process) {
170
198
  try {
171
199
  const { options, files } = parseArgs(argv);
172
200
  if (options.help) {
@@ -177,7 +205,11 @@ function main(argv = process.argv.slice(2), io = process) {
177
205
  io.stdout.write(`${VERSION}\n`);
178
206
  return 0;
179
207
  }
180
- const input = readInput(files);
208
+ if (files.length === 0) {
209
+ io.stdout.write(help());
210
+ return 0;
211
+ }
212
+ const input = await readInput(files);
181
213
  const compiled = compile(input.source, {
182
214
  filename: input.filename,
183
215
  baseIRI: input.baseIRI,
@@ -239,15 +271,18 @@ function main(argv = process.argv.slice(2), io = process) {
239
271
  }
240
272
 
241
273
  if (options.json) {
242
- io.stdout.write(`${JSON.stringify(toJSON(result, { all: options.all, trace: options.trace, analysis: options.deps }), null, 2)}\n`);
274
+ io.stdout.write(`${JSON.stringify(toJSON(result, { all: options.all, proof: options.prove, analysis: options.deps }), null, 2)}\n`);
243
275
  } else if (result.query) {
244
276
  const out = formatBindings(result.query.bindings, result.prefixes, result.query.select);
245
277
  if (out) io.stdout.write(`${out}\n`);
246
278
  } else {
247
- if (options.trace && result.trace.length > 0) io.stderr.write(`${formatTrace(result.trace, result.prefixes)}\n`);
248
279
  const triples = resultTriples(result, compiled.program, options);
249
280
  const out = formatTriples(triples, result.prefixes);
250
281
  if (out) io.stdout.write(`${out}\n`);
282
+ if (options.prove) {
283
+ const proof = formatProof(result.trace, result.prefixes);
284
+ if (proof) io.stdout.write(`${out ? '\n' : ''}${proof}\n`);
285
+ }
251
286
  }
252
287
 
253
288
  if (options.stats) {
@@ -265,6 +300,6 @@ function main(argv = process.argv.slice(2), io = process) {
265
300
  }
266
301
  }
267
302
 
268
- if (require.main === module) process.exitCode = main();
303
+ if (require.main === module) main().then((code) => { process.exitCode = code; });
269
304
 
270
- module.exports = { main, parseArgs, help, VERSION, createFileImportResolver };
305
+ module.exports = { main, parseArgs, readInput, help, VERSION, createFileImportResolver };
package/src/engine.js CHANGED
@@ -202,6 +202,7 @@ function applyRuleOnce(program, store, ruleIndex, context) {
202
202
  rule: rule.name || `rule#${ruleIndex + 1}`,
203
203
  triple,
204
204
  binding,
205
+ uses: proofUses(rule.body, binding),
205
206
  });
206
207
  }
207
208
  }
@@ -212,6 +213,17 @@ function applyRuleOnce(program, store, ruleIndex, context) {
212
213
  return { applications, added };
213
214
  }
214
215
 
216
+ function proofUses(body, binding) {
217
+ return body
218
+ .filter((clause) => clause.type === 'triple')
219
+ .map((clause) => ({
220
+ s: instantiateTerm(clause.triple.s, binding),
221
+ p: instantiateTerm(clause.triple.p, binding),
222
+ o: instantiateTerm(clause.triple.o, binding),
223
+ }))
224
+ .filter((triple) => ![triple.s, triple.p, triple.o].some((term) => term && term.type === 'var'));
225
+ }
226
+
215
227
  function prepareBodyContext(program, store, context) {
216
228
  if (!context.hybridBackwardPredicates || context.hybridBackwardPredicates.size === 0) return context;
217
229
  return {
package/src/format.js CHANGED
@@ -20,6 +20,35 @@ function formatTrace(trace, prefixes = {}) {
20
20
  return trace.map((entry) => `#${entry.iteration} ${entry.rule} => ${formatTriple(entry.triple, prefixes)}`).join('\n');
21
21
  }
22
22
 
23
+ function formatProof(trace, prefixes = {}) {
24
+ if (!trace.length) return '';
25
+ const lines = ['@prefix pe: <https://eyereasoner.github.io/pe#> .', ''];
26
+ for (const entry of trace) {
27
+ const conclusion = formatTriple(entry.triple, prefixes);
28
+ lines.push(`{ ${conclusion} } pe:why {`);
29
+ lines.push(` { ${conclusion} }`);
30
+ lines.push(` pe:by [ pe:rule ${quoteString(entry.rule)} ]${proofDetails(entry, prefixes)} .`);
31
+ lines.push('}.', '');
32
+ }
33
+ return lines.join('\n').trimEnd();
34
+ }
35
+
36
+ function proofDetails(entry, prefixes) {
37
+ const details = [];
38
+ const bindings = Object.entries(entry.binding || {}).sort(([a], [b]) => a.localeCompare(b));
39
+ if (bindings.length > 0) {
40
+ details.push(`\n pe:binding ${bindings.map(([name, value]) => `[ pe:var ${quoteString(name)}; pe:value ${formatTerm(value, prefixes)} ]`).join(', ')}`);
41
+ }
42
+ if (entry.uses && entry.uses.length > 0) {
43
+ details.push(`\n pe:uses ${entry.uses.map((triple) => `{ ${formatTriple(triple, prefixes)} }`).join(', ')}`);
44
+ }
45
+ return details.length ? `;${details.join(';')}` : '';
46
+ }
47
+
48
+ function quoteString(value) {
49
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
50
+ }
51
+
23
52
  function formatBindings(bindings, prefixes = {}, select = null) {
24
53
  const columns = select && select.length > 0 ? select : inferColumns(bindings);
25
54
  return bindings
@@ -51,7 +80,7 @@ function toJSON(result, options = {}) {
51
80
  prefixes: result.prefixes,
52
81
  diagnostics: result.diagnostics || [],
53
82
  triples: sortTriples(triples, result.prefixes).map(jsonSafeTriple),
54
- trace: options.trace ? result.trace : undefined,
83
+ proof: options.proof ? result.trace : undefined,
55
84
  };
56
85
  if (result.query) json.query = jsonSafeValue(result.query);
57
86
  if (result.analysis && options.analysis) json.analysis = result.analysis;
@@ -80,4 +109,4 @@ function jsonSafeValue(value) {
80
109
  return value;
81
110
  }
82
111
 
83
- module.exports = { sortTriples, formatTriples, formatTrace, formatBindings, formatBinding, toJSON };
112
+ module.exports = { sortTriples, formatTriples, formatTrace, formatProof, formatBindings, formatBinding, toJSON };
package/src/parser.js CHANGED
@@ -296,7 +296,7 @@ class Parser {
296
296
  }
297
297
 
298
298
  parseTripleStatement(options = {}) {
299
- const subjectNode = this.parseGraphNode(options);
299
+ const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });
300
300
  const triples = [...subjectNode.triples];
301
301
  triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));
302
302
  return triples;
@@ -310,7 +310,7 @@ class Parser {
310
310
  if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;
311
311
  const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);
312
312
  do {
313
- const objectNode = this.parseGraphNode(options);
313
+ const objectNode = this.parseGraphNode({ ...options, position: 'object' });
314
314
  triples.push(...objectNode.triples);
315
315
  const baseTriple = { s: subject, p: predicate, o: objectNode.term };
316
316
  triples.push(baseTriple);
@@ -377,6 +377,7 @@ class Parser {
377
377
  currentReifier = this.parseOptionalReifier(options);
378
378
  triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });
379
379
  } else if (this.matchValue('{|')) {
380
+ if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');
380
381
  const annotationSubject = currentReifier || this.freshGraphNode(options);
381
382
  triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });
382
383
  triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));
@@ -426,7 +427,7 @@ class Parser {
426
427
  }
427
428
 
428
429
  parseVerbTerm(options = {}) {
429
- const term = this.parseTerm(options);
430
+ const term = this.parseTerm({ ...options, position: 'predicate' });
430
431
  if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');
431
432
  return term;
432
433
  }
@@ -549,7 +550,10 @@ class Parser {
549
550
  if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);
550
551
  if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);
551
552
  if (token.type === 'word') {
552
- if (token.value === 'a') return iri(RDF_TYPE);
553
+ if (token.value === 'a') {
554
+ if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);
555
+ return iri(RDF_TYPE);
556
+ }
553
557
  if (token.value === 'true') return literal(true, XSD_BOOLEAN);
554
558
  if (token.value === 'false') return literal(false, XSD_BOOLEAN);
555
559
  if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));
@@ -583,7 +587,13 @@ class Parser {
583
587
  return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
584
588
  }
585
589
  if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {
586
- const tag = this.advance().value.slice(1).toLowerCase();
590
+ const tagToken = this.advance();
591
+ const rawTag = tagToken.value.slice(1);
592
+ const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;
593
+ if (direction && direction !== 'ltr' && direction !== 'rtl') {
594
+ throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);
595
+ }
596
+ const tag = rawTag.toLowerCase();
587
597
  const [lang, langDir = null] = tag.split('--');
588
598
  return literal(token.value, null, lang, langDir);
589
599
  }
package/src/rdfSyntax.js CHANGED
@@ -1160,6 +1160,7 @@ function parseN3(source, options = {}) {
1160
1160
  const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
1161
1161
  const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
1162
1162
  const implicitStatementNodes = new Set();
1163
+ function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }
1163
1164
 
1164
1165
  function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
1165
1166
  function peek(offset = 0) { return tokens[i + offset]; }
@@ -1310,7 +1311,7 @@ function parseN3(source, options = {}) {
1310
1311
  expect('>>');
1311
1312
  const node = reifier || freshBlank();
1312
1313
  out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
1313
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
1314
+ implicitStatementNodes.add(implicitStatementNodeKey(node));
1314
1315
  return node;
1315
1316
  }
1316
1317
 
@@ -1339,7 +1340,7 @@ function parseN3(source, options = {}) {
1339
1340
  if (accept(']')) return node;
1340
1341
  parsePredicateObjectList(node, out, graph);
1341
1342
  expect(']');
1342
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
1343
+ if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));
1343
1344
  return node;
1344
1345
  }
1345
1346
 
@@ -1417,9 +1418,8 @@ function parseN3(source, options = {}) {
1417
1418
  if (options3.requireDot) expect('.'); else accept('.');
1418
1419
  return;
1419
1420
  }
1420
- if (peek()?.type === '<<' && peek(1)?.type === '(') throw new Error('Triple term cannot be used as a subject');
1421
- const subject = parseTerm(out, graph, { noLiteral: true, noA: true });
1422
- if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
1421
+ const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });
1422
+ if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {
1423
1423
  if (options3.requireDot) expect('.'); else accept('.');
1424
1424
  return;
1425
1425
  }
package/test/api.test.js CHANGED
@@ -746,6 +746,41 @@ RULE { :test :annotation ?source } WHERE {
746
746
  assert.match(output, /:test :annotation :witness \./);
747
747
  });
748
748
 
749
+ test('DATA blocks accept RDF 1.2 symmetric and standalone reifier syntax', () => {
750
+ const symmetric = parse(`
751
+ PREFIX : <http://example/>
752
+ DATA {
753
+ 123 :q 456 .
754
+ <<( :s :p :o )>> :q <<( :s1 :p1 :o1 )>> .
755
+ }
756
+ `);
757
+ assert.equal(symmetric.data.length, 2);
758
+ assert.equal(symmetric.data[0].s.type, 'literal');
759
+ assert.equal(symmetric.data[1].s.type, 'triple');
760
+
761
+ const standalone = parse(`
762
+ PREFIX : <http://example/>
763
+ DATA { << :a :b :c ~:r >> . }
764
+ `);
765
+ assert.equal(standalone.data.length, 1);
766
+ assert.equal(standalone.data[0].s.value, 'http://example/r');
767
+ assert.equal(standalone.data[0].p.value, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies');
768
+ });
769
+
770
+ test('SRL rejects invalid base directions, subject a, and empty annotations', () => {
771
+ const invalidPatterns = [
772
+ 'RULE { } WHERE { :s :p "abc"@en--LTR }',
773
+ 'RULE { :s :p "abc"@en--LTR } WHERE { ?a ?b ?c }',
774
+ 'RULE { } WHERE { a :p "abc" }',
775
+ 'RULE { a :p "abc" } WHERE { ?a ?b ?c }',
776
+ 'RULE { } WHERE { :s :p :o ~:r {| |} }',
777
+ 'RULE { :s :p :o ~:r {| |} } WHERE { ?a ?b ?c }',
778
+ ];
779
+ for (const pattern of invalidPatterns) {
780
+ assert.throws(() => parse(`PREFIX : <http://example/>\n${pattern}`), Error, pattern);
781
+ }
782
+ });
783
+
749
784
 
750
785
  test('strict conformance allows recursive constant BIND aliases', () => {
751
786
  assert.doesNotThrow(() => compile(`
package/test/cli.test.js CHANGED
@@ -4,7 +4,7 @@ const assert = require('node:assert/strict');
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
  const { test, main } = require('./harness.js').createHarness('CLI');
7
- const { help, parseArgs } = require('../src/cli.js');
7
+ const { help, parseArgs, readInput, main: cliMain } = require('../src/cli.js');
8
8
 
9
9
  function longOptions(text) {
10
10
  return Array.from(text.matchAll(/(^|\s)(--[a-z][a-z0-9-]*)\b/gm), (match) => match[2])
@@ -37,6 +37,11 @@ test('RDF Message Log flags are accepted by parseArgs', () => {
37
37
  assert.equal(parseArgs(['--include-message-facts']).options.includeMessageFacts, true);
38
38
  });
39
39
 
40
+ test('--prove enables proof explanations and --trace is rejected', () => {
41
+ assert.equal(parseArgs(['--prove']).options.prove, true);
42
+ assert.throws(() => parseArgs(['--trace']), /Unknown option --trace/);
43
+ });
44
+
40
45
  test('query mode flag is accepted by parseArgs', () => {
41
46
  assert.equal(parseArgs(['--query-mode', 'auto']).options.queryMode, 'auto');
42
47
  assert.equal(parseArgs(['--query-mode', 'forward']).options.queryMode, 'forward');
@@ -49,4 +54,31 @@ test('query mode flag is accepted by parseArgs', () => {
49
54
  assert.throws(() => parseArgs(['--stream-messages']), /Unknown option --stream-messages/);
50
55
  });
51
56
 
57
+ test('stdin marker and URLs are accepted as inputs', async () => {
58
+ assert.deepEqual(parseArgs(['-']).files, ['-']);
59
+ assert.deepEqual(parseArgs(['https://example.test/rules.n3']).files, ['https://example.test/rules.n3']);
60
+ const input = await readInput(['https://example.test/rules.n3'], async () => ({
61
+ ok: true,
62
+ text: async () => 'DATA { <a> <b> <c> }',
63
+ }));
64
+ assert.equal(input.filename, 'https://example.test/rules.n3');
65
+ assert.equal(input.baseIRI, 'https://example.test/rules.n3');
66
+ });
67
+
68
+ test('no arguments print the same help as -h', async () => {
69
+ function capture() {
70
+ let stdout = '';
71
+ let stderr = '';
72
+ return {
73
+ io: { stdout: { write: (text) => { stdout += text; } }, stderr: { write: (text) => { stderr += text; } } },
74
+ output: () => ({ stdout, stderr }),
75
+ };
76
+ }
77
+ const empty = capture();
78
+ const explicit = capture();
79
+ assert.equal(await cliMain([], empty.io), 0);
80
+ assert.equal(await cliMain(['-h'], explicit.io), 0);
81
+ assert.deepEqual(empty.output(), explicit.output());
82
+ });
83
+
52
84
  main();
@@ -129,6 +129,7 @@ function buildModuleWrapper() {
129
129
  'sortTriples',
130
130
  'toJSON',
131
131
  'formatTrace',
132
+ 'formatProof',
132
133
  ];
133
134
 
134
135
  const chunks = [];
package/tools/bundle.js CHANGED
@@ -106,7 +106,9 @@ function buildCli() {
106
106
  chunks.push(' __modules[id](localRequire, module, module.exports);');
107
107
  chunks.push(' return module.exports;');
108
108
  chunks.push(' }');
109
- chunks.push(` process.exitCode = __require(${js(cliEntry)}).main(process.argv.slice(2));`);
109
+ chunks.push(` Promise.resolve(__require(${js(cliEntry)}).main(process.argv.slice(2)))`);
110
+ chunks.push(' .then((code) => { process.exitCode = code; })');
111
+ chunks.push(" .catch((error) => { console.error(error); process.exitCode = 1; });");
110
112
  chunks.push('}());');
111
113
  chunks.push('');
112
114