eyeling 1.7.20 → 1.8.1

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/README.md CHANGED
@@ -114,14 +114,15 @@ npm run test:packlist
114
114
  Usage: eyeling [options] <file.n3>
115
115
 
116
116
  Options:
117
+ -a, --ast Print parsed AST as JSON and exit.
118
+ -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.
117
119
  -h, --help Show this help and exit.
118
- -v, --version Print version and exit.
119
- -p, --proof-comments Enable proof explanations.
120
120
  -n, --no-proof-comments Disable proof explanations (default).
121
+ -p, --proof-comments Enable proof explanations.
122
+ -r, --strings Print log:outputString strings (ordered by key) instead of N3 output.
121
123
  -s, --super-restricted Disable all builtins except => and <=.
122
- -a, --ast Print parsed AST as JSON and exit.
123
- --strings Print log:outputString strings (ordered by key) instead of N3 output.
124
- --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.
124
+ -t, --stream Stream derived triples as soon as they are derived.
125
+ -v, --version Print version and exit.
125
126
  ```
126
127
 
127
128
  By default, `eyeling`:
package/eyeling.js CHANGED
@@ -111,9 +111,7 @@ let enforceHttpsEnabled = false;
111
111
 
112
112
  function __maybeEnforceHttps(iri) {
113
113
  if (!enforceHttpsEnabled) return iri;
114
- return typeof iri === 'string' && iri.startsWith('http://')
115
- ? 'https://' + iri.slice('http://'.length)
116
- : iri;
114
+ return typeof iri === 'string' && iri.startsWith('http://') ? 'https://' + iri.slice('http://'.length) : iri;
117
115
  }
118
116
 
119
117
  // Environment detection (Node vs Browser/Worker).
@@ -427,12 +425,7 @@ let __tracePrefixes = null;
427
425
  function __traceWriteLine(line) {
428
426
  // Prefer stderr in Node, fall back to console.error elsewhere.
429
427
  try {
430
- if (
431
- __IS_NODE &&
432
- typeof process !== 'undefined' &&
433
- process.stderr &&
434
- typeof process.stderr.write === 'function'
435
- ) {
428
+ if (__IS_NODE && typeof process !== 'undefined' && process.stderr && typeof process.stderr.write === 'function') {
436
429
  process.stderr.write(String(line) + '\n');
437
430
  return;
438
431
  }
@@ -3109,7 +3102,6 @@ function termToJsString(t) {
3109
3102
  return typeof lex === 'string' ? lex : String(lex);
3110
3103
  }
3111
3104
 
3112
-
3113
3105
  function makeStringLiteral(str) {
3114
3106
  // JSON.stringify gives us a valid N3/Turtle-style quoted string
3115
3107
  // (with proper escaping for quotes, backslashes, newlines, …).
@@ -3757,19 +3749,37 @@ function __tfnFormatYear(y) {
3757
3749
 
3758
3750
  function __tfnAdd1ms(c) {
3759
3751
  // Mutates and returns c; c: {year,month,day,hour,minute,second,millis}
3760
- if (c.millis < 999) { c.millis++; return c; }
3752
+ if (c.millis < 999) {
3753
+ c.millis++;
3754
+ return c;
3755
+ }
3761
3756
  c.millis = 0;
3762
- if (c.second < 59) { c.second++; return c; }
3757
+ if (c.second < 59) {
3758
+ c.second++;
3759
+ return c;
3760
+ }
3763
3761
  c.second = 0;
3764
- if (c.minute < 59) { c.minute++; return c; }
3762
+ if (c.minute < 59) {
3763
+ c.minute++;
3764
+ return c;
3765
+ }
3765
3766
  c.minute = 0;
3766
- if (c.hour < 23) { c.hour++; return c; }
3767
+ if (c.hour < 23) {
3768
+ c.hour++;
3769
+ return c;
3770
+ }
3767
3771
  c.hour = 0;
3768
3772
 
3769
3773
  const dim = __tfnDaysInMonth(c.year, c.month);
3770
- if (c.day < dim) { c.day++; return c; }
3774
+ if (c.day < dim) {
3775
+ c.day++;
3776
+ return c;
3777
+ }
3771
3778
  c.day = 1;
3772
- if (c.month < 12) { c.month++; return c; }
3779
+ if (c.month < 12) {
3780
+ c.month++;
3781
+ return c;
3782
+ }
3773
3783
  c.month = 1;
3774
3784
  c.year = c.year + 1n;
3775
3785
  return c;
@@ -3777,16 +3787,31 @@ function __tfnAdd1ms(c) {
3777
3787
 
3778
3788
  function __tfnSub1ms(c) {
3779
3789
  // Mutates and returns c; c: {year,month,day,hour,minute,second,millis}
3780
- if (c.millis > 0) { c.millis--; return c; }
3790
+ if (c.millis > 0) {
3791
+ c.millis--;
3792
+ return c;
3793
+ }
3781
3794
  c.millis = 999;
3782
- if (c.second > 0) { c.second--; return c; }
3795
+ if (c.second > 0) {
3796
+ c.second--;
3797
+ return c;
3798
+ }
3783
3799
  c.second = 59;
3784
- if (c.minute > 0) { c.minute--; return c; }
3800
+ if (c.minute > 0) {
3801
+ c.minute--;
3802
+ return c;
3803
+ }
3785
3804
  c.minute = 59;
3786
- if (c.hour > 0) { c.hour--; return c; }
3805
+ if (c.hour > 0) {
3806
+ c.hour--;
3807
+ return c;
3808
+ }
3787
3809
  c.hour = 23;
3788
3810
 
3789
- if (c.day > 1) { c.day--; return c; }
3811
+ if (c.day > 1) {
3812
+ c.day--;
3813
+ return c;
3814
+ }
3790
3815
  // move to previous month
3791
3816
  if (c.month > 1) {
3792
3817
  c.month--;
@@ -3824,7 +3849,15 @@ function __tfnComputePeriodBounds(parts) {
3824
3849
 
3825
3850
  if (parts.kind === 'date') {
3826
3851
  const startC = { year: parts.year, month: parts.month, day: parts.day, hour: 0, minute: 0, second: 0, millis: 0 };
3827
- const endC = { year: parts.year, month: parts.month, day: parts.day, hour: 23, minute: 59, second: 59, millis: 999 };
3852
+ const endC = {
3853
+ year: parts.year,
3854
+ month: parts.month,
3855
+ day: parts.day,
3856
+ hour: 23,
3857
+ minute: 59,
3858
+ second: 59,
3859
+ millis: 999,
3860
+ };
3828
3861
  return { tzMin, tzMax, startC, endC };
3829
3862
  }
3830
3863
 
@@ -3857,12 +3890,8 @@ function __tfnBindDefaultTimezone(timeLit, tzLit) {
3857
3890
  if (__tfnHasTimezoneSuffix(v)) return timeLit;
3858
3891
 
3859
3892
  // Only support the temporal types we parse.
3860
- if (
3861
- dt !== XSD_NS + 'dateTime' &&
3862
- dt !== XSD_NS + 'date' &&
3863
- dt !== XSD_NS + 'gYearMonth' &&
3864
- dt !== XSD_NS + 'gYear'
3865
- ) return null;
3893
+ if (dt !== XSD_NS + 'dateTime' && dt !== XSD_NS + 'date' && dt !== XSD_NS + 'gYearMonth' && dt !== XSD_NS + 'gYear')
3894
+ return null;
3866
3895
 
3867
3896
  const outLex = `"${v}${tz}"^^<${dt}>`;
3868
3897
  return internLiteral(outLex);
@@ -5163,72 +5192,73 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
5163
5192
  return [];
5164
5193
  }
5165
5194
 
5166
- // -----------------------------------------------------------------
5167
- // 4.3.1 tfn: Time Functions builtins
5168
- // -----------------------------------------------------------------
5169
-
5170
- // tfn:periodMinInclusive / periodMaxInclusive / periodMinExclusive / periodMaxExclusive
5171
- // Schema: ( $s.1+ )+ tfn:* $o-
5172
- const tfnPeriodKind =
5173
- pv === TFN_NS + 'periodMinInclusive'
5174
- ? 'minInc'
5175
- : pv === TFN_NS + 'periodMaxInclusive'
5176
- ? 'maxInc'
5177
- : pv === TFN_NS + 'periodMinExclusive'
5178
- ? 'minEx'
5179
- : pv === TFN_NS + 'periodMaxExclusive'
5180
- ? 'maxEx'
5181
- : null;
5182
- if (tfnPeriodKind) {
5183
- if (!(g.s instanceof ListTerm) || g.s.elems.length !== 1) return [];
5184
- const arg = g.s.elems[0];
5185
- const parts = __tfnParseTemporalLiteralParts(arg);
5186
- if (!parts) return [];
5187
- const bounds = __tfnComputePeriodBounds(parts);
5188
- if (!bounds) return [];
5189
-
5190
- let out;
5191
- if (tfnPeriodKind === 'minInc') {
5192
- out = __tfnMakeDateTimeLiteral({ ...bounds.startC }, bounds.tzMin);
5193
- } else if (tfnPeriodKind === 'maxInc') {
5194
- out = __tfnMakeDateTimeLiteral({ ...bounds.endC }, bounds.tzMax);
5195
- } else if (tfnPeriodKind === 'minEx') {
5196
- const c = __tfnSub1ms({ ...bounds.startC });
5197
- out = __tfnMakeDateTimeLiteral(c, bounds.tzMin);
5198
- } else { // maxEx
5199
- const c = __tfnAdd1ms({ ...bounds.endC });
5200
- out = __tfnMakeDateTimeLiteral(c, bounds.tzMax);
5201
- }
5195
+ // -----------------------------------------------------------------
5196
+ // 4.3.1 tfn: Time Functions builtins
5197
+ // -----------------------------------------------------------------
5202
5198
 
5203
- if (g.o instanceof Var) {
5204
- const s2 = { ...subst };
5205
- s2[g.o.name] = out;
5206
- return [s2];
5199
+ // tfn:periodMinInclusive / periodMaxInclusive / periodMinExclusive / periodMaxExclusive
5200
+ // Schema: ( $s.1+ )+ tfn:* $o-
5201
+ const tfnPeriodKind =
5202
+ pv === TFN_NS + 'periodMinInclusive'
5203
+ ? 'minInc'
5204
+ : pv === TFN_NS + 'periodMaxInclusive'
5205
+ ? 'maxInc'
5206
+ : pv === TFN_NS + 'periodMinExclusive'
5207
+ ? 'minEx'
5208
+ : pv === TFN_NS + 'periodMaxExclusive'
5209
+ ? 'maxEx'
5210
+ : null;
5211
+ if (tfnPeriodKind) {
5212
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 1) return [];
5213
+ const arg = g.s.elems[0];
5214
+ const parts = __tfnParseTemporalLiteralParts(arg);
5215
+ if (!parts) return [];
5216
+ const bounds = __tfnComputePeriodBounds(parts);
5217
+ if (!bounds) return [];
5218
+
5219
+ let out;
5220
+ if (tfnPeriodKind === 'minInc') {
5221
+ out = __tfnMakeDateTimeLiteral({ ...bounds.startC }, bounds.tzMin);
5222
+ } else if (tfnPeriodKind === 'maxInc') {
5223
+ out = __tfnMakeDateTimeLiteral({ ...bounds.endC }, bounds.tzMax);
5224
+ } else if (tfnPeriodKind === 'minEx') {
5225
+ const c = __tfnSub1ms({ ...bounds.startC });
5226
+ out = __tfnMakeDateTimeLiteral(c, bounds.tzMin);
5227
+ } else {
5228
+ // maxEx
5229
+ const c = __tfnAdd1ms({ ...bounds.endC });
5230
+ out = __tfnMakeDateTimeLiteral(c, bounds.tzMax);
5231
+ }
5232
+
5233
+ if (g.o instanceof Var) {
5234
+ const s2 = { ...subst };
5235
+ s2[g.o.name] = out;
5236
+ return [s2];
5237
+ }
5238
+ if (g.o instanceof Blank) return [{ ...subst }];
5239
+
5240
+ const s2 = unifyTerm(g.o, out, subst);
5241
+ return s2 !== null ? [s2] : [];
5207
5242
  }
5208
- if (g.o instanceof Blank) return [{ ...subst }];
5209
5243
 
5210
- const s2 = unifyTerm(g.o, out, subst);
5211
- return s2 !== null ? [s2] : [];
5212
- }
5244
+ // tfn:bindDefaultTimezone
5245
+ // Schema: ( $s.1+ $s.2+ )+ tfn:bindDefaultTimezone $o-
5246
+ if (pv === TFN_NS + 'bindDefaultTimezone') {
5247
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
5248
+ const [timeLit, tzLit] = g.s.elems;
5249
+ const out = __tfnBindDefaultTimezone(timeLit, tzLit);
5250
+ if (!out) return [];
5213
5251
 
5214
- // tfn:bindDefaultTimezone
5215
- // Schema: ( $s.1+ $s.2+ )+ tfn:bindDefaultTimezone $o-
5216
- if (pv === TFN_NS + 'bindDefaultTimezone') {
5217
- if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
5218
- const [timeLit, tzLit] = g.s.elems;
5219
- const out = __tfnBindDefaultTimezone(timeLit, tzLit);
5220
- if (!out) return [];
5252
+ if (g.o instanceof Var) {
5253
+ const s2 = { ...subst };
5254
+ s2[g.o.name] = out;
5255
+ return [s2];
5256
+ }
5257
+ if (g.o instanceof Blank) return [{ ...subst }];
5221
5258
 
5222
- if (g.o instanceof Var) {
5223
- const s2 = { ...subst };
5224
- s2[g.o.name] = out;
5225
- return [s2];
5259
+ const s2 = unifyTerm(g.o, out, subst);
5260
+ return s2 !== null ? [s2] : [];
5226
5261
  }
5227
- if (g.o instanceof Blank) return [{ ...subst }];
5228
-
5229
- const s2 = unifyTerm(g.o, out, subst);
5230
- return s2 !== null ? [s2] : [];
5231
- }
5232
5262
 
5233
5263
  // -----------------------------------------------------------------
5234
5264
  // 4.4 list: builtins
@@ -6879,7 +6909,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
6879
6909
  if (allKnown) continue;
6880
6910
  }
6881
6911
 
6882
- const maxSols = (r.isFuse || headIsStrictGround) ? 1 : undefined;
6912
+ const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
6883
6913
  const sols = proveGoals(r.premise.slice(), empty, facts, backRules, 0, visited, varGen, maxSols);
6884
6914
 
6885
6915
  // Inference fuse
@@ -7397,7 +7427,17 @@ try {
7397
7427
 
7398
7428
  function main() {
7399
7429
  // Drop "node" and script name; keep only user-provided args
7400
- const argv = process.argv.slice(2);
7430
+ // Expand combined short options: -pt == -p -t
7431
+ const argvRaw = process.argv.slice(2);
7432
+ const argv = [];
7433
+ for (const a of argvRaw) {
7434
+ if (a === '-' || !a.startsWith('-') || a.startsWith('--') || a.length === 2) {
7435
+ argv.push(a);
7436
+ continue;
7437
+ }
7438
+ // Combined short flags (no flag in eyeling takes a value)
7439
+ for (const ch of a.slice(1)) argv.push('-' + ch);
7440
+ }
7401
7441
  const prog = String(process.argv[1] || 'eyeling')
7402
7442
  .split(/[\/]/)
7403
7443
  .pop();
@@ -7406,14 +7446,15 @@ function main() {
7406
7446
  const msg =
7407
7447
  `Usage: ${prog} [options] <file.n3>\n\n` +
7408
7448
  `Options:\n` +
7449
+ ` -a, --ast Print parsed AST as JSON and exit.\n` +
7450
+ ` -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
7409
7451
  ` -h, --help Show this help and exit.\n` +
7410
- ` -v, --version Print version and exit.\n` +
7411
- ` -p, --proof-comments Enable proof explanations.\n` +
7412
7452
  ` -n, --no-proof-comments Disable proof explanations (default).\n` +
7453
+ ` -p, --proof-comments Enable proof explanations.\n` +
7454
+ ` -r, --strings Print log:outputString strings (ordered by key) instead of N3 output.\n` +
7413
7455
  ` -s, --super-restricted Disable all builtins except => and <=.\n` +
7414
- ` -a, --ast Print parsed AST as JSON and exit.\n` +
7415
- ` --strings Print log:outputString strings (ordered by key) instead of N3 output.\n` +
7416
- ` --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n`;
7456
+ ` -t, --stream Stream derived triples as soon as they are derived.\n` +
7457
+ ` -v, --version Print version and exit.\n`;
7417
7458
  (toStderr ? console.error : console.log)(msg);
7418
7459
  }
7419
7460
 
@@ -7434,10 +7475,11 @@ function main() {
7434
7475
 
7435
7476
  const showAst = argv.includes('--ast') || argv.includes('-a');
7436
7477
 
7437
- const outputStringsMode = argv.includes('--strings');
7478
+ const outputStringsMode = argv.includes('--strings') || argv.includes('-r');
7479
+ const streamMode = argv.includes('--stream') || argv.includes('-t');
7438
7480
 
7439
7481
  // --enforce-https: rewrite http:// -> https:// for log dereferencing builtins
7440
- if (argv.includes('--enforce-https')) {
7482
+ if (argv.includes('--enforce-https') || argv.includes('-e')) {
7441
7483
  enforceHttpsEnabled = true;
7442
7484
  }
7443
7485
 
@@ -7517,13 +7559,133 @@ function main() {
7517
7559
  materializeRdfLists(triples, frules, brules);
7518
7560
 
7519
7561
  const facts = triples.filter((tr) => isGroundTriple(tr));
7520
- const derived = forwardChain(facts, frules, brules);
7562
+
7521
7563
  // If requested, print log:outputString values (ordered by subject key) and exit.
7564
+ // Note: log:outputString values may depend on derived facts, so we must saturate first.
7522
7565
  if (outputStringsMode) {
7566
+ forwardChain(facts, frules, brules);
7523
7567
  const out = __collectOutputStringsFromFacts(facts, prefixes);
7524
7568
  if (out) process.stdout.write(out);
7525
7569
  process.exit(0);
7526
7570
  }
7571
+
7572
+ // In --stream mode we print prefixes *before* any derivations happen.
7573
+ // To keep the header small and stable, emit only prefixes that are actually
7574
+ // used (as QNames) in the *input* N3 program.
7575
+ function prefixesUsedInInputTokens(toks2, prefEnv) {
7576
+ const used = new Set();
7577
+
7578
+ function maybeAddFromQName(name) {
7579
+ if (typeof name !== 'string') return;
7580
+ if (!name.includes(':')) return;
7581
+ if (name.startsWith('_:')) return; // blank node
7582
+
7583
+ // Split only on the first ':'
7584
+ const idx = name.indexOf(':');
7585
+ const p = name.slice(0, idx); // may be '' for ":foo"
7586
+
7587
+ // Ignore things like "http://..." unless that prefix is actually defined.
7588
+ if (!Object.prototype.hasOwnProperty.call(prefEnv.map, p)) return;
7589
+
7590
+ used.add(p);
7591
+ }
7592
+
7593
+ for (let i = 0; i < toks2.length; i++) {
7594
+ const t = toks2[i];
7595
+
7596
+ // Skip @prefix ... .
7597
+ if (t.typ === 'AtPrefix') {
7598
+ // @prefix <pfx:> <iri> .
7599
+ // We skip the directive itself so declared-but-unused prefixes don't count.
7600
+ // Advance until we pass the terminating dot (if present).
7601
+ while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
7602
+ continue;
7603
+ }
7604
+ // Skip @base ... .
7605
+ if (t.typ === 'AtBase') {
7606
+ while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
7607
+ continue;
7608
+ }
7609
+
7610
+ // Skip SPARQL/Turtle PREFIX pfx: <iri>
7611
+ if (
7612
+ t.typ === 'Ident' &&
7613
+ typeof t.value === 'string' &&
7614
+ t.value.toLowerCase() === 'prefix' &&
7615
+ toks2[i + 1] &&
7616
+ toks2[i + 1].typ === 'Ident' &&
7617
+ typeof toks2[i + 1].value === 'string' &&
7618
+ toks2[i + 1].value.endsWith(':') &&
7619
+ toks2[i + 2] &&
7620
+ (toks2[i + 2].typ === 'IriRef' || toks2[i + 2].typ === 'Ident')
7621
+ ) {
7622
+ // Consume PREFIX <pfx:> <iri>
7623
+ i += 2;
7624
+ continue;
7625
+ }
7626
+ // Skip SPARQL BASE <iri>
7627
+ if (
7628
+ t.typ === 'Ident' &&
7629
+ typeof t.value === 'string' &&
7630
+ t.value.toLowerCase() === 'base' &&
7631
+ toks2[i + 1] &&
7632
+ toks2[i + 1].typ === 'IriRef'
7633
+ ) {
7634
+ i += 1;
7635
+ continue;
7636
+ }
7637
+
7638
+ // Count QNames in identifiers (including datatypes like xsd:integer).
7639
+ if (t.typ === 'Ident') {
7640
+ maybeAddFromQName(t.value);
7641
+ }
7642
+ }
7643
+
7644
+ return used;
7645
+ }
7646
+
7647
+ function restrictPrefixEnv(prefEnv, usedSet) {
7648
+ const m = {};
7649
+ for (const p of usedSet) {
7650
+ if (Object.prototype.hasOwnProperty.call(prefEnv.map, p)) {
7651
+ m[p] = prefEnv.map[p];
7652
+ }
7653
+ }
7654
+ return new PrefixEnv(m, prefEnv.baseIri || '');
7655
+ }
7656
+
7657
+ // Streaming mode: print (input) prefixes first, then print derived triples as soon as they are found.
7658
+ if (streamMode) {
7659
+ const usedInInput = prefixesUsedInInputTokens(toks, prefixes);
7660
+ const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
7661
+
7662
+ // Ensure log:trace uses the same compact prefix set as the output.
7663
+ __tracePrefixes = outPrefixes;
7664
+
7665
+ const entries = Object.entries(outPrefixes.map)
7666
+ .filter(([_p, base]) => !!base)
7667
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
7668
+
7669
+ for (const [pfx, base] of entries) {
7670
+ if (pfx === '') console.log(`@prefix : <${base}> .`);
7671
+ else console.log(`@prefix ${pfx}: <${base}> .`);
7672
+ }
7673
+ if (entries.length) console.log();
7674
+
7675
+ forwardChain(facts, frules, brules, (df) => {
7676
+ if (proofCommentsEnabled) {
7677
+ printExplanation(df, outPrefixes);
7678
+ console.log(tripleToN3(df.fact, outPrefixes));
7679
+ console.log();
7680
+ } else {
7681
+ console.log(tripleToN3(df.fact, outPrefixes));
7682
+ }
7683
+ });
7684
+ return;
7685
+ }
7686
+
7687
+ // Default (non-streaming): derive everything first, then print only the newly derived facts.
7688
+ const derived = forwardChain(facts, frules, brules);
7527
7689
  const derivedTriples = derived.map((df) => df.fact);
7528
7690
  const usedPrefixes = prefixes.prefixesUsedForOutput(derivedTriples);
7529
7691
 
package/index.js CHANGED
@@ -22,7 +22,11 @@ function reason(opt = {}, n3_input = '') {
22
22
  const proofCommentsSpecified = typeof opt.proofComments === 'boolean' || typeof opt.noProofComments === 'boolean';
23
23
 
24
24
  const proofComments =
25
- typeof opt.proofComments === 'boolean' ? opt.proofComments : typeof opt.noProofComments === 'boolean' ? !opt.noProofComments : false;
25
+ typeof opt.proofComments === 'boolean'
26
+ ? opt.proofComments
27
+ : typeof opt.noProofComments === 'boolean'
28
+ ? !opt.noProofComments
29
+ : false;
26
30
 
27
31
  // Only pass a flag when the caller explicitly asked.
28
32
  // (CLI default is now: no proof comments.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.7.20",
3
+ "version": "1.8.1",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -2,9 +2,14 @@
2
2
 
3
3
  const assert = require('node:assert/strict');
4
4
  const { reason } = require('..');
5
+ // Direct eyeling.js API (in-process) for testing reasonStream/onDerived.
6
+ // This is the "latest eyeling.js" surface and is used by the browser demo.
7
+ const { reasonStream } = require('../eyeling.js');
5
8
 
6
9
  const TTY = process.stdout.isTTY;
7
- const C = TTY ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' } : { g: '', r: '', y: '', dim: '', n: '' };
10
+ const C = TTY
11
+ ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
12
+ : { g: '', r: '', y: '', dim: '', n: '' };
8
13
 
9
14
  function ok(msg) {
10
15
  console.log(`${C.g}OK${C.n} ${msg}`);
@@ -350,7 +355,10 @@ ${U('a')} ${U('p')} ${U('b')}.
350
355
  name: '17 heavier reachability: branching graph reach closure',
351
356
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
352
357
  input: reachabilityGraphN3(12),
353
- expect: [new RegExp(`${EX}g0>\\s+<${EX}reach>\\s+<${EX}g12>\\s*\\.`), new RegExp(`${EX}g2>\\s+<${EX}reach>\\s+<${EX}g10>\\s*\\.`)],
358
+ expect: [
359
+ new RegExp(`${EX}g0>\\s+<${EX}reach>\\s+<${EX}g12>\\s*\\.`),
360
+ new RegExp(`${EX}g2>\\s+<${EX}reach>\\s+<${EX}g10>\\s*\\.`),
361
+ ],
354
362
  },
355
363
  {
356
364
  name: '18 heavier taxonomy: diamond subclass inference',
@@ -378,7 +386,10 @@ ${U('a')} ${U('p')} ${U('b')}.
378
386
  name: '21 heavier equivalence: sameAs propagation (with symmetric sameAs)',
379
387
  opt: { proofComments: false },
380
388
  input: sameAsN3(),
381
- expect: [new RegExp(`${EX}b>\\s+<${EX}p>\\s+<${EX}o>\\s*\\.`), new RegExp(`${EX}b>\\s+<${EX}sameAs>\\s+<${EX}a>\\s*\\.`)],
389
+ expect: [
390
+ new RegExp(`${EX}b>\\s+<${EX}p>\\s+<${EX}o>\\s*\\.`),
391
+ new RegExp(`${EX}b>\\s+<${EX}sameAs>\\s+<${EX}a>\\s*\\.`),
392
+ ],
382
393
  },
383
394
  {
384
395
  name: '22 heavier closure: transitive property via generic rule',
@@ -390,7 +401,10 @@ ${U('c')} ${U('sub')} ${U('d')}.
390
401
  ${U('d')} ${U('sub')} ${U('e')}.
391
402
  ${transitiveClosureN3('sub')}
392
403
  `,
393
- expect: [new RegExp(`${EX}a>\\s+<${EX}sub>\\s+<${EX}e>\\s*\\.`), new RegExp(`${EX}b>\\s+<${EX}sub>\\s+<${EX}d>\\s*\\.`)],
404
+ expect: [
405
+ new RegExp(`${EX}a>\\s+<${EX}sub>\\s+<${EX}e>\\s*\\.`),
406
+ new RegExp(`${EX}b>\\s+<${EX}sub>\\s+<${EX}d>\\s*\\.`),
407
+ ],
394
408
  },
395
409
  {
396
410
  name: '23 heavier social: symmetric + reachFriend closure',
@@ -405,7 +419,10 @@ ${transitiveClosureN3('sub')}
405
419
  name: '24 heavier volume: 400 facts, simple rewrite rule p -> q',
406
420
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
407
421
  input: bigFactsN3(400),
408
- expect: [new RegExp(`${EX}x>\\s+<${EX}q>\\s+<${EX}o0>\\s*\\.`), new RegExp(`${EX}x>\\s+<${EX}q>\\s+<${EX}o399>\\s*\\.`)],
422
+ expect: [
423
+ new RegExp(`${EX}x>\\s+<${EX}q>\\s+<${EX}o0>\\s*\\.`),
424
+ new RegExp(`${EX}x>\\s+<${EX}q>\\s+<${EX}o399>\\s*\\.`),
425
+ ],
409
426
  },
410
427
  {
411
428
  name: '25 heavier negative entailment: batch + forbidden => false (expect exit 2)',
@@ -491,7 +508,10 @@ ${U('c')} ${U('p')} ${U('d')}.
491
508
 
492
509
  { ?s ${U('p')} ?o. } => { ?s ${U('q')} ?o. }.
493
510
  `,
494
- expect: [new RegExp(`${EX}a>\\s+<${EX}q>\\s+<${EX}b>\\s*\\.`), new RegExp(`${EX}c>\\s+<${EX}q>\\s+<${EX}d>\\s*\\.`)],
511
+ expect: [
512
+ new RegExp(`${EX}a>\\s+<${EX}q>\\s+<${EX}b>\\s*\\.`),
513
+ new RegExp(`${EX}c>\\s+<${EX}q>\\s+<${EX}d>\\s*\\.`),
514
+ ],
495
515
  },
496
516
 
497
517
  {
@@ -537,7 +557,10 @@ ${U('s')} ${U('p')} ${U('o')}.
537
557
 
538
558
  { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. ${U('s')} ${U('r')} ${U('o')}. }.
539
559
  `,
540
- expect: [new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`), new RegExp(`${EX}s>\\s+<${EX}r>\\s+<${EX}o>\\s*\\.`)],
560
+ expect: [
561
+ new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`),
562
+ new RegExp(`${EX}s>\\s+<${EX}r>\\s+<${EX}o>\\s*\\.`),
563
+ ],
541
564
  },
542
565
 
543
566
  {
@@ -687,6 +710,104 @@ _:l2 rdf:rest rdf:nil.
687
710
  new RegExp(`${EX}s>\\s+<${EX}q3>\\s+<${EX}b>\\s*\\.`),
688
711
  ],
689
712
  },
713
+
714
+ // -------------------------
715
+ // Newer eyeling.js features
716
+ // -------------------------
717
+
718
+ {
719
+ name: '51 --strings: prints log:outputString values ordered by key (subject)',
720
+ opt: ['--strings', '-n'],
721
+ input: `@prefix log: <http://www.w3.org/2000/10/swap/log#>.
722
+
723
+ <http://example.org/2> log:outputString "B".
724
+ <http://example.org/1> log:outputString "A".
725
+ `,
726
+ // CLI prints concatenated strings and exits.
727
+ check(out) {
728
+ assert.equal(String(out).trimEnd(), 'AB');
729
+ },
730
+ },
731
+
732
+ {
733
+ name: '52 --ast: prints parse result as JSON array [prefixes, triples, frules, brules]',
734
+ opt: ['--ast'],
735
+ input: `@prefix ex: <http://example.org/>.
736
+ ex:s ex:p ex:o.
737
+ `,
738
+ expect: [/^\s*\[/m],
739
+ check(out) {
740
+ const v = JSON.parse(String(out));
741
+ assert.ok(Array.isArray(v), 'AST output should be a JSON array');
742
+ assert.equal(v.length, 4, 'AST output should have 4 top-level elements');
743
+ // The second element is the parsed triples array.
744
+ assert.ok(Array.isArray(v[1]), 'AST[1] (triples) should be an array');
745
+ },
746
+ },
747
+
748
+ {
749
+ name: '53 --stream: prints prefixes used in input (not just derived output) before streaming triples',
750
+ opt: ['--stream', '-n'],
751
+ input: `@prefix ex: <http://example.org/>.
752
+ @prefix p: <http://premise.example/>.
753
+ @prefix unused: <http://unused.example/>.
754
+
755
+ ex:a p:trig ex:b.
756
+ { ?s p:trig ?o. } => { ?s ex:q ?o. }.
757
+ `,
758
+ expect: [
759
+ /@prefix\s+ex:\s+<http:\/\/example\.org\/>\s*\./m,
760
+ /@prefix\s+p:\s+<http:\/\/premise\.example\/>\s*\./m,
761
+ /(?:ex:a|<http:\/\/example\.org\/a>)\s+(?:ex:q|<http:\/\/example\.org\/q>)\s+(?:ex:b|<http:\/\/example\.org\/b>)\s*\./m,
762
+ ],
763
+ notExpect: [/@prefix\s+unused:/m, /^#/m],
764
+ check(out) {
765
+ const lines = String(out).split(/\r?\n/);
766
+ const firstNonPrefix = lines.findIndex((l) => {
767
+ const t = l.trim();
768
+ return t && !t.startsWith('@prefix');
769
+ });
770
+ assert.ok(firstNonPrefix > 0, 'Expected at least one @prefix line before the first triple');
771
+ for (let i = 0; i < firstNonPrefix; i++) {
772
+ const t = lines[i].trim();
773
+ if (!t) continue;
774
+ assert.ok(t.startsWith('@prefix'), `Non-prefix line found before first triple: ${lines[i]}`);
775
+ }
776
+ },
777
+ },
778
+
779
+ {
780
+ name: '54 reasonStream: onDerived callback fires and includeInputFactsInClosure=false excludes input facts',
781
+ run() {
782
+ const input = `
783
+ { <http://example.org/s> <http://example.org/p> <http://example.org/o>. }
784
+ => { <http://example.org/s> <http://example.org/q> <http://example.org/o>. }.
785
+
786
+ <http://example.org/s> <http://example.org/p> <http://example.org/o>.
787
+ `;
788
+
789
+ const seen = [];
790
+ const r = reasonStream(input, {
791
+ proof: false,
792
+ includeInputFactsInClosure: false,
793
+ onDerived: ({ triple }) => seen.push(triple),
794
+ });
795
+
796
+ // stash for check()
797
+ this._seen = seen;
798
+ this._result = r;
799
+ return r.closureN3;
800
+ },
801
+ expect: [/http:\/\/example\.org\/q/m],
802
+ notExpect: [/http:\/\/example\.org\/p/m],
803
+ check(out, tc) {
804
+ assert.equal(tc._seen.length, 1, 'Expected onDerived to be called once');
805
+ assert.match(tc._seen[0], /http:\/\/example\.org\/q/, 'Expected streamed triple to be the derived one');
806
+ // closureN3 should be exactly the derived triple (no input facts).
807
+ assert.ok(String(out).trim().includes('http://example.org/q'));
808
+ assert.ok(!String(out).includes('http://example.org/p'));
809
+ },
810
+ },
690
811
  ];
691
812
 
692
813
  let passed = 0;
@@ -699,7 +820,7 @@ let failed = 0;
699
820
  for (const tc of cases) {
700
821
  const start = msNow();
701
822
  try {
702
- const out = reason(tc.opt, tc.input);
823
+ const out = typeof tc.run === 'function' ? await tc.run() : reason(tc.opt, tc.input);
703
824
 
704
825
  if (tc.expectErrorCode != null || tc.expectError) {
705
826
  throw new Error(`Expected an error, but reason() returned output:\n${out}`);
@@ -708,7 +829,7 @@ let failed = 0;
708
829
  for (const re of tc.expect || []) mustMatch(out, re, `${tc.name}: missing expected pattern ${re}`);
709
830
  for (const re of tc.notExpect || []) mustNotMatch(out, re, `${tc.name}: unexpected pattern ${re}`);
710
831
 
711
- if (typeof tc.check === 'function') tc.check(out);
832
+ if (typeof tc.check === 'function') tc.check(out, tc);
712
833
 
713
834
  const dur = msNow() - start;
714
835
  ok(`${tc.name} ${C.dim}(${dur} ms)${C.n}`);
@@ -7,7 +7,9 @@ const path = require('node:path');
7
7
  const cp = require('node:child_process');
8
8
 
9
9
  const TTY = process.stdout.isTTY;
10
- const C = TTY ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' } : { g: '', r: '', y: '', dim: '', n: '' };
10
+ const C = TTY
11
+ ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
12
+ : { g: '', r: '', y: '', dim: '', n: '' };
11
13
  const msTag = (ms) => `${C.dim}(${ms} ms)${C.n}`;
12
14
 
13
15
  function ok(msg) {
@@ -7,7 +7,9 @@ const path = require('node:path');
7
7
  const cp = require('node:child_process');
8
8
 
9
9
  const TTY = process.stdout.isTTY;
10
- const C = TTY ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' } : { g: '', r: '', y: '', dim: '', n: '' };
10
+ const C = TTY
11
+ ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
12
+ : { g: '', r: '', y: '', dim: '', n: '' };
11
13
 
12
14
  function info(msg) {
13
15
  console.log(`${C.y}==${C.n} ${msg}`);
@@ -110,7 +112,9 @@ function main() {
110
112
  ok('API works');
111
113
 
112
114
  info('CLI smoke test');
113
- const bin = isWin() ? path.join(tmp, 'node_modules', '.bin', 'eyeling.cmd') : path.join(tmp, 'node_modules', '.bin', 'eyeling');
115
+ const bin = isWin()
116
+ ? path.join(tmp, 'node_modules', '.bin', 'eyeling.cmd')
117
+ : path.join(tmp, 'node_modules', '.bin', 'eyeling');
114
118
  runChecked(bin, ['-v'], { cwd: tmp, stdio: 'inherit' });
115
119
  ok('CLI works');
116
120