eyeling 1.7.19 → 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
@@ -21,7 +21,7 @@ Try it here:
21
21
  - Load an N3 program from a URL (in the "Load N3 from URL" box or as ?url=...).
22
22
  - Share a link with the program encoded in the URL fragment (`#...`).
23
23
 
24
- - [Eyeling streaming playground](https://eyereasoner.github.io/eyeling/stream)
24
+ - Streaming demo
25
25
  - Browse a Wikidata entity, load its facts, and see Eyeling’s **deductive closure appear incrementally** as triples are derived.
26
26
  - Edit **N3 rules live** and re-run to watch how different inference rules change what gets derived.
27
27
  - Demo **CORS-safe dynamic fetching**: derived “fetch requests” can trigger extra facts (e.g., Wikiquote extracts) that are injected and re-reasoned.
@@ -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`:
@@ -0,0 +1,29 @@
1
+ @prefix ex: <http://example.org/> .
2
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
3
+ @prefix tfn: <https://w3id.org/time-fn#> .
4
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
5
+
6
+ ex:e1 a ex:Event ;
7
+ ex:when "2000-01-02T03:04:05"^^xsd:dateTime ; # floating (no TZ)
8
+ ex:day "2000-01-02"^^xsd:date ;
9
+ ex:tz "Z" .
10
+ ex:e2 a ex:Event ;
11
+ ex:when "2000-01-02T03:04:05-03:00"^^xsd:dateTime ;
12
+ ex:day "2000-01-02"^^xsd:date ;
13
+ ex:tz "-03:00" .
14
+
15
+ {
16
+ ?this ex:when ?whenRaw .
17
+ ?this ex:day ?day .
18
+ ?this ex:tz ?tz .
19
+ ?startInc math:notGreaterThan ?whenBound .
20
+ ?whenBound math:notGreaterThan ?endInc .
21
+ (?whenRaw ?tz) tfn:bindDefaultTimezone ?whenBound .
22
+ (?day) tfn:periodMinInclusive ?startInc .
23
+ (?day) tfn:periodMaxInclusive ?endInc .
24
+ (?day) tfn:periodMinExclusive ?startEx .
25
+ (?day) tfn:periodMaxExclusive ?endEx .
26
+ } => {
27
+ ?this ex:occursOn ?day .
28
+ ?this ex:whenBound ?whenBound .
29
+ } .
@@ -0,0 +1,36 @@
1
+ PREFIX ex: <http://example.org/>
2
+ PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
3
+ PREFIX tfn: <https://w3id.org/time-fn#>
4
+
5
+ DATA {
6
+ ex:e1 a ex:Event ;
7
+ ex:when "2000-01-02T03:04:05"^^xsd:dateTime ; # floating (no TZ)
8
+ ex:day "2000-01-02"^^xsd:date ;
9
+ ex:tz "Z" .
10
+
11
+ ex:e2 a ex:Event ;
12
+ ex:when "2000-01-02T03:04:05-03:00"^^xsd:dateTime ;
13
+ ex:day "2000-01-02"^^xsd:date ;
14
+ ex:tz "-03:00" .
15
+ }
16
+
17
+ RULE {
18
+ $this ex:occursOn ?day .
19
+ $this ex:whenBound ?whenBound .
20
+ }
21
+ WHERE {
22
+ $this ex:when ?whenRaw ;
23
+ ex:day ?day ;
24
+ ex:tz ?tz .
25
+
26
+ BIND( tfn:bindDefaultTimezone(?whenRaw, ?tz) AS ?whenBound )
27
+
28
+ BIND( tfn:periodMinInclusive(?day) AS ?startInc )
29
+ BIND( tfn:periodMaxInclusive(?day) AS ?endInc )
30
+ BIND( tfn:periodMinExclusive(?day) AS ?startEx )
31
+ BIND( tfn:periodMaxExclusive(?day) AS ?endEx )
32
+
33
+ # Inclusive containment: start <= whenBound <= end
34
+ FILTER( ?startInc <= ?whenBound && ?whenBound <= ?endInc )
35
+ }
36
+
@@ -0,0 +1,7 @@
1
+ @prefix ex: <http://example.org/> .
2
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
3
+
4
+ ex:e1 ex:occursOn "2000-01-02"^^xsd:date .
5
+ ex:e1 ex:whenBound "2000-01-02T03:04:05Z"^^xsd:dateTime .
6
+ ex:e2 ex:occursOn "2000-01-02"^^xsd:date .
7
+ ex:e2 ex:whenBound "2000-01-02T03:04:05-03:00"^^xsd:dateTime .
@@ -8,6 +8,7 @@
8
8
  @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
9
9
  @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
10
10
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
11
+ @prefix tfn: <https://w3id.org/time-fn#> .
11
12
 
12
13
  # ------------------------------------------------------------------------------
13
14
  # Builtin catalog (eyeling.js)
@@ -336,3 +337,26 @@ string:format a ex:Builtin ; ex:kind ex:Function ;
336
337
 
337
338
  string:jsonPointer a ex:Builtin ; ex:kind ex:Function ;
338
339
  rdfs:comment "JSON Pointer lookup: (jsonText pointer) -> value term. Expects rdf:JSON-typed literal (or treated as rdf:JSON); caches parsed JSON and pointer lookups." .
340
+
341
+ # --- tfn: (Time Functions) -------------------------------------------
342
+ # Source semantics: Smessaert et al., “It’s About Time: Time Functions for Comparing Partial and Floating Time Literals in SPARQL”, §2.2.
343
+
344
+ tfn:periodMinInclusive a ex:Builtin ;
345
+ ex:kind ex:Function ;
346
+ rdfs:comment "Time Functions: inclusive lower bound of the period represented by a temporal literal (xsd:date, xsd:gYearMonth, xsd:gYear, xsd:dateTime, ...), returned as xsd:dateTime." .
347
+
348
+ tfn:periodMaxInclusive a ex:Builtin ;
349
+ ex:kind ex:Function ;
350
+ rdfs:comment "Time Functions: inclusive upper bound of the period represented by a temporal literal, returned as xsd:dateTime." .
351
+
352
+ tfn:periodMinExclusive a ex:Builtin ;
353
+ ex:kind ex:Function ;
354
+ rdfs:comment "Time Functions: exclusive lower bound of the period represented by a temporal literal, returned as xsd:dateTime." .
355
+
356
+ tfn:periodMaxExclusive a ex:Builtin ;
357
+ ex:kind ex:Function ;
358
+ rdfs:comment "Time Functions: exclusive upper bound of the period represented by a temporal literal, returned as xsd:dateTime." .
359
+
360
+ tfn:bindDefaultTimezone a ex:Builtin ;
361
+ ex:kind ex:Function ;
362
+ rdfs:comment "Time Functions: binds a default timezone to a floating time literal. Input is (timeLiteral timeZone). If the literal already has a timezone, it is returned unchanged; otherwise the specified timezone is applied." .
package/eyeling.js CHANGED
@@ -42,6 +42,7 @@ const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
42
42
  const CRYPTO_NS = 'http://www.w3.org/2000/10/swap/crypto#';
43
43
  const MATH_NS = 'http://www.w3.org/2000/10/swap/math#';
44
44
  const TIME_NS = 'http://www.w3.org/2000/10/swap/time#';
45
+ const TFN_NS = 'https://w3id.org/time-fn#';
45
46
  const LIST_NS = 'http://www.w3.org/2000/10/swap/list#';
46
47
  const LOG_NS = 'http://www.w3.org/2000/10/swap/log#';
47
48
  const STRING_NS = 'http://www.w3.org/2000/10/swap/string#';
@@ -110,9 +111,7 @@ let enforceHttpsEnabled = false;
110
111
 
111
112
  function __maybeEnforceHttps(iri) {
112
113
  if (!enforceHttpsEnabled) return iri;
113
- return typeof iri === 'string' && iri.startsWith('http://')
114
- ? 'https://' + iri.slice('http://'.length)
115
- : iri;
114
+ return typeof iri === 'string' && iri.startsWith('http://') ? 'https://' + iri.slice('http://'.length) : iri;
116
115
  }
117
116
 
118
117
  // Environment detection (Node vs Browser/Worker).
@@ -426,12 +425,7 @@ let __tracePrefixes = null;
426
425
  function __traceWriteLine(line) {
427
426
  // Prefer stderr in Node, fall back to console.error elsewhere.
428
427
  try {
429
- if (
430
- __IS_NODE &&
431
- typeof process !== 'undefined' &&
432
- process.stderr &&
433
- typeof process.stderr.write === 'function'
434
- ) {
428
+ if (__IS_NODE && typeof process !== 'undefined' && process.stderr && typeof process.stderr.write === 'function') {
435
429
  process.stderr.write(String(line) + '\n');
436
430
  return;
437
431
  }
@@ -3108,7 +3102,6 @@ function termToJsString(t) {
3108
3102
  return typeof lex === 'string' ? lex : String(lex);
3109
3103
  }
3110
3104
 
3111
-
3112
3105
  function makeStringLiteral(str) {
3113
3106
  // JSON.stringify gives us a valid N3/Turtle-style quoted string
3114
3107
  // (with proper escaping for quotes, backslashes, newlines, …).
@@ -3625,6 +3618,285 @@ function parseXsdDateTimeLexParts(t) {
3625
3618
  return { yearStr, month, day, hour, minute, second, tz };
3626
3619
  }
3627
3620
 
3621
+ // -----------------------------------------------------------------------------
3622
+ // Time Functions (tfn:) helpers
3623
+ // -----------------------------------------------------------------------------
3624
+
3625
+ function __tfnParseTimezoneValue(t) {
3626
+ // Accept plain string literals and xsd:string. Return "Z" or "+hh:mm" or "-hh:mm".
3627
+ if (!(t instanceof Literal)) return null;
3628
+ const [lex, dt] = literalParts(t.value);
3629
+ const s = stripQuotes(lex).trim();
3630
+ if (!s) return null;
3631
+ if (s === 'Z') return 'Z';
3632
+ const m = /^([+-])(\d{2}):(\d{2})$/.exec(s);
3633
+ if (!m) return null;
3634
+ const hh = parseInt(m[2], 10);
3635
+ const mm = parseInt(m[3], 10);
3636
+ if (!(hh >= 0 && hh <= 14)) return null;
3637
+ if (!(mm >= 0 && mm <= 59)) return null;
3638
+ // disallow offsets beyond 14:00 (e.g., +14:30)
3639
+ if (hh === 14 && mm !== 0) return null;
3640
+ return `${m[1]}${m[2]}:${m[3]}`;
3641
+ }
3642
+
3643
+ function __tfnHasTimezoneSuffix(s) {
3644
+ return /(Z|[+-]\d{2}:\d{2})$/.test(s);
3645
+ }
3646
+
3647
+ function __tfnParseTemporalLiteralParts(t) {
3648
+ if (!(t instanceof Literal)) return null;
3649
+ const [lex, dt] = literalParts(t.value);
3650
+ if (!dt) return null;
3651
+ const val = stripQuotes(lex);
3652
+
3653
+ // xsd:dateTime
3654
+ if (dt === XSD_NS + 'dateTime') {
3655
+ const m = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?$/.exec(val);
3656
+ if (!m) return null;
3657
+ const year = BigInt(m[1]);
3658
+ const month = parseInt(m[2], 10);
3659
+ const day = parseInt(m[3], 10);
3660
+ const hour = parseInt(m[4], 10);
3661
+ const minute = parseInt(m[5], 10);
3662
+ const second = parseInt(m[6], 10);
3663
+ const frac = m[7] || '';
3664
+ const tz = m[8] || null;
3665
+
3666
+ if (!(month >= 1 && month <= 12)) return null;
3667
+ if (!(day >= 1 && day <= 31)) return null;
3668
+ if (!(hour >= 0 && hour <= 23)) return null;
3669
+ if (!(minute >= 0 && minute <= 59)) return null;
3670
+ if (!(second >= 0 && second <= 59)) return null;
3671
+
3672
+ // Keep milliseconds precision (3 digits), truncating extra digits.
3673
+ const msStr = (frac + '000').slice(0, 3);
3674
+ const millis = parseInt(msStr, 10);
3675
+
3676
+ return { kind: 'dateTime', year, month, day, hour, minute, second, millis, tz, dt };
3677
+ }
3678
+
3679
+ // xsd:date
3680
+ if (dt === XSD_NS + 'date') {
3681
+ const m = /^(-?\d{4,})-(\d{2})-(\d{2})(Z|[+-]\d{2}:\d{2})?$/.exec(val);
3682
+ if (!m) return null;
3683
+ const year = BigInt(m[1]);
3684
+ const month = parseInt(m[2], 10);
3685
+ const day = parseInt(m[3], 10);
3686
+ const tz = m[4] || null;
3687
+ if (!(month >= 1 && month <= 12)) return null;
3688
+ if (!(day >= 1 && day <= 31)) return null;
3689
+ return { kind: 'date', year, month, day, tz, dt };
3690
+ }
3691
+
3692
+ // xsd:gYearMonth
3693
+ if (dt === XSD_NS + 'gYearMonth') {
3694
+ const m = /^(-?\d{4,})-(\d{2})(Z|[+-]\d{2}:\d{2})?$/.exec(val);
3695
+ if (!m) return null;
3696
+ const year = BigInt(m[1]);
3697
+ const month = parseInt(m[2], 10);
3698
+ const tz = m[3] || null;
3699
+ if (!(month >= 1 && month <= 12)) return null;
3700
+ return { kind: 'gYearMonth', year, month, tz, dt };
3701
+ }
3702
+
3703
+ // xsd:gYear
3704
+ if (dt === XSD_NS + 'gYear') {
3705
+ const m = /^(-?\d{4,})(Z|[+-]\d{2}:\d{2})?$/.exec(val);
3706
+ if (!m) return null;
3707
+ const year = BigInt(m[1]);
3708
+ const tz = m[2] || null;
3709
+ return { kind: 'gYear', year, tz, dt };
3710
+ }
3711
+
3712
+ return null;
3713
+ }
3714
+
3715
+ function __tfnMod(a, b) {
3716
+ let r = a % b;
3717
+ if (r < 0n) r += b;
3718
+ return r;
3719
+ }
3720
+
3721
+ function __tfnIsLeapYear(y) {
3722
+ // Proleptic Gregorian leap-year rule.
3723
+ // Works for negative years using normalized modulus.
3724
+ if (__tfnMod(y, 4n) !== 0n) return false;
3725
+ if (__tfnMod(y, 100n) !== 0n) return true;
3726
+ return __tfnMod(y, 400n) === 0n;
3727
+ }
3728
+
3729
+ function __tfnDaysInMonth(y, m) {
3730
+ if (m === 2) return __tfnIsLeapYear(y) ? 29 : 28;
3731
+ if (m === 4 || m === 6 || m === 9 || m === 11) return 30;
3732
+ return 31;
3733
+ }
3734
+
3735
+ function __tfnPad2(n) {
3736
+ return n < 10 ? '0' + String(n) : String(n);
3737
+ }
3738
+ function __tfnPad3(n) {
3739
+ const s = String(n);
3740
+ return s.length === 1 ? '00' + s : s.length === 2 ? '0' + s : s;
3741
+ }
3742
+ function __tfnFormatYear(y) {
3743
+ const neg = y < 0n;
3744
+ const abs = neg ? -y : y;
3745
+ let digits = abs.toString();
3746
+ if (digits.length < 4) digits = '0'.repeat(4 - digits.length) + digits;
3747
+ return neg ? '-' + digits : digits;
3748
+ }
3749
+
3750
+ function __tfnAdd1ms(c) {
3751
+ // Mutates and returns c; c: {year,month,day,hour,minute,second,millis}
3752
+ if (c.millis < 999) {
3753
+ c.millis++;
3754
+ return c;
3755
+ }
3756
+ c.millis = 0;
3757
+ if (c.second < 59) {
3758
+ c.second++;
3759
+ return c;
3760
+ }
3761
+ c.second = 0;
3762
+ if (c.minute < 59) {
3763
+ c.minute++;
3764
+ return c;
3765
+ }
3766
+ c.minute = 0;
3767
+ if (c.hour < 23) {
3768
+ c.hour++;
3769
+ return c;
3770
+ }
3771
+ c.hour = 0;
3772
+
3773
+ const dim = __tfnDaysInMonth(c.year, c.month);
3774
+ if (c.day < dim) {
3775
+ c.day++;
3776
+ return c;
3777
+ }
3778
+ c.day = 1;
3779
+ if (c.month < 12) {
3780
+ c.month++;
3781
+ return c;
3782
+ }
3783
+ c.month = 1;
3784
+ c.year = c.year + 1n;
3785
+ return c;
3786
+ }
3787
+
3788
+ function __tfnSub1ms(c) {
3789
+ // Mutates and returns c; c: {year,month,day,hour,minute,second,millis}
3790
+ if (c.millis > 0) {
3791
+ c.millis--;
3792
+ return c;
3793
+ }
3794
+ c.millis = 999;
3795
+ if (c.second > 0) {
3796
+ c.second--;
3797
+ return c;
3798
+ }
3799
+ c.second = 59;
3800
+ if (c.minute > 0) {
3801
+ c.minute--;
3802
+ return c;
3803
+ }
3804
+ c.minute = 59;
3805
+ if (c.hour > 0) {
3806
+ c.hour--;
3807
+ return c;
3808
+ }
3809
+ c.hour = 23;
3810
+
3811
+ if (c.day > 1) {
3812
+ c.day--;
3813
+ return c;
3814
+ }
3815
+ // move to previous month
3816
+ if (c.month > 1) {
3817
+ c.month--;
3818
+ } else {
3819
+ c.month = 12;
3820
+ c.year = c.year - 1n;
3821
+ }
3822
+ c.day = __tfnDaysInMonth(c.year, c.month);
3823
+ return c;
3824
+ }
3825
+
3826
+ function __tfnMakeDateTimeLiteral(c, tz) {
3827
+ const y = __tfnFormatYear(c.year);
3828
+ const lex = `${y}-${__tfnPad2(c.month)}-${__tfnPad2(c.day)}T${__tfnPad2(c.hour)}:${__tfnPad2(c.minute)}:${__tfnPad2(c.second)}.${__tfnPad3(c.millis)}${tz}`;
3829
+ return internLiteral(`"${lex}"^^<${XSD_NS}dateTime>`);
3830
+ }
3831
+
3832
+ function __tfnComputePeriodBounds(parts) {
3833
+ // Returns { tzMin, tzMax, startC, endC } where startC/endC are component objects.
3834
+ const tzMin = parts.tz || '+14:00';
3835
+ const tzMax = parts.tz || '-14:00';
3836
+
3837
+ if (parts.kind === 'dateTime') {
3838
+ const base = {
3839
+ year: parts.year,
3840
+ month: parts.month,
3841
+ day: parts.day,
3842
+ hour: parts.hour,
3843
+ minute: parts.minute,
3844
+ second: parts.second,
3845
+ millis: parts.millis,
3846
+ };
3847
+ return { tzMin, tzMax, startC: { ...base }, endC: { ...base } };
3848
+ }
3849
+
3850
+ if (parts.kind === 'date') {
3851
+ const startC = { year: parts.year, month: parts.month, day: parts.day, hour: 0, minute: 0, second: 0, millis: 0 };
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
+ };
3861
+ return { tzMin, tzMax, startC, endC };
3862
+ }
3863
+
3864
+ if (parts.kind === 'gYearMonth') {
3865
+ const dim = __tfnDaysInMonth(parts.year, parts.month);
3866
+ const startC = { year: parts.year, month: parts.month, day: 1, hour: 0, minute: 0, second: 0, millis: 0 };
3867
+ const endC = { year: parts.year, month: parts.month, day: dim, hour: 23, minute: 59, second: 59, millis: 999 };
3868
+ return { tzMin, tzMax, startC, endC };
3869
+ }
3870
+
3871
+ if (parts.kind === 'gYear') {
3872
+ const startC = { year: parts.year, month: 1, day: 1, hour: 0, minute: 0, second: 0, millis: 0 };
3873
+ const endC = { year: parts.year, month: 12, day: 31, hour: 23, minute: 59, second: 59, millis: 999 };
3874
+ return { tzMin, tzMax, startC, endC };
3875
+ }
3876
+
3877
+ return null;
3878
+ }
3879
+
3880
+ function __tfnBindDefaultTimezone(timeLit, tzLit) {
3881
+ if (!(timeLit instanceof Literal) || !(tzLit instanceof Literal)) return null;
3882
+ const tz = __tfnParseTimezoneValue(tzLit);
3883
+ if (!tz) return null;
3884
+
3885
+ const [lex, dt] = literalParts(timeLit.value);
3886
+ if (!dt) return null;
3887
+ const v = stripQuotes(lex);
3888
+
3889
+ // If already has tz, return unchanged.
3890
+ if (__tfnHasTimezoneSuffix(v)) return timeLit;
3891
+
3892
+ // Only support the temporal types we parse.
3893
+ if (dt !== XSD_NS + 'dateTime' && dt !== XSD_NS + 'date' && dt !== XSD_NS + 'gYearMonth' && dt !== XSD_NS + 'gYear')
3894
+ return null;
3895
+
3896
+ const outLex = `"${v}${tz}"^^<${dt}>`;
3897
+ return internLiteral(outLex);
3898
+ }
3899
+
3628
3900
  function parseDatetimeLike(t) {
3629
3901
  const d = parseXsdDateTerm(t);
3630
3902
  if (d !== null) return d;
@@ -4920,6 +5192,74 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4920
5192
  return [];
4921
5193
  }
4922
5194
 
5195
+ // -----------------------------------------------------------------
5196
+ // 4.3.1 tfn: Time Functions builtins
5197
+ // -----------------------------------------------------------------
5198
+
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] : [];
5242
+ }
5243
+
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 [];
5251
+
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 }];
5258
+
5259
+ const s2 = unifyTerm(g.o, out, subst);
5260
+ return s2 !== null ? [s2] : [];
5261
+ }
5262
+
4923
5263
  // -----------------------------------------------------------------
4924
5264
  // 4.4 list: builtins
4925
5265
  // -----------------------------------------------------------------
@@ -6144,6 +6484,7 @@ function isBuiltinPred(p) {
6144
6484
  v.startsWith(LOG_NS) ||
6145
6485
  v.startsWith(STRING_NS) ||
6146
6486
  v.startsWith(TIME_NS) ||
6487
+ v.startsWith(TFN_NS) ||
6147
6488
  v.startsWith(LIST_NS)
6148
6489
  );
6149
6490
  }
@@ -6568,7 +6909,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
6568
6909
  if (allKnown) continue;
6569
6910
  }
6570
6911
 
6571
- const maxSols = (r.isFuse || headIsStrictGround) ? 1 : undefined;
6912
+ const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
6572
6913
  const sols = proveGoals(r.premise.slice(), empty, facts, backRules, 0, visited, varGen, maxSols);
6573
6914
 
6574
6915
  // Inference fuse
@@ -7102,7 +7443,8 @@ function main() {
7102
7443
  ` -s, --super-restricted Disable all builtins except => and <=.\n` +
7103
7444
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
7104
7445
  ` --strings Print log:outputString strings (ordered by key) instead of N3 output.\n` +
7105
- ` --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`;
7106
7448
  (toStderr ? console.error : console.log)(msg);
7107
7449
  }
7108
7450
 
@@ -7124,6 +7466,7 @@ function main() {
7124
7466
  const showAst = argv.includes('--ast') || argv.includes('-a');
7125
7467
 
7126
7468
  const outputStringsMode = argv.includes('--strings');
7469
+ const streamMode = argv.includes('--stream');
7127
7470
 
7128
7471
  // --enforce-https: rewrite http:// -> https:// for log dereferencing builtins
7129
7472
  if (argv.includes('--enforce-https')) {
@@ -7206,13 +7549,133 @@ function main() {
7206
7549
  materializeRdfLists(triples, frules, brules);
7207
7550
 
7208
7551
  const facts = triples.filter((tr) => isGroundTriple(tr));
7209
- const derived = forwardChain(facts, frules, brules);
7552
+
7210
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.
7211
7555
  if (outputStringsMode) {
7556
+ forwardChain(facts, frules, brules);
7212
7557
  const out = __collectOutputStringsFromFacts(facts, prefixes);
7213
7558
  if (out) process.stdout.write(out);
7214
7559
  process.exit(0);
7215
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);
7216
7679
  const derivedTriples = derived.map((df) => df.fact);
7217
7680
  const usedPrefixes = prefixes.prefixesUsedForOutput(derivedTriples);
7218
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.19",
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}`);