eyeling 1.7.20 → 1.8.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/README.md CHANGED
@@ -122,6 +122,7 @@ Options:
122
122
  -a, --ast Print parsed AST as JSON and exit.
123
123
  --strings Print log:outputString strings (ordered by key) instead of N3 output.
124
124
  --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.
125
+ --stream Stream derived triples as soon as they are derived.
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
@@ -7413,7 +7443,8 @@ function main() {
7413
7443
  ` -s, --super-restricted Disable all builtins except => and <=.\n` +
7414
7444
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
7415
7445
  ` --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`;
7446
+ ` --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
7447
+ ` --stream Stream derived triples as soon as they are derived.\n`;
7417
7448
  (toStderr ? console.error : console.log)(msg);
7418
7449
  }
7419
7450
 
@@ -7435,6 +7466,7 @@ function main() {
7435
7466
  const showAst = argv.includes('--ast') || argv.includes('-a');
7436
7467
 
7437
7468
  const outputStringsMode = argv.includes('--strings');
7469
+ const streamMode = argv.includes('--stream');
7438
7470
 
7439
7471
  // --enforce-https: rewrite http:// -> https:// for log dereferencing builtins
7440
7472
  if (argv.includes('--enforce-https')) {
@@ -7517,13 +7549,133 @@ function main() {
7517
7549
  materializeRdfLists(triples, frules, brules);
7518
7550
 
7519
7551
  const facts = triples.filter((tr) => isGroundTriple(tr));
7520
- const derived = forwardChain(facts, frules, brules);
7552
+
7521
7553
  // If requested, print log:outputString values (ordered by subject key) and exit.
7554
+ // Note: log:outputString values may depend on derived facts, so we must saturate first.
7522
7555
  if (outputStringsMode) {
7556
+ forwardChain(facts, frules, brules);
7523
7557
  const out = __collectOutputStringsFromFacts(facts, prefixes);
7524
7558
  if (out) process.stdout.write(out);
7525
7559
  process.exit(0);
7526
7560
  }
7561
+
7562
+ // In --stream mode we print prefixes *before* any derivations happen.
7563
+ // To keep the header small and stable, emit only prefixes that are actually
7564
+ // used (as QNames) in the *input* N3 program.
7565
+ function prefixesUsedInInputTokens(toks2, prefEnv) {
7566
+ const used = new Set();
7567
+
7568
+ function maybeAddFromQName(name) {
7569
+ if (typeof name !== 'string') return;
7570
+ if (!name.includes(':')) return;
7571
+ if (name.startsWith('_:')) return; // blank node
7572
+
7573
+ // Split only on the first ':'
7574
+ const idx = name.indexOf(':');
7575
+ const p = name.slice(0, idx); // may be '' for ":foo"
7576
+
7577
+ // Ignore things like "http://..." unless that prefix is actually defined.
7578
+ if (!Object.prototype.hasOwnProperty.call(prefEnv.map, p)) return;
7579
+
7580
+ used.add(p);
7581
+ }
7582
+
7583
+ for (let i = 0; i < toks2.length; i++) {
7584
+ const t = toks2[i];
7585
+
7586
+ // Skip @prefix ... .
7587
+ if (t.typ === 'AtPrefix') {
7588
+ // @prefix <pfx:> <iri> .
7589
+ // We skip the directive itself so declared-but-unused prefixes don't count.
7590
+ // Advance until we pass the terminating dot (if present).
7591
+ while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
7592
+ continue;
7593
+ }
7594
+ // Skip @base ... .
7595
+ if (t.typ === 'AtBase') {
7596
+ while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
7597
+ continue;
7598
+ }
7599
+
7600
+ // Skip SPARQL/Turtle PREFIX pfx: <iri>
7601
+ if (
7602
+ t.typ === 'Ident' &&
7603
+ typeof t.value === 'string' &&
7604
+ t.value.toLowerCase() === 'prefix' &&
7605
+ toks2[i + 1] &&
7606
+ toks2[i + 1].typ === 'Ident' &&
7607
+ typeof toks2[i + 1].value === 'string' &&
7608
+ toks2[i + 1].value.endsWith(':') &&
7609
+ toks2[i + 2] &&
7610
+ (toks2[i + 2].typ === 'IriRef' || toks2[i + 2].typ === 'Ident')
7611
+ ) {
7612
+ // Consume PREFIX <pfx:> <iri>
7613
+ i += 2;
7614
+ continue;
7615
+ }
7616
+ // Skip SPARQL BASE <iri>
7617
+ if (
7618
+ t.typ === 'Ident' &&
7619
+ typeof t.value === 'string' &&
7620
+ t.value.toLowerCase() === 'base' &&
7621
+ toks2[i + 1] &&
7622
+ toks2[i + 1].typ === 'IriRef'
7623
+ ) {
7624
+ i += 1;
7625
+ continue;
7626
+ }
7627
+
7628
+ // Count QNames in identifiers (including datatypes like xsd:integer).
7629
+ if (t.typ === 'Ident') {
7630
+ maybeAddFromQName(t.value);
7631
+ }
7632
+ }
7633
+
7634
+ return used;
7635
+ }
7636
+
7637
+ function restrictPrefixEnv(prefEnv, usedSet) {
7638
+ const m = {};
7639
+ for (const p of usedSet) {
7640
+ if (Object.prototype.hasOwnProperty.call(prefEnv.map, p)) {
7641
+ m[p] = prefEnv.map[p];
7642
+ }
7643
+ }
7644
+ return new PrefixEnv(m, prefEnv.baseIri || '');
7645
+ }
7646
+
7647
+ // Streaming mode: print (input) prefixes first, then print derived triples as soon as they are found.
7648
+ if (streamMode) {
7649
+ const usedInInput = prefixesUsedInInputTokens(toks, prefixes);
7650
+ const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
7651
+
7652
+ // Ensure log:trace uses the same compact prefix set as the output.
7653
+ __tracePrefixes = outPrefixes;
7654
+
7655
+ const entries = Object.entries(outPrefixes.map)
7656
+ .filter(([_p, base]) => !!base)
7657
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
7658
+
7659
+ for (const [pfx, base] of entries) {
7660
+ if (pfx === '') console.log(`@prefix : <${base}> .`);
7661
+ else console.log(`@prefix ${pfx}: <${base}> .`);
7662
+ }
7663
+ if (entries.length) console.log();
7664
+
7665
+ forwardChain(facts, frules, brules, (df) => {
7666
+ if (proofCommentsEnabled) {
7667
+ printExplanation(df, outPrefixes);
7668
+ console.log(tripleToN3(df.fact, outPrefixes));
7669
+ console.log();
7670
+ } else {
7671
+ console.log(tripleToN3(df.fact, outPrefixes));
7672
+ }
7673
+ });
7674
+ return;
7675
+ }
7676
+
7677
+ // Default (non-streaming): derive everything first, then print only the newly derived facts.
7678
+ const derived = forwardChain(facts, frules, brules);
7527
7679
  const derivedTriples = derived.map((df) => df.fact);
7528
7680
  const usedPrefixes = prefixes.prefixesUsedForOutput(derivedTriples);
7529
7681
 
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.0",
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,6 +2,9 @@
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
10
  const C = TTY ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' } : { g: '', r: '', y: '', dim: '', n: '' };
@@ -687,6 +690,104 @@ _:l2 rdf:rest rdf:nil.
687
690
  new RegExp(`${EX}s>\\s+<${EX}q3>\\s+<${EX}b>\\s*\\.`),
688
691
  ],
689
692
  },
693
+
694
+ // -------------------------
695
+ // Newer eyeling.js features
696
+ // -------------------------
697
+
698
+ {
699
+ name: '51 --strings: prints log:outputString values ordered by key (subject)',
700
+ opt: ['--strings', '-n'],
701
+ input: `@prefix log: <http://www.w3.org/2000/10/swap/log#>.
702
+
703
+ <http://example.org/2> log:outputString "B".
704
+ <http://example.org/1> log:outputString "A".
705
+ `,
706
+ // CLI prints concatenated strings and exits.
707
+ check(out) {
708
+ assert.equal(String(out).trimEnd(), 'AB');
709
+ },
710
+ },
711
+
712
+ {
713
+ name: '52 --ast: prints parse result as JSON array [prefixes, triples, frules, brules]',
714
+ opt: ['--ast'],
715
+ input: `@prefix ex: <http://example.org/>.
716
+ ex:s ex:p ex:o.
717
+ `,
718
+ expect: [/^\s*\[/m],
719
+ check(out) {
720
+ const v = JSON.parse(String(out));
721
+ assert.ok(Array.isArray(v), 'AST output should be a JSON array');
722
+ assert.equal(v.length, 4, 'AST output should have 4 top-level elements');
723
+ // The second element is the parsed triples array.
724
+ assert.ok(Array.isArray(v[1]), 'AST[1] (triples) should be an array');
725
+ },
726
+ },
727
+
728
+ {
729
+ name: '53 --stream: prints prefixes used in input (not just derived output) before streaming triples',
730
+ opt: ['--stream', '-n'],
731
+ input: `@prefix ex: <http://example.org/>.
732
+ @prefix p: <http://premise.example/>.
733
+ @prefix unused: <http://unused.example/>.
734
+
735
+ ex:a p:trig ex:b.
736
+ { ?s p:trig ?o. } => { ?s ex:q ?o. }.
737
+ `,
738
+ expect: [
739
+ /@prefix\s+ex:\s+<http:\/\/example\.org\/>\s*\./m,
740
+ /@prefix\s+p:\s+<http:\/\/premise\.example\/>\s*\./m,
741
+ /(?:ex:a|<http:\/\/example\.org\/a>)\s+(?:ex:q|<http:\/\/example\.org\/q>)\s+(?:ex:b|<http:\/\/example\.org\/b>)\s*\./m,
742
+ ],
743
+ notExpect: [/@prefix\s+unused:/m, /^#/m],
744
+ check(out) {
745
+ const lines = String(out).split(/\r?\n/);
746
+ const firstNonPrefix = lines.findIndex((l) => {
747
+ const t = l.trim();
748
+ return t && !t.startsWith('@prefix');
749
+ });
750
+ assert.ok(firstNonPrefix > 0, 'Expected at least one @prefix line before the first triple');
751
+ for (let i = 0; i < firstNonPrefix; i++) {
752
+ const t = lines[i].trim();
753
+ if (!t) continue;
754
+ assert.ok(t.startsWith('@prefix'), `Non-prefix line found before first triple: ${lines[i]}`);
755
+ }
756
+ },
757
+ },
758
+
759
+ {
760
+ name: '54 reasonStream: onDerived callback fires and includeInputFactsInClosure=false excludes input facts',
761
+ run() {
762
+ const input = `
763
+ { <http://example.org/s> <http://example.org/p> <http://example.org/o>. }
764
+ => { <http://example.org/s> <http://example.org/q> <http://example.org/o>. }.
765
+
766
+ <http://example.org/s> <http://example.org/p> <http://example.org/o>.
767
+ `;
768
+
769
+ const seen = [];
770
+ const r = reasonStream(input, {
771
+ proof: false,
772
+ includeInputFactsInClosure: false,
773
+ onDerived: ({ triple }) => seen.push(triple),
774
+ });
775
+
776
+ // stash for check()
777
+ this._seen = seen;
778
+ this._result = r;
779
+ return r.closureN3;
780
+ },
781
+ expect: [/http:\/\/example\.org\/q/m],
782
+ notExpect: [/http:\/\/example\.org\/p/m],
783
+ check(out, tc) {
784
+ assert.equal(tc._seen.length, 1, 'Expected onDerived to be called once');
785
+ assert.match(tc._seen[0], /http:\/\/example\.org\/q/, 'Expected streamed triple to be the derived one');
786
+ // closureN3 should be exactly the derived triple (no input facts).
787
+ assert.ok(String(out).trim().includes('http://example.org/q'));
788
+ assert.ok(!String(out).includes('http://example.org/p'));
789
+ },
790
+ },
690
791
  ];
691
792
 
692
793
  let passed = 0;
@@ -699,7 +800,7 @@ let failed = 0;
699
800
  for (const tc of cases) {
700
801
  const start = msNow();
701
802
  try {
702
- const out = reason(tc.opt, tc.input);
803
+ const out = typeof tc.run === 'function' ? await tc.run() : reason(tc.opt, tc.input);
703
804
 
704
805
  if (tc.expectErrorCode != null || tc.expectError) {
705
806
  throw new Error(`Expected an error, but reason() returned output:\n${out}`);
@@ -708,7 +809,7 @@ let failed = 0;
708
809
  for (const re of tc.expect || []) mustMatch(out, re, `${tc.name}: missing expected pattern ${re}`);
709
810
  for (const re of tc.notExpect || []) mustNotMatch(out, re, `${tc.name}: unexpected pattern ${re}`);
710
811
 
711
- if (typeof tc.check === 'function') tc.check(out);
812
+ if (typeof tc.check === 'function') tc.check(out, tc);
712
813
 
713
814
  const dur = msNow() - start;
714
815
  ok(`${tc.name} ${C.dim}(${dur} ms)${C.n}`);