eyeling 1.24.25 → 1.24.26

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
@@ -3,6 +3,8 @@
3
3
  [![npm version](https://img.shields.io/npm/v/eyeling.svg)](https://www.npmjs.com/package/eyeling)
4
4
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19068086-blue.svg)](https://doi.org/10.5281/zenodo.19068086)
5
5
 
6
+ ![Eyeling](eyeling.png)
7
+
6
8
  A compact [Notation3 (N3)](https://notation3.org/) reasoner in **JavaScript**.
7
9
 
8
10
  ## Quick start
@@ -1699,7 +1699,7 @@ function __pushBuiltinDeltaLimited(out, delta, maxResults) {
1699
1699
 
1700
1700
  function __collectListLikeTermsFromTerm(t, out, seen) {
1701
1701
  if (t instanceof ListTerm || t instanceof OpenListTerm) {
1702
- const k = termFastKey ? termFastKey(t) : null;
1702
+ const k = termFastKey(t);
1703
1703
  if (k === null || !seen.has(k)) {
1704
1704
  if (k !== null) seen.add(k);
1705
1705
  out.push(t);
@@ -11498,6 +11498,20 @@ class Parser {
11498
11498
  if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
11499
11499
  this.next();
11500
11500
  pred = internIri(RDF_NS + 'type');
11501
+ } else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
11502
+ // N3 syntactic sugar is also valid in predicate-object lists,
11503
+ // including blank node property lists: [ has :p :o ] means _:b :p :o.
11504
+ this.next();
11505
+ pred = this.parseTerm();
11506
+ } else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
11507
+ // N3 syntactic sugar: [ is :p of :s ] means :s :p _:b.
11508
+ this.next();
11509
+ pred = this.parseTerm();
11510
+ if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
11511
+ this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
11512
+ }
11513
+ this.next();
11514
+ invert = true;
11501
11515
  } else if (this.peek().typ === 'OpPredInvert') {
11502
11516
  this.next();
11503
11517
  pred = this.parseTerm();
@@ -1,5 +1,29 @@
1
+ # ==============================
2
+ # RDF Message Flow input sidecar
3
+ # ==============================
4
+ #
5
+ # This file is the data half of the runnable example:
6
+ #
7
+ # eyeling -r examples/rdf-message-flow.n3 examples/input/rdf-message-flow.trig
8
+ #
9
+ # It is intentionally plain TriG rather than a parser-level RDF Message Log with
10
+ # VERSION "1.2-messages" and MESSAGE delimiters. The RDF Messages draft says an
11
+ # RDF Message is an RDF dataset interpreted atomically, that a stream is an
12
+ # ordered sequence of such messages, and that messages in a stream should not be
13
+ # combined by default. To demonstrate those ideas in Eyeling today, the default
14
+ # graph below contains example-local envelope records (:m001 ... :m005) and each
15
+ # non-empty message payload is placed in its own named graph (in:payload001 ...
16
+ # in:payload005).
17
+ #
18
+ # The envelope IRIs are not identifiers for RDF Messages defined by the spec;
19
+ # they are application-level records used by the flow-control rules. The named
20
+ # payload graphs are the datasets/messages being interpreted atomically. The
21
+ # third envelope is an empty heartbeat, which is valid because RDF Messages may
22
+ # be empty. Blank node labels are kept unique because this sidecar is ordinary
23
+ # TriG; a true RDF Message Log parser would reset blank-node scope per MESSAGE.
24
+
1
25
  @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-message-flow#> .
2
- @prefix msg: <https://example.org/msg#> .
26
+ @prefix flow: <https://eyereasoner.github.io/eyeling/examples/rdf-message-flow/vocab#> .
3
27
  @prefix prov: <http://www.w3.org/ns/prov#> .
4
28
  @prefix sosa: <http://www.w3.org/ns/sosa/> .
5
29
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@@ -10,77 +34,87 @@
10
34
  @prefix see: <https://example.org/see#> .
11
35
  @prefix in: <https://example.org/see/input#> .
12
36
 
13
- # Formal Eyeling input evidence in RDF 1.2 TriG.
14
- # The generated runner reads this TriG evidence directly.
15
-
16
- :temperatureFlow a msg:MessageStream .
17
- :temperatureFlow msg:orderedMessages (:m001 :m002 :m003 :m004 :m005) .
18
- :temperatureFlow msg:message :m001 .
19
- :temperatureFlow msg:message :m002 .
20
- :temperatureFlow msg:message :m003 .
21
- :temperatureFlow msg:message :m004 .
22
- :temperatureFlow msg:message :m005 .
37
+ :temperatureFlow a flow:RDFMessageStream .
38
+ :temperatureFlow flow:orderedEnvelopes (:m001 :m002 :m003 :m004 :m005) .
39
+ :temperatureFlow flow:envelope :m001 .
40
+ :temperatureFlow flow:envelope :m002 .
41
+ :temperatureFlow flow:envelope :m003 .
42
+ :temperatureFlow flow:envelope :m004 .
43
+ :temperatureFlow flow:envelope :m005 .
44
+ :temperatureFlow :producer :thermometerA .
45
+ :temperatureFlow :consumer :flowProcessor .
23
46
  :temperatureFlow :pipeline (:ingest :validate :interpret :route :sink) .
24
47
  :temperatureFlow :highThreshold 26 .
48
+
49
+ :thermometerA a flow:StreamProducer .
50
+ :flowProcessor a flow:StreamConsumer .
51
+ :archiveSink a flow:StreamConsumer .
52
+ :alertSink a flow:StreamConsumer .
53
+ :heartbeatSink a flow:StreamConsumer .
54
+
55
+ :m001 a flow:MessageEnvelope .
25
56
  :m001 :atStage :ingest .
26
- :m001 a msg:RDFMessage .
27
- :m001 msg:offset 1 .
28
- :m001 msg:nextMessage :m002 .
29
- :m001 prov:generatedAtTime "2026-05-12T18:20:00Z" .
30
- :m001 msg:payloadKind :observation .
31
- :m001 msg:expectedResult 21 .
32
- :m001 msg:payload in:formula1 .
33
- :m002 a msg:RDFMessage .
34
- :m002 msg:offset 2 .
35
- :m002 msg:nextMessage :m003 .
36
- :m002 prov:generatedAtTime "2026-05-12T18:21:00Z" .
37
- :m002 msg:payloadKind :observation .
38
- :m002 msg:expectedResult 22 .
39
- :m002 msg:payload in:formula2 .
40
- :m003 a msg:RDFMessage .
41
- :m003 msg:offset 3 .
42
- :m003 msg:nextMessage :m004 .
43
- :m003 prov:generatedAtTime "2026-05-12T18:22:00Z" .
44
- :m003 msg:payloadKind :heartbeat .
45
- :m004 a msg:RDFMessage .
46
- :m004 msg:offset 4 .
47
- :m004 msg:nextMessage :m005 .
48
- :m004 prov:generatedAtTime "2026-05-12T18:23:00Z" .
49
- :m004 msg:payloadKind :observation .
50
- :m004 msg:expectedResult 28 .
51
- :m004 msg:payload in:formula3 .
52
- :m005 a msg:RDFMessage .
53
- :m005 msg:offset 5 .
54
- :m005 prov:generatedAtTime "2026-05-12T18:24:00Z" .
55
- :m005 msg:payloadKind :observation .
56
- :m005 msg:expectedResult 29 .
57
- :m005 msg:payload in:formula4 .
57
+ :m001 flow:offset 1 .
58
+ :m001 flow:nextEnvelope :m002 .
59
+ :m001 prov:generatedAtTime "2026-05-12T18:20:00Z"^^xsd:dateTime .
60
+ :m001 flow:payloadKind :observation .
61
+ :m001 flow:expectedResult 21 .
62
+ :m001 flow:payloadGraph in:payload001 .
63
+
64
+ :m002 a flow:MessageEnvelope .
65
+ :m002 flow:offset 2 .
66
+ :m002 flow:nextEnvelope :m003 .
67
+ :m002 prov:generatedAtTime "2026-05-12T18:21:00Z"^^xsd:dateTime .
68
+ :m002 flow:payloadKind :observation .
69
+ :m002 flow:expectedResult 22 .
70
+ :m002 flow:payloadGraph in:payload002 .
71
+
72
+ :m003 a flow:MessageEnvelope .
73
+ :m003 flow:offset 3 .
74
+ :m003 flow:nextEnvelope :m004 .
75
+ :m003 prov:generatedAtTime "2026-05-12T18:22:00Z"^^xsd:dateTime .
76
+ :m003 flow:payloadKind :heartbeat .
77
+
78
+ :m004 a flow:MessageEnvelope .
79
+ :m004 flow:offset 4 .
80
+ :m004 flow:nextEnvelope :m005 .
81
+ :m004 prov:generatedAtTime "2026-05-12T18:23:00Z"^^xsd:dateTime .
82
+ :m004 flow:payloadKind :observation .
83
+ :m004 flow:expectedResult 28 .
84
+ :m004 flow:payloadGraph in:payload004 .
85
+
86
+ :m005 a flow:MessageEnvelope .
87
+ :m005 flow:offset 5 .
88
+ :m005 prov:generatedAtTime "2026-05-12T18:24:00Z"^^xsd:dateTime .
89
+ :m005 flow:payloadKind :observation .
90
+ :m005 flow:expectedResult 29 .
91
+ :m005 flow:payloadGraph in:payload005 .
58
92
 
59
- in:formula1 {
93
+ in:payload001 {
60
94
  _:m001b0 a sosa:Observation .
61
95
  _:m001b0 sosa:madeBySensor :thermometerA .
62
- _:m001b0 sosa:resultTime "2026-05-12T18:20:00Z" .
96
+ _:m001b0 sosa:resultTime "2026-05-12T18:20:00Z"^^xsd:dateTime .
63
97
  _:m001b0 sosa:hasSimpleResult 21 .
64
98
  }
65
99
 
66
- in:formula2 {
100
+ in:payload002 {
67
101
  _:m002b0 a sosa:Observation .
68
102
  _:m002b0 sosa:madeBySensor :thermometerA .
69
- _:m002b0 sosa:resultTime "2026-05-12T18:21:00Z" .
103
+ _:m002b0 sosa:resultTime "2026-05-12T18:21:00Z"^^xsd:dateTime .
70
104
  _:m002b0 sosa:hasSimpleResult 22 .
71
105
  }
72
106
 
73
- in:formula3 {
107
+ in:payload004 {
74
108
  _:m004b0 a sosa:Observation .
75
109
  _:m004b0 sosa:madeBySensor :thermometerA .
76
- _:m004b0 sosa:resultTime "2026-05-12T18:23:00Z" .
110
+ _:m004b0 sosa:resultTime "2026-05-12T18:23:00Z"^^xsd:dateTime .
77
111
  _:m004b0 sosa:hasSimpleResult 28 .
78
112
  }
79
113
 
80
- in:formula4 {
114
+ in:payload005 {
81
115
  _:m005b0 a sosa:Observation .
82
116
  _:m005b0 sosa:madeBySensor :thermometerA .
83
- _:m005b0 sosa:resultTime "2026-05-12T18:24:00Z" .
117
+ _:m005b0 sosa:resultTime "2026-05-12T18:24:00Z"^^xsd:dateTime .
84
118
  _:m005b0 sosa:hasSimpleResult 29 .
85
119
  }
86
120
 
@@ -88,13 +122,7 @@ in:metadata {
88
122
  in:run a see:InputDataset .
89
123
  in:run see:name "rdf_message_flow" .
90
124
  in:run see:title "RDF Message Flow" .
91
- in:run see:sourceFile "examples/n3/rdf_message_flow.n3" .
92
- in:run see:sourceSHA256 "e4e534c8ac3c2aa276e7158cca8d3146531879033f73685c302b486be2ab0099" .
93
- in:run see:description "A companion to rdf_messages.n3. This example focuses on a live stream where\nRDF Messages continuously flow through a small processing pipeline. The next\nmessage is released only after the current message reaches the sink, so the\nstream behaves as an ordered, replayable flow rather than a single merged RDF\ngraph." .
125
+ in:run see:sourceFile "examples/rdf-message-flow.n3" .
126
+ in:run see:description "A single Eyeling example split across an N3 rule file and a TriG input sidecar. It models an ordered RDF Message Stream using application-level envelopes and named payload graphs so that each payload can be interpreted atomically without merging all message contents into one graph." .
94
127
  in:run see:compiler "Eyeling RDF/TriG input sidecar" .
95
- in:run see:inputFacts 42 .
96
- in:run see:compiledRules 9 .
97
- in:run see:compiledBackwardRules 0 .
98
- in:run see:compiledFuses 0 .
99
- in:run see:compiledQueries 1 .
100
128
  }
@@ -0,0 +1,89 @@
1
+ # ===================================
2
+ # RDF Message Microgrid input sidecar
3
+ # ===================================
4
+ #
5
+ # This file is the data half of the runnable example:
6
+ #
7
+ # eyeling -r examples/rdf-message-microgrid.n3 examples/input/rdf-message-microgrid.trig
8
+ #
9
+ # The scene is a clinic operating as an islanded microgrid after a storm. The
10
+ # data arrives as a replayable stream of atomic messages: life-safety loads,
11
+ # power status, flexible demand, and an empty heartbeat. The default graph holds
12
+ # application-local envelope facts, while each non-empty message payload is kept
13
+ # in a named graph.
14
+ #
15
+ # The envelope IRIs are not RDF Message identifiers defined by the specification;
16
+ # they are local replay records for this example. The named graphs are the
17
+ # datasets/messages interpreted atomically by the N3 rules.
18
+
19
+ @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-message-microgrid#> .
20
+ @prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-message-microgrid/vocab#> .
21
+ @prefix prov: <http://www.w3.org/ns/prov#> .
22
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
23
+ @prefix see: <https://example.org/see#> .
24
+ @prefix in: <https://example.org/see/input#> .
25
+
26
+ :stormClinicLog a rmsg:MessageLog .
27
+ :stormClinicLog rmsg:orderedMessages (:m001 :m002 :m003 :m004) .
28
+ :stormClinicLog rmsg:message :m001 .
29
+ :stormClinicLog rmsg:message :m002 .
30
+ :stormClinicLog rmsg:message :m003 .
31
+ :stormClinicLog rmsg:message :m004 .
32
+ :stormClinicLog rmsg:retentionPolicy "storm replay archive" .
33
+
34
+ :m001 a rmsg:MessageEnvelope .
35
+ :m001 rmsg:offset 1 .
36
+ :m001 prov:generatedAtTime "2026-05-15T19:00:00Z"^^xsd:dateTime .
37
+ :m001 rmsg:payloadKind :lifeSafetyLoads .
38
+ :m001 rmsg:payloadGraph in:lifeSafetyPayload .
39
+
40
+ :m002 a rmsg:MessageEnvelope .
41
+ :m002 rmsg:offset 2 .
42
+ :m002 prov:generatedAtTime "2026-05-15T19:01:00Z"^^xsd:dateTime .
43
+ :m002 rmsg:payloadKind :powerStatus .
44
+ :m002 rmsg:payloadGraph in:powerPayload .
45
+
46
+ :m003 a rmsg:MessageEnvelope .
47
+ :m003 rmsg:offset 3 .
48
+ :m003 prov:generatedAtTime "2026-05-15T19:02:00Z"^^xsd:dateTime .
49
+ :m003 rmsg:payloadKind :flexibleDemand .
50
+ :m003 rmsg:payloadGraph in:flexPayload .
51
+
52
+ :m004 a rmsg:MessageEnvelope .
53
+ :m004 rmsg:offset 4 .
54
+ :m004 prov:generatedAtTime "2026-05-15T19:03:00Z"^^xsd:dateTime .
55
+ :m004 rmsg:payloadKind :heartbeat .
56
+
57
+ in:lifeSafetyPayload {
58
+ :oxygenConcentrator a :LifeSafetyLoad .
59
+ :oxygenConcentrator :requiresWatts 500 .
60
+ :oxygenConcentrator :serves :respiratoryCareRoom .
61
+
62
+ :vaccineFridge a :ColdChainLoad .
63
+ :vaccineFridge :requiresWatts 120 .
64
+ :vaccineFridge :serves :vaccineColdChain .
65
+ }
66
+
67
+ in:powerPayload {
68
+ :batteryBank a :PowerReserve .
69
+ :batteryBank :availableWatts 650 .
70
+ :batteryBank :state :islanded .
71
+
72
+ :solarForecast a :NearTermForecast .
73
+ :solarForecast :expectedWatts 150 .
74
+ }
75
+
76
+ in:flexPayload {
77
+ :evChargers a :FlexibleLoad .
78
+ :evChargers :shedWatts 600 .
79
+ :evChargers :serves :staffVehicles .
80
+ }
81
+
82
+ in:metadata {
83
+ in:run a see:InputDataset .
84
+ in:run see:name "rdf_message_microgrid" .
85
+ in:run see:title "RDF Message Microgrid" .
86
+ in:run see:sourceFile "examples/rdf-message-microgrid.n3" .
87
+ in:run see:description "A storm clinic microgrid example using application-local RDF Message envelopes and named payload graphs so a reasoner can make a bounded, explainable load-shedding decision." .
88
+ in:run see:compiler "Eyeling RDF/TriG input sidecar" .
89
+ }
@@ -1,5 +1,32 @@
1
+ # ==========================
2
+ # RDF Messages input sidecar
3
+ # ==========================
4
+ #
5
+ # This file is the data half of the runnable example:
6
+ #
7
+ # eyeling -r examples/rdf-messages.n3 examples/input/rdf-messages.trig
8
+ #
9
+ # It is intentionally plain TriG rather than a parser-level RDF Message Log with
10
+ # VERSION "1.2-messages" and MESSAGE delimiters. The RDF Messages draft says an
11
+ # RDF Message is an RDF dataset interpreted atomically, that a message stream is
12
+ # an ordered sequence of such messages, that messages are not combined by
13
+ # default, and that blank-node labels are scoped to the message.
14
+ #
15
+ # To demonstrate those ideas in Eyeling today, the default graph contains
16
+ # application-local envelope records (:m001 ... :m003) plus replay order and
17
+ # offsets. Each non-empty message payload is placed in its own named graph
18
+ # (in:payload001 and in:payload003). The second envelope is an empty heartbeat,
19
+ # which is valid because RDF Messages may be empty.
20
+ #
21
+ # The envelope IRIs are not message identifiers defined by the spec; they are
22
+ # application-level records used by the rules. The named payload graphs are the
23
+ # datasets/messages being interpreted atomically. The rmsg:localBlankLabel values
24
+ # show that the same source-local label "_:b0" can recur in different messages;
25
+ # the concrete TriG blank nodes themselves are unique because this sidecar is
26
+ # ordinary TriG rather than an RDF Message Log parser resetting blank-node scope.
27
+
1
28
  @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-messages#> .
2
- @prefix msg: <https://w3c-cg.github.io/rsp/spec/messages#> .
29
+ @prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-messages/vocab#> .
3
30
  @prefix prov: <http://www.w3.org/ns/prov#> .
4
31
  @prefix sosa: <http://www.w3.org/ns/sosa/> .
5
32
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@@ -10,46 +37,46 @@
10
37
  @prefix see: <https://example.org/see#> .
11
38
  @prefix in: <https://example.org/see/input#> .
12
39
 
13
- # Formal Eyeling input evidence in RDF 1.2 TriG.
14
- # The generated runner reads this TriG evidence directly.
40
+ :temperatureLog a rmsg:MessageLog .
41
+ :temperatureLog rmsg:orderedMessages (:m001 :m002 :m003) .
42
+ :temperatureLog rmsg:message :m001 .
43
+ :temperatureLog rmsg:message :m002 .
44
+ :temperatureLog rmsg:message :m003 .
45
+ :temperatureLog rmsg:profile :generatedAtTimeProfile .
46
+ :temperatureLog rmsg:retentionPolicy "replayable archive" .
47
+
48
+ :m001 a rmsg:MessageEnvelope .
49
+ :m001 rmsg:offset 1 .
50
+ :m001 prov:generatedAtTime "2026-05-12T18:20:00Z"^^xsd:dateTime .
51
+ :m001 rmsg:payloadKind :observation .
52
+ :m001 rmsg:localBlankLabel "_:b0" .
53
+ :m001 rmsg:expectedResult 22 .
54
+ :m001 rmsg:payloadGraph in:payload001 .
55
+
56
+ :m002 a rmsg:MessageEnvelope .
57
+ :m002 rmsg:offset 2 .
58
+ :m002 prov:generatedAtTime "2026-05-12T18:22:00Z"^^xsd:dateTime .
59
+ :m002 rmsg:payloadKind :heartbeat .
15
60
 
16
- :temperatureLog a msg:MessageLog .
17
- :temperatureLog msg:orderedMessages (:m001 :m002 :m003) .
18
- :temperatureLog msg:message :m001 .
19
- :temperatureLog msg:message :m002 .
20
- :temperatureLog msg:message :m003 .
21
- :temperatureLog msg:profile :generatedAtTimeProfile .
22
- :temperatureLog msg:retentionPolicy "replayable archive" .
23
- :m001 a msg:RDFMessage .
24
- :m001 msg:offset 1 .
25
- :m001 prov:generatedAtTime "2026-05-12T18:20:00Z" .
26
- :m001 msg:payloadKind :observation .
27
- :m001 msg:localBlankLabel "_:b0" .
28
- :m001 msg:expectedResult 22 .
29
- :m001 msg:payload in:formula1 .
30
- :m002 a msg:RDFMessage .
31
- :m002 msg:offset 2 .
32
- :m002 prov:generatedAtTime "2026-05-12T18:22:00Z" .
33
- :m002 msg:payloadKind :heartbeat .
34
- :m003 a msg:RDFMessage .
35
- :m003 msg:offset 3 .
36
- :m003 prov:generatedAtTime "2026-05-12T18:25:00Z" .
37
- :m003 msg:payloadKind :observation .
38
- :m003 msg:localBlankLabel "_:b0" .
39
- :m003 msg:expectedResult 23 .
40
- :m003 msg:payload in:formula2 .
61
+ :m003 a rmsg:MessageEnvelope .
62
+ :m003 rmsg:offset 3 .
63
+ :m003 prov:generatedAtTime "2026-05-12T18:25:00Z"^^xsd:dateTime .
64
+ :m003 rmsg:payloadKind :observation .
65
+ :m003 rmsg:localBlankLabel "_:b0" .
66
+ :m003 rmsg:expectedResult 23 .
67
+ :m003 rmsg:payloadGraph in:payload003 .
41
68
 
42
- in:formula1 {
69
+ in:payload001 {
43
70
  _:m001b0 a sosa:Observation .
44
71
  _:m001b0 sosa:madeBySensor :thermometerA .
45
- _:m001b0 sosa:resultTime "2026-05-12T18:20:00Z" .
72
+ _:m001b0 sosa:resultTime "2026-05-12T18:20:00Z"^^xsd:dateTime .
46
73
  _:m001b0 sosa:hasSimpleResult 22 .
47
74
  }
48
75
 
49
- in:formula2 {
76
+ in:payload003 {
50
77
  _:m003b0 a sosa:Observation .
51
78
  _:m003b0 sosa:madeBySensor :thermometerA .
52
- _:m003b0 sosa:resultTime "2026-05-12T18:25:00Z" .
79
+ _:m003b0 sosa:resultTime "2026-05-12T18:25:00Z"^^xsd:dateTime .
53
80
  _:m003b0 sosa:hasSimpleResult 23 .
54
81
  }
55
82
 
@@ -57,13 +84,7 @@ in:metadata {
57
84
  in:run a see:InputDataset .
58
85
  in:run see:name "rdf_messages" .
59
86
  in:run see:title "RDF Messages" .
60
- in:run see:sourceFile "examples/n3/rdf_messages.n3" .
61
- in:run see:sourceSHA256 "2ea8b414b92e65531cf384000955ca47811d5b7c779a8d2c9fb007515e745f32" .
62
- in:run see:description "This Eyeling example models the main idea from\nhttps://pietercolpaert.be/papers/eswc2026-rdf-messages/:\na message stream/log is not just one freely mergeable RDF graph. It is an\nordered sequence of RDF Datasets that are interpreted atomically, one message\nat a time. The middle message is deliberately empty to model a heartbeat, and\nthe local blank-node label \"_:b0\" is deliberately reused by two messages to\nshow message-scoped blank nodes." .
87
+ in:run see:sourceFile "examples/rdf-messages.n3" .
88
+ in:run see:description "A single Eyeling example split across an N3 rule file and a TriG input sidecar. It models a replayable RDF Message Log with application-level envelopes and named payload graphs so that each payload can be interpreted atomically without merging all message contents into one graph." .
63
89
  in:run see:compiler "Eyeling RDF/TriG input sidecar" .
64
- in:run see:inputFacts 25 .
65
- in:run see:compiledRules 6 .
66
- in:run see:compiledBackwardRules 0 .
67
- in:run see:compiledFuses 0 .
68
- in:run see:compiledQueries 1 .
69
90
  }
@@ -6,7 +6,7 @@
6
6
  - [Input TriG](../input/rdf-message-flow.trig)
7
7
 
8
8
  ## Answer
9
- Continuous RDF Message flow accepted: 5 ordered messages moved through the ingest → validate → interpret → route → sink pipeline. The threshold was 26, so results 21 and 22 were archived, the heartbeat kept the stream alive, and results 28 and 29 were emitted as alerts.
9
+ Continuous RDF Message flow accepted: 5 ordered message envelopes moved through the ingest → validate → interpret → route → sink pipeline. The threshold was 26, so results 21 and 22 were archived, the heartbeat kept the stream alive, and results 28 and 29 were emitted as alerts.
10
10
 
11
11
  ## Explanation
12
- The N3 source starts only :m001 at ingress. Each message must reach :sink before the continuous-flow rule releases its msg:nextMessage. Observation payloads are inspected with log:includes inside each message formula, while the empty heartbeat uses the same envelope and routing stages without a payload. This models messages flowing through a live stream while preserving message boundaries.
12
+ The input is a single runnable example split across an N3 rule file and a TriG sidecar. The TriG file uses example-local envelope facts for stream order and processing state, while each named payload graph is treated as an atomic RDF Message dataset. Only :m001 starts at ingress; each envelope must reach :sink before the continuous-flow rule releases its flow:nextEnvelope. Observation payloads are inspected with log:includes inside their own payload formula, and the empty heartbeat advances without a payload graph. This keeps message boundaries visible to the reasoner instead of merging all payload triples into one global graph.
@@ -0,0 +1,14 @@
1
+ # rdf-message-microgrid
2
+
3
+ ## Source files
4
+
5
+ - [N3 rules](../rdf-message-microgrid.n3)
6
+ - [Input TriG](../input/rdf-message-microgrid.trig)
7
+
8
+ ## Answer
9
+ Storm clinic microgrid accepted: 4 RDF Message envelopes were replayed atomically. Critical care needs 620 W, current battery plus solar gives 800 W, and deferring the EV chargers frees 600 W, so the protected budget is 1400 W. The reasoned action is to keep the oxygen concentrator and vaccine fridge online, while deferring EV charging.
10
+
11
+ ## Why this is an RDF Messages example
12
+ The input is a single runnable example split across an N3 rule file and a TriG sidecar. The default graph records stream order, offsets, and envelope metadata. Each non-empty named graph is treated as an atomic message payload, and the fourth message is an empty heartbeat. The rules inspect each payload with log:includes inside its own formula, then combine only the derived conclusions needed for the microgrid decision. This keeps the explanation tied to message boundaries instead of silently flattening the stream into one global graph.
13
+
14
+ This is intentionally not a parser-level VERSION \"1.2-messages\" / MESSAGE delimiter test. It is a reasoning example over an already-materialized sidecar representation of a message log.
@@ -6,7 +6,9 @@
6
6
  - [Input TriG](../input/rdf-messages.trig)
7
7
 
8
8
  ## Answer
9
- RDF Message log accepted: 3 explicit message boundaries are preserved. Message :m002 is an empty heartbeat, and the local blank-node label _:b0 is safely reused in separate messages.
9
+ RDF Message replay archive accepted: 3 explicit message boundaries are preserved. Message :m002 is an empty heartbeat, and the local blank-node label _:b0 is safely reused in separate message envelopes.
10
10
 
11
11
  ## Explanation
12
- The N3 source models an RDF Message Log as an ordered sequence of RDF Messages. Each non-empty message has a formula-valued payload that is inspected with log:includes, so the observation data stays inside the message boundary instead of being treated as one global graph. The two temperature results, 22 and 23, are different observations from the same stream but are contextualized by their message boundaries.
12
+ The input is a single runnable example split across an N3 rule file and a TriG sidecar. The TriG file uses application-local envelope facts for stream order and replay metadata, while each non-empty named payload graph is treated as an atomic RDF Message dataset. Payloads are inspected with log:includes inside their own formulas, so the observation data stays inside the message boundary instead of being treated as one global graph. The two temperature results, 22 and 23, are different observations from the same stream but are contextualized by their message boundaries.
13
+
14
+ This is intentionally not a parser-level VERSION \"1.2-messages\" / MESSAGE delimiter test. It is a reasoning example over an already-materialized sidecar representation of a message log.
@@ -1,14 +1,35 @@
1
1
  # ================
2
2
  # RDF Message Flow
3
3
  # ================
4
- # A companion to rdf_messages.n3. This example focuses on a live stream where
5
- # RDF Messages continuously flow through a small processing pipeline. The next
6
- # message is released only after the current message reaches the sink, so the
7
- # stream behaves as an ordered, replayable flow rather than a single merged RDF
8
- # graph.
4
+ #
5
+ # Run as:
6
+ #
7
+ # eyeling -r examples/rdf-message-flow.n3 examples/input/rdf-message-flow.trig
8
+ #
9
+ # Motivation
10
+ # ----------
11
+ # The RDF Messages draft describes an RDF Message as an RDF dataset interpreted
12
+ # atomically, and an RDF Message Stream as an ordered sequence of such messages.
13
+ # It also defines RDF Message Logs with explicit MESSAGE delimiters. This Eyeling
14
+ # example keeps the same message-level discipline while staying in ordinary
15
+ # N3/TriG that Eyeling can reason over today.
16
+ #
17
+ # The companion TriG file therefore separates two layers:
18
+ #
19
+ # 1. example-local envelope facts in the default graph, such as order,
20
+ # processing stage, and payload graph; and
21
+ # 2. one named graph per non-empty message payload, treated here as the RDF
22
+ # dataset/message that must be inspected atomically.
23
+ #
24
+ # The rules below deliberately do not merge all payload graphs into one global
25
+ # graph. Instead, each observation is checked with log:includes inside the named
26
+ # payload formula. The empty heartbeat has no payload graph but still advances
27
+ # through the same pipeline. The next message is released only after the current
28
+ # one reaches the sink, making the example a small back-pressure / flow-control
29
+ # story rather than a batch merge of all input triples.
9
30
 
10
31
  @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-message-flow#>.
11
- @prefix msg: <https://example.org/msg#>.
32
+ @prefix flow: <https://eyereasoner.github.io/eyeling/examples/rdf-message-flow/vocab#>.
12
33
  @prefix prov: <http://www.w3.org/ns/prov#>.
13
34
  @prefix sosa: <http://www.w3.org/ns/sosa/>.
14
35
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@@ -17,126 +38,120 @@
17
38
  @prefix log: <http://www.w3.org/2000/10/swap/log#>.
18
39
  @prefix string: <http://www.w3.org/2000/10/swap/string#>.
19
40
 
20
-
21
- # The stream starts with just the first message at ingress. Later messages are
22
- # not pre-loaded; each one is released by the previous message after sink.
23
-
24
-
25
-
26
- # Empty heartbeat: it flows through the same pipeline and keeps the stream live.
27
-
28
-
29
-
30
- # Stage 1: once a message has entered the stream, validate its envelope.
31
-
32
- { ?Message :atStage :ingest. } => {
33
- ?Message :atStage :validate.
41
+ # Stage 1: once an envelope has entered the stream, validate it.
42
+ { ?Envelope :atStage :ingest. } => {
43
+ ?Envelope :atStage :validate.
34
44
  }.
35
45
 
36
- # Stage 2: validation preserves the message boundary before interpretation.
46
+ # Stage 2: validation records that the envelope keeps an explicit message
47
+ # boundary before the payload is interpreted.
37
48
  {
38
- ?Message :atStage :validate;
39
- a msg:RDFMessage;
40
- msg:offset ?Offset.
49
+ ?Envelope :atStage :validate;
50
+ a flow:MessageEnvelope;
51
+ flow:offset ?Offset.
41
52
  } => {
42
- ?Message msg:boundaryExplicit true.
43
- ?Message :atStage :interpret.
53
+ ?Envelope flow:boundaryExplicit true.
54
+ ?Envelope :atStage :interpret.
44
55
  }.
45
56
 
46
- # Stage 3a: observation payloads are inspected inside their message formula.
57
+ # Stage 3a: observation payloads are inspected inside their own payload graph.
58
+ # This models the RDF Messages idea that messages are separate atomic datasets.
47
59
  {
48
- ?Message :atStage :interpret;
49
- msg:payloadKind :observation;
50
- msg:expectedResult ?Result;
51
- msg:payload ?Payload.
60
+ ?Envelope :atStage :interpret;
61
+ flow:payloadKind :observation;
62
+ flow:expectedResult ?Result;
63
+ flow:payloadGraph ?Payload.
52
64
  ?Payload log:nameOf ?PayloadContext.
53
65
  ?PayloadContext log:includes { ?Observation sosa:hasSimpleResult ?Result. }.
54
66
  } => {
55
- ?Message msg:payloadResult ?Result.
56
- ?Message :atStage :route.
67
+ ?Envelope flow:payloadResult ?Result.
68
+ ?Envelope :atStage :route.
57
69
  }.
58
70
 
59
- # Stage 3b: empty heartbeats have no payload, but still move through the flow.
71
+ # Stage 3b: empty heartbeats contain no quads, but RDF Messages explicitly allow
72
+ # empty messages, so the envelope still moves through the flow.
60
73
  {
61
- ?Message :atStage :interpret;
62
- msg:payloadKind :heartbeat.
74
+ ?Envelope :atStage :interpret;
75
+ flow:payloadKind :heartbeat.
63
76
  } => {
64
- ?Message msg:emptyMessageAllowed true.
65
- ?Message :atStage :route.
77
+ ?Envelope flow:emptyMessageAllowed true.
78
+ ?Envelope :atStage :route.
66
79
  }.
67
80
 
68
81
  # Stage 4a: hot observations are routed to the alert sink.
69
82
  {
70
- ?Message :atStage :route;
71
- msg:payloadResult ?Result.
83
+ ?Envelope :atStage :route;
84
+ flow:payloadResult ?Result.
72
85
  :temperatureFlow :highThreshold ?Threshold.
73
86
  ?Result math:greaterThan ?Threshold.
74
87
  } => {
75
- ?Message :route :alertSink.
76
- ?Message :atStage :sink.
77
- :alertSink :received ?Message.
88
+ ?Envelope :route :alertSink.
89
+ ?Envelope :atStage :sink.
90
+ :alertSink :received ?Envelope.
78
91
  }.
79
92
 
80
93
  # Stage 4b: normal observations are routed to the archive sink.
81
94
  {
82
- ?Message :atStage :route;
83
- msg:payloadResult ?Result.
95
+ ?Envelope :atStage :route;
96
+ flow:payloadResult ?Result.
84
97
  :temperatureFlow :highThreshold ?Threshold.
85
98
  ?Result math:notGreaterThan ?Threshold.
86
99
  } => {
87
- ?Message :route :archiveSink.
88
- ?Message :atStage :sink.
89
- :archiveSink :received ?Message.
100
+ ?Envelope :route :archiveSink.
101
+ ?Envelope :atStage :sink.
102
+ :archiveSink :received ?Envelope.
90
103
  }.
91
104
 
92
105
  # Stage 4c: heartbeats are routed separately from observations.
93
106
  {
94
- ?Message :atStage :route;
95
- msg:emptyMessageAllowed true.
107
+ ?Envelope :atStage :route;
108
+ flow:emptyMessageAllowed true.
96
109
  } => {
97
- ?Message :route :heartbeatSink.
98
- ?Message :atStage :sink.
99
- :heartbeatSink :received ?Message.
110
+ ?Envelope :route :heartbeatSink.
111
+ ?Envelope :atStage :sink.
112
+ :heartbeatSink :received ?Envelope.
100
113
  }.
101
114
 
102
- # Continuous-flow rule: reaching the sink releases the next message into ingress.
115
+ # Continuous-flow rule: reaching the sink releases the next envelope into ingress.
116
+ # This mirrors a consumer-visible ordered stream: later messages are not processed
117
+ # until the earlier message has completed the pipeline.
103
118
  {
104
- ?Message :atStage :sink;
105
- msg:nextMessage ?Next.
106
- ?Next a msg:RDFMessage.
119
+ ?Envelope :atStage :sink;
120
+ flow:nextEnvelope ?Next.
121
+ ?Next a flow:MessageEnvelope.
107
122
  } => {
108
- ?Message :releases ?Next.
123
+ ?Envelope :releases ?Next.
109
124
  ?Next :atStage :ingest.
110
125
  }.
111
126
 
112
- # The Eyeling verdict is emitted only after all five messages have flowed through
113
- # ingest, validation, interpretation, routing, and sink.
127
+ # The Eyeling verdict is emitted only after all five envelopes have flowed
128
+ # through ingest, validation, interpretation, routing, and sink while preserving
129
+ # their message boundaries.
114
130
  {
115
- :temperatureFlow msg:orderedMessages ?Messages;
131
+ :temperatureFlow flow:orderedEnvelopes ?Envelopes;
116
132
  :highThreshold ?Threshold.
117
- ?Messages list:length ?Count.
133
+ ?Envelopes list:length ?Count.
118
134
  :m001 :atStage :sink;
119
- msg:payloadResult ?FirstResult;
135
+ flow:payloadResult ?FirstResult;
120
136
  :route :archiveSink;
121
137
  :releases :m002.
122
138
  :m002 :atStage :sink;
123
- msg:payloadResult ?SecondResult;
139
+ flow:payloadResult ?SecondResult;
124
140
  :route :archiveSink;
125
141
  :releases :m003.
126
142
  :m003 :atStage :sink;
127
- msg:emptyMessageAllowed true;
143
+ flow:emptyMessageAllowed true;
128
144
  :route :heartbeatSink;
129
145
  :releases :m004.
130
146
  :m004 :atStage :sink;
131
- msg:payloadResult ?FourthResult;
147
+ flow:payloadResult ?FourthResult;
132
148
  :route :alertSink;
133
149
  :releases :m005.
134
150
  :m005 :atStage :sink;
135
- msg:payloadResult ?FifthResult;
151
+ flow:payloadResult ?FifthResult;
136
152
  :route :alertSink.
137
- ("# rdf-message-flow\n\n## Source files\n\n- [N3 rules](../rdf-message-flow.n3)\n- [Input TriG](../input/rdf-message-flow.trig)\n\n## Answer\nContinuous RDF Message flow accepted: %d ordered messages moved through the ingest → validate → interpret → route → sink pipeline. The threshold was %d, so results %s and %s were archived, the heartbeat kept the stream alive, and results %s and %s were emitted as alerts.\n\n## Explanation\nThe N3 source starts only :m001 at ingress. Each message must reach :sink before the continuous-flow rule releases its msg:nextMessage. Observation payloads are inspected with log:includes inside each message formula, while the empty heartbeat uses the same envelope and routing stages without a payload. This models messages flowing through a live stream while preserving message boundaries." ?Count ?Threshold ?FirstResult ?SecondResult ?FourthResult ?FifthResult) string:format ?Block.
153
+ ("# rdf-message-flow\n\n## Source files\n\n- [N3 rules](../rdf-message-flow.n3)\n- [Input TriG](../input/rdf-message-flow.trig)\n\n## Answer\nContinuous RDF Message flow accepted: %d ordered message envelopes moved through the ingest → validate → interpret → route → sink pipeline. The threshold was %d, so results %s and %s were archived, the heartbeat kept the stream alive, and results %s and %s were emitted as alerts.\n\n## Explanation\nThe input is a single runnable example split across an N3 rule file and a TriG sidecar. The TriG file uses example-local envelope facts for stream order and processing state, while each named payload graph is treated as an atomic RDF Message dataset. Only :m001 starts at ingress; each envelope must reach :sink before the continuous-flow rule releases its flow:nextEnvelope. Observation payloads are inspected with log:includes inside their own payload formula, and the empty heartbeat advances without a payload graph. This keeps message boundaries visible to the reasoner instead of merging all payload triples into one global graph." ?Count ?Threshold ?FirstResult ?SecondResult ?FourthResult ?FifthResult) string:format ?Block.
138
154
  } => {
139
155
  :rdfMessageFlowExample log:outputString ?Block.
140
156
  :rdfMessageFlowExample :demonstrates :ContinuousFlow, :BackPressureRelease, :AtomicMessageContext, :HeartbeatInFlow, :ThresholdRouting.
141
157
  }.
142
-
@@ -0,0 +1,141 @@
1
+ # =====================
2
+ # RDF Message Microgrid
3
+ # =====================
4
+ #
5
+ # Run as:
6
+ #
7
+ # eyeling -r examples/rdf-message-microgrid.n3 examples/input/rdf-message-microgrid.trig
8
+ #
9
+ # Motivation
10
+ # ----------
11
+ # A clinic has lost grid power during a storm. Several small systems continue to
12
+ # send RDF data: one message describes life-safety loads, another describes the
13
+ # current battery and solar situation, another describes flexible demand that may
14
+ # be deferred, and a fourth message is an empty heartbeat.
15
+ #
16
+ # The RDF Messages draft treats each message as an RDF dataset intended to be
17
+ # interpreted atomically. It also treats a message stream as an ordered sequence
18
+ # and allows empty messages. This example models those ideas in ordinary TriG/N3:
19
+ # the companion TriG file contains application-local envelope facts in the
20
+ # default graph, and each non-empty payload is placed in its own named graph.
21
+ #
22
+ # The point is not only technical. Atomic messages let a small reasoner make a
23
+ # careful decision under pressure: protect the oxygen concentrator and vaccine
24
+ # fridge, defer the EV chargers, and explain the action from the replayed message
25
+ # boundaries instead of silently merging every payload into one timeless graph.
26
+ #
27
+ # This is intentionally not a parser-level VERSION "1.2-messages" / MESSAGE
28
+ # delimiter test. It is a reasoning example over an already-materialized sidecar
29
+ # representation of a message log.
30
+
31
+ @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-message-microgrid#>.
32
+ @prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-message-microgrid/vocab#>.
33
+ @prefix prov: <http://www.w3.org/ns/prov#>.
34
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
35
+ @prefix math: <http://www.w3.org/2000/10/swap/math#>.
36
+ @prefix list: <http://www.w3.org/2000/10/swap/list#>.
37
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
38
+ @prefix string: <http://www.w3.org/2000/10/swap/string#>.
39
+
40
+ # Every envelope with an offset is an explicit replay boundary. The envelope is
41
+ # application-local metadata; the payload graph is the dataset/message being
42
+ # interpreted atomically.
43
+ {
44
+ ?Log a rmsg:MessageLog;
45
+ rmsg:message ?Message.
46
+ ?Message a rmsg:MessageEnvelope;
47
+ rmsg:offset ?Offset.
48
+ } => {
49
+ ?Message rmsg:boundaryExplicit true.
50
+ ?Log rmsg:replayContains ?Message.
51
+ }.
52
+
53
+ # Empty RDF Messages are valid. Here the fourth envelope is a heartbeat that
54
+ # confirms the stream is still alive without adding any payload triples.
55
+ {
56
+ ?Message a rmsg:MessageEnvelope;
57
+ rmsg:payloadKind :heartbeat.
58
+ } => {
59
+ ?Message rmsg:emptyMessageAllowed true.
60
+ }.
61
+
62
+ # Inspect the life-safety message inside its own payload graph.
63
+ {
64
+ ?Message rmsg:payloadKind :lifeSafetyLoads;
65
+ rmsg:payloadGraph ?Payload.
66
+ ?Payload log:nameOf ?PayloadContext.
67
+ ?PayloadContext log:includes {
68
+ :oxygenConcentrator :requiresWatts ?Oxygen.
69
+ :vaccineFridge :requiresWatts ?Fridge.
70
+ }.
71
+ (?Oxygen ?Fridge) math:sum ?CriticalWatts.
72
+ } => {
73
+ :clinicMicrogrid rmsg:criticalWatts ?CriticalWatts.
74
+ :clinicMicrogrid rmsg:mustKeep :oxygenConcentrator, :vaccineFridge.
75
+ }.
76
+
77
+ # Inspect the power-status message atomically.
78
+ {
79
+ ?Message rmsg:payloadKind :powerStatus;
80
+ rmsg:payloadGraph ?Payload.
81
+ ?Payload log:nameOf ?PayloadContext.
82
+ ?PayloadContext log:includes {
83
+ :batteryBank :availableWatts ?BatteryWatts.
84
+ :solarForecast :expectedWatts ?SolarWatts.
85
+ }.
86
+ (?BatteryWatts ?SolarWatts) math:sum ?AvailableWatts.
87
+ } => {
88
+ :clinicMicrogrid rmsg:availableWatts ?AvailableWatts.
89
+ }.
90
+
91
+ # Inspect the flexible-demand message atomically. The EV chargers are useful, but
92
+ # they are safe to defer so life-safety loads keep running.
93
+ {
94
+ ?Message rmsg:payloadKind :flexibleDemand;
95
+ rmsg:payloadGraph ?Payload.
96
+ ?Payload log:nameOf ?PayloadContext.
97
+ ?PayloadContext log:includes {
98
+ :evChargers :shedWatts ?ShedWatts.
99
+ }.
100
+ } => {
101
+ :clinicMicrogrid rmsg:deferrableWatts ?ShedWatts.
102
+ :clinicMicrogrid rmsg:mayDefer :evChargers.
103
+ }.
104
+
105
+ # The decision combines conclusions from the separate messages, while preserving
106
+ # the evidence that each conclusion came through an explicit message boundary.
107
+ {
108
+ :clinicMicrogrid rmsg:criticalWatts ?CriticalWatts;
109
+ rmsg:availableWatts ?AvailableWatts;
110
+ rmsg:deferrableWatts ?ShedWatts;
111
+ rmsg:mustKeep :oxygenConcentrator, :vaccineFridge;
112
+ rmsg:mayDefer :evChargers.
113
+ (?AvailableWatts ?ShedWatts) math:sum ?ProtectedBudget.
114
+ ?ProtectedBudget math:greaterThan ?CriticalWatts.
115
+ } => {
116
+ :clinicMicrogrid rmsg:protectedBudgetWatts ?ProtectedBudget.
117
+ :clinicMicrogrid rmsg:resilienceAction :protectClinic.
118
+ :clinicMicrogrid rmsg:keeps :oxygenConcentrator, :vaccineFridge.
119
+ :clinicMicrogrid rmsg:defers :evChargers.
120
+ }.
121
+
122
+ # Emit the example report only when the message stream, empty heartbeat, atomic
123
+ # payload inspection, and protection decision have all been derived.
124
+ {
125
+ :stormClinicLog rmsg:orderedMessages ?Messages.
126
+ ?Messages list:length ?Count.
127
+ :m001 rmsg:boundaryExplicit true.
128
+ :m002 rmsg:boundaryExplicit true.
129
+ :m003 rmsg:boundaryExplicit true.
130
+ :m004 rmsg:boundaryExplicit true;
131
+ rmsg:emptyMessageAllowed true.
132
+ :clinicMicrogrid rmsg:criticalWatts ?CriticalWatts;
133
+ rmsg:availableWatts ?AvailableWatts;
134
+ rmsg:deferrableWatts ?ShedWatts;
135
+ rmsg:protectedBudgetWatts ?ProtectedBudget;
136
+ rmsg:resilienceAction :protectClinic.
137
+ ("# rdf-message-microgrid\n\n## Source files\n\n- [N3 rules](../rdf-message-microgrid.n3)\n- [Input TriG](../input/rdf-message-microgrid.trig)\n\n## Answer\nStorm clinic microgrid accepted: %d RDF Message envelopes were replayed atomically. Critical care needs %d W, current battery plus solar gives %d W, and deferring the EV chargers frees %d W, so the protected budget is %d W. The reasoned action is to keep the oxygen concentrator and vaccine fridge online, while deferring EV charging.\n\n## Why this is an RDF Messages example\nThe input is a single runnable example split across an N3 rule file and a TriG sidecar. The default graph records stream order, offsets, and envelope metadata. Each non-empty named graph is treated as an atomic message payload, and the fourth message is an empty heartbeat. The rules inspect each payload with log:includes inside its own formula, then combine only the derived conclusions needed for the microgrid decision. This keeps the explanation tied to message boundaries instead of silently flattening the stream into one global graph.\n\nThis is intentionally not a parser-level VERSION \\\"1.2-messages\\\" / MESSAGE delimiter test. It is a reasoning example over an already-materialized sidecar representation of a message log." ?Count ?CriticalWatts ?AvailableWatts ?ShedWatts ?ProtectedBudget) string:format ?Block.
138
+ } => {
139
+ :rdfMessageMicrogridExample log:outputString ?Block.
140
+ :rdfMessageMicrogridExample :demonstrates :AtomicMessageContext, :EmptyHeartbeat, :ReplayableMessageLog, :ResilientDecisionSupport.
141
+ }.
@@ -1,16 +1,38 @@
1
1
  # ============
2
2
  # RDF Messages
3
3
  # ============
4
- # This Eyeling example models the main idea from
5
- # https://pietercolpaert.be/papers/eswc2026-rdf-messages/:
6
- # a message stream/log is not just one freely mergeable RDF graph. It is an
7
- # ordered sequence of RDF Datasets that are interpreted atomically, one message
8
- # at a time. The middle message is deliberately empty to model a heartbeat, and
9
- # the local blank-node label "_:b0" is deliberately reused by two messages to
10
- # show message-scoped blank nodes.
4
+ #
5
+ # Run as:
6
+ #
7
+ # eyeling -r examples/rdf-messages.n3 examples/input/rdf-messages.trig
8
+ #
9
+ # Motivation
10
+ # ----------
11
+ # The RDF Messages draft defines an RDF Message as an RDF dataset interpreted
12
+ # atomically, an RDF Message Stream as an ordered sequence of messages, and an
13
+ # RDF Message Log as a replayable record of such a stream. The draft also says
14
+ # that messages should not be combined by default and that blank node labels are
15
+ # scoped to the message in which they occur.
16
+ #
17
+ # This Eyeling example demonstrates those ideas in ordinary N3/TriG rather than
18
+ # in a parser-level RDF Message Log with VERSION "1.2-messages" and MESSAGE
19
+ # delimiters. The companion TriG file therefore has two layers:
20
+ #
21
+ # 1. application-local envelope facts in the default graph, such as message
22
+ # order, offset, expected result, and the textual blank-node label seen in
23
+ # the original stream; and
24
+ # 2. one named graph per non-empty message payload, treated as the atomic RDF
25
+ # dataset/message to be inspected.
26
+ #
27
+ # The rules below validate a replay archive with three message envelopes. The
28
+ # middle message is an empty heartbeat. The first and third messages both record
29
+ # the local blank-node label "_:b0"; because this sidecar is normal TriG, the
30
+ # actual TriG blank nodes are unique, while the envelope records show the
31
+ # message-scoped label reuse that a true RDF Message Log parser would preserve
32
+ # by resetting blank-node scope at each MESSAGE delimiter.
11
33
 
12
34
  @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-messages#>.
13
- @prefix msg: <https://w3c-cg.github.io/rsp/spec/messages#>.
35
+ @prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-messages/vocab#>.
14
36
  @prefix prov: <http://www.w3.org/ns/prov#>.
15
37
  @prefix sosa: <http://www.w3.org/ns/sosa/>.
16
38
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@@ -19,51 +41,49 @@
19
41
  @prefix log: <http://www.w3.org/2000/10/swap/log#>.
20
42
  @prefix string: <http://www.w3.org/2000/10/swap/string#>.
21
43
 
22
-
23
-
24
- # Empty RDF Message: a legal heartbeat/keep-alive event.
25
-
26
-
27
44
  # A message with an offset inside a log has an explicit replay boundary.
28
-
45
+ # The envelope resource is application-local; the payload graph is the dataset
46
+ # treated as the RDF Message.
29
47
  {
30
- ?Log a msg:MessageLog;
31
- msg:message ?Message.
32
- ?Message a msg:RDFMessage;
33
- msg:offset ?Offset.
48
+ ?Log a rmsg:MessageLog;
49
+ rmsg:message ?Message.
50
+ ?Message a rmsg:MessageEnvelope;
51
+ rmsg:offset ?Offset.
34
52
  } => {
35
- ?Message msg:boundaryExplicit true.
36
- ?Log msg:replayContains ?Message.
53
+ ?Message rmsg:boundaryExplicit true.
54
+ ?Log rmsg:replayContains ?Message.
37
55
  }.
38
56
 
39
- # The payload is a dataset-like formula. We can inspect it without merging it
40
- # into the global graph, preserving the message as the unit of interpretation.
57
+ # The payload is a dataset-like named graph. Inspect it with log:includes instead
58
+ # of merging all payload triples into the global graph.
41
59
  {
42
- ?Message a msg:RDFMessage;
43
- msg:expectedResult ?Result;
44
- msg:payload ?Payload.
60
+ ?Message a rmsg:MessageEnvelope;
61
+ rmsg:expectedResult ?Result;
62
+ rmsg:payloadGraph ?Payload.
45
63
  ?Payload log:nameOf ?PayloadContext.
46
64
  ?PayloadContext log:includes { ?Observation sosa:hasSimpleResult ?Result. }.
47
65
  } => {
48
- ?Message msg:payloadResult ?Result.
66
+ ?Message rmsg:payloadResult ?Result.
49
67
  }.
50
68
 
51
- # Heartbeat/keep-alive messages can be empty and still occupy a boundary.
69
+ # Empty messages are legal RDF Messages. Here the empty second envelope acts as
70
+ # a heartbeat/keep-alive and still occupies a boundary in the replay archive.
52
71
  {
53
- ?Message a msg:RDFMessage;
54
- msg:payloadKind :heartbeat.
72
+ ?Message a rmsg:MessageEnvelope;
73
+ rmsg:payloadKind :heartbeat.
55
74
  } => {
56
- ?Message msg:emptyMessageAllowed true.
75
+ ?Message rmsg:emptyMessageAllowed true.
57
76
  :HeartbeatEvidence :accepted ?Message.
58
77
  }.
59
78
 
60
- # The same blank-node label may recur in a message log because blank nodes are
61
- # scoped to their RDF Message, not to the whole replay archive.
79
+ # The same local blank-node label may recur in different messages. In this
80
+ # plain-TriG sidecar the real blank nodes are unique, and the envelope metadata
81
+ # records the per-message label visible in the source stream.
62
82
  {
63
- ?First a msg:RDFMessage;
64
- msg:localBlankLabel ?Label.
65
- ?Second a msg:RDFMessage;
66
- msg:localBlankLabel ?Label.
83
+ ?First a rmsg:MessageEnvelope;
84
+ rmsg:localBlankLabel ?Label.
85
+ ?Second a rmsg:MessageEnvelope;
86
+ rmsg:localBlankLabel ?Label.
67
87
  ?First log:notEqualTo ?Second.
68
88
  } => {
69
89
  :BlankNodeScope :reusedLabel ?Label.
@@ -71,35 +91,35 @@
71
91
  }.
72
92
 
73
93
  # Different observations from the same sensor are not a global contradiction:
74
- # each message is interpreted in its own communication context.
94
+ # by default, message payloads remain separate communication contexts.
75
95
  {
76
- ?First a msg:RDFMessage;
77
- msg:payloadResult ?FirstResult.
78
- ?Second a msg:RDFMessage;
79
- msg:payloadResult ?SecondResult.
96
+ ?First a rmsg:MessageEnvelope;
97
+ rmsg:payloadResult ?FirstResult.
98
+ ?Second a rmsg:MessageEnvelope;
99
+ rmsg:payloadResult ?SecondResult.
80
100
  ?FirstResult math:notEqualTo ?SecondResult.
81
101
  ?First log:notEqualTo ?Second.
82
102
  } => {
83
103
  :MessageContext :differentObservationsStayContextual true.
84
104
  }.
85
105
 
86
- # The Eyeling verdict is emitted only when all message-specific validations have been
87
- # derived from the log, message metadata, payload formulas, and built-ins.
106
+ # The Eyeling verdict is emitted only when all message-specific validations have
107
+ # been derived from the log metadata, ordered message list, payload graphs, and
108
+ # built-ins.
88
109
  {
89
- :temperatureLog msg:orderedMessages ?Messages.
110
+ :temperatureLog rmsg:orderedMessages ?Messages.
90
111
  ?Messages list:length ?Count.
91
- :m001 msg:boundaryExplicit true;
92
- msg:payloadResult ?FirstResult.
93
- :m002 msg:boundaryExplicit true;
94
- msg:emptyMessageAllowed true.
95
- :m003 msg:boundaryExplicit true;
96
- msg:payloadResult ?SecondResult.
112
+ :m001 rmsg:boundaryExplicit true;
113
+ rmsg:payloadResult ?FirstResult.
114
+ :m002 rmsg:boundaryExplicit true;
115
+ rmsg:emptyMessageAllowed true.
116
+ :m003 rmsg:boundaryExplicit true;
117
+ rmsg:payloadResult ?SecondResult.
97
118
  :BlankNodeScope :reusedLabel ?Label;
98
119
  :isPerMessage true.
99
120
  :MessageContext :differentObservationsStayContextual true.
100
- ("# rdf-messages\n\n## Source files\n\n- [N3 rules](../rdf-messages.n3)\n- [Input TriG](../input/rdf-messages.trig)\n\n## Answer\nRDF Message log accepted: %d explicit message boundaries are preserved. Message :m002 is an empty heartbeat, and the local blank-node label %s is safely reused in separate messages.\n\n## Explanation\nThe N3 source models an RDF Message Log as an ordered sequence of RDF Messages. Each non-empty message has a formula-valued payload that is inspected with log:includes, so the observation data stays inside the message boundary instead of being treated as one global graph. The two temperature results, %s and %s, are different observations from the same stream but are contextualized by their message boundaries." ?Count ?Label ?FirstResult ?SecondResult) string:format ?Block.
121
+ ("# rdf-messages\n\n## Source files\n\n- [N3 rules](../rdf-messages.n3)\n- [Input TriG](../input/rdf-messages.trig)\n\n## Answer\nRDF Message replay archive accepted: %d explicit message boundaries are preserved. Message :m002 is an empty heartbeat, and the local blank-node label %s is safely reused in separate message envelopes.\n\n## Explanation\nThe input is a single runnable example split across an N3 rule file and a TriG sidecar. The TriG file uses application-local envelope facts for stream order and replay metadata, while each non-empty named payload graph is treated as an atomic RDF Message dataset. Payloads are inspected with log:includes inside their own formulas, so the observation data stays inside the message boundary instead of being treated as one global graph. The two temperature results, %s and %s, are different observations from the same stream but are contextualized by their message boundaries.\n\nThis is intentionally not a parser-level VERSION \\\"1.2-messages\\\" / MESSAGE delimiter test. It is a reasoning example over an already-materialized sidecar representation of a message log." ?Count ?Label ?FirstResult ?SecondResult) string:format ?Block.
101
122
  } => {
102
123
  :rdfMessagesExample log:outputString ?Block.
103
124
  :rdfMessagesExample :demonstrates :ExplicitBoundaries, :AtomicMessageContext, :EmptyHeartbeat, :MessageScopedBlankNodes, :ReplayableMessageLog.
104
125
  }.
105
-
package/eyeling.js CHANGED
@@ -1699,7 +1699,7 @@ function __pushBuiltinDeltaLimited(out, delta, maxResults) {
1699
1699
 
1700
1700
  function __collectListLikeTermsFromTerm(t, out, seen) {
1701
1701
  if (t instanceof ListTerm || t instanceof OpenListTerm) {
1702
- const k = termFastKey ? termFastKey(t) : null;
1702
+ const k = termFastKey(t);
1703
1703
  if (k === null || !seen.has(k)) {
1704
1704
  if (k !== null) seen.add(k);
1705
1705
  out.push(t);
@@ -11498,6 +11498,20 @@ class Parser {
11498
11498
  if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
11499
11499
  this.next();
11500
11500
  pred = internIri(RDF_NS + 'type');
11501
+ } else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
11502
+ // N3 syntactic sugar is also valid in predicate-object lists,
11503
+ // including blank node property lists: [ has :p :o ] means _:b :p :o.
11504
+ this.next();
11505
+ pred = this.parseTerm();
11506
+ } else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
11507
+ // N3 syntactic sugar: [ is :p of :s ] means :s :p _:b.
11508
+ this.next();
11509
+ pred = this.parseTerm();
11510
+ if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
11511
+ this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
11512
+ }
11513
+ this.next();
11514
+ invert = true;
11501
11515
  } else if (this.peek().typ === 'OpPredInvert') {
11502
11516
  this.next();
11503
11517
  pred = this.parseTerm();
package/lib/builtins.js CHANGED
@@ -1688,7 +1688,7 @@ function __pushBuiltinDeltaLimited(out, delta, maxResults) {
1688
1688
 
1689
1689
  function __collectListLikeTermsFromTerm(t, out, seen) {
1690
1690
  if (t instanceof ListTerm || t instanceof OpenListTerm) {
1691
- const k = termFastKey ? termFastKey(t) : null;
1691
+ const k = termFastKey(t);
1692
1692
  if (k === null || !seen.has(k)) {
1693
1693
  if (k !== null) seen.add(k);
1694
1694
  out.push(t);
package/lib/parser.js CHANGED
@@ -465,6 +465,20 @@ class Parser {
465
465
  if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
466
466
  this.next();
467
467
  pred = internIri(RDF_NS + 'type');
468
+ } else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
469
+ // N3 syntactic sugar is also valid in predicate-object lists,
470
+ // including blank node property lists: [ has :p :o ] means _:b :p :o.
471
+ this.next();
472
+ pred = this.parseTerm();
473
+ } else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
474
+ // N3 syntactic sugar: [ is :p of :s ] means :s :p _:b.
475
+ this.next();
476
+ pred = this.parseTerm();
477
+ if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
478
+ this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
479
+ }
480
+ this.next();
481
+ invert = true;
468
482
  } else if (this.peek().typ === 'OpPredInvert') {
469
483
  this.next();
470
484
  pred = this.parseTerm();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.24.25",
3
+ "version": "1.24.26",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -1245,35 +1245,6 @@ ${U('o1')} ${U('path')} (${U('c')} ${U('d')}).
1245
1245
  ],
1246
1246
  },
1247
1247
 
1248
- {
1249
- name: '49c rdf:first/rest bind an unbound rule blank to an N3 list literal',
1250
- opt: { proofComments: false },
1251
- input: `@prefix : <http://example.org/> .
1252
-
1253
- (1) <http://a.example/p> <http://a.example/o> .
1254
-
1255
- {
1256
- _:el1 rdf:first 1 .
1257
- _:el1 rdf:rest rdf:nil .
1258
- _:el1 <http://a.example/p> <http://a.example/o> .
1259
- }
1260
- =>
1261
- {
1262
- :result :has :success-literal-25 .
1263
- }.
1264
-
1265
- { :result :has :success-literal-25 . }
1266
- =>
1267
- {
1268
- :test :is true .
1269
- }.
1270
- `,
1271
- expect: [
1272
- /:result\s+:has\s+:success-literal-25\s*\./,
1273
- /:test\s+:is\s+true\s*\./,
1274
- ],
1275
- },
1276
-
1277
1248
  {
1278
1249
  name: '50 rdf collection materialization: rdf:first/rdf:rest triples become list terms',
1279
1250
  opt: { proofComments: false },
@@ -1298,6 +1269,38 @@ _:l2 rdf:rest rdf:nil.
1298
1269
  // Newer eyeling.js features
1299
1270
  // -------------------------
1300
1271
 
1272
+ {
1273
+ name: '50b regression: rdf:first/rest with variable subject binds existing N3 list term',
1274
+ opt: { proofComments: false },
1275
+ input: `@prefix : <http://example.org/> .
1276
+
1277
+ (1) <http://a.example/p> <http://a.example/o> .
1278
+
1279
+ {
1280
+ _:el1 <http://www.w3.org/1999/02/22-rdf-syntax-ns#first> 1 .
1281
+ _:el1 <http://www.w3.org/1999/02/22-rdf-syntax-ns#rest> <http://www.w3.org/1999/02/22-rdf-syntax-ns#nil> .
1282
+ _:el1 <http://a.example/p> <http://a.example/o> .
1283
+ }
1284
+ =>
1285
+ {
1286
+ :result :has :success-literal-25.
1287
+ }.
1288
+
1289
+ {} => {
1290
+ :test :contains :success-literal-25.
1291
+ }.
1292
+
1293
+ {
1294
+ :result :has :success-literal-25.
1295
+ }
1296
+ =>
1297
+ {
1298
+ :test :is true.
1299
+ }.
1300
+ `,
1301
+ expect: [/:result\s+:has\s+:success-literal-25\s*\./, /:test\s+:is\s+true\s*\./],
1302
+ },
1303
+
1301
1304
  {
1302
1305
  name: '51 automatic output rendering: prints log:outputString values ordered by key (subject)',
1303
1306
  opt: ['-n'],
@@ -1425,6 +1428,111 @@ res:CITY_Chañaral rdfs:label "Chañaral".
1425
1428
  expect: [/:result\s+:has\s+:success-literal-24\s*\./, /:test\s+:is\s+true\s*\./],
1426
1429
  },
1427
1430
 
1431
+ {
1432
+ name: '52d regression: @base and @prefix remapping resolve relative IRIs incrementally',
1433
+ opt: { proofComments: false },
1434
+ input: `# Reference: https://www.w3.org/TR/turtle/#sec-iri-references
1435
+ @base <http://foo/bar/> .
1436
+ <a1> <b1> <c1> .
1437
+ @base <http://example.org/ns/> .
1438
+ <a2> <http://example.org/ns/b2> <c2> .
1439
+ @base <foo/> .
1440
+ <a3> <b3> <c3> .
1441
+ @prefix : <bar#> .
1442
+ :a4 :b4 :c4 .
1443
+ @prefix : <http://example.org/ns2#> .
1444
+ :a5 :b5 :c5 .
1445
+
1446
+ {
1447
+ <http://foo/bar/a1> <http://foo/bar/b1> <http://foo/bar/c1> .
1448
+ <http://example.org/ns/a2> <http://example.org/ns/b2> <http://example.org/ns/c2> .
1449
+ <http://example.org/ns/foo/a3> <http://example.org/ns/foo/b3> <http://example.org/ns/foo/c3> .
1450
+ <http://example.org/ns/foo/bar#a4> <http://example.org/ns/foo/bar#b4> <http://example.org/ns/foo/bar#c4> .
1451
+ <http://example.org/ns2#a5> <http://example.org/ns2#b5> <http://example.org/ns2#c5> .
1452
+ }
1453
+ =>
1454
+ {
1455
+ :result :has :success-literal-28.
1456
+ }.
1457
+
1458
+ {} => {
1459
+ :test :contains :success-literal-28.
1460
+ }.
1461
+
1462
+ {
1463
+ :result :has :success-literal-28.
1464
+ }
1465
+ =>
1466
+ {
1467
+ :test :is true.
1468
+ }.
1469
+ `,
1470
+ expect: [/:result\s+:has\s+:success-literal-28\s*\./, /:test\s+:is\s+true\s*\./],
1471
+ },
1472
+
1473
+ {
1474
+ name: '52e regression: is/of works in blank node property lists with list values',
1475
+ opt: { proofComments: false },
1476
+ input: `@prefix : <http://example.org/> .
1477
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1478
+
1479
+ :thing :prop (1 2 3).
1480
+
1481
+ {
1482
+ (1 2 3) is :prop of :thing.
1483
+ [ is :prop of :thing ].
1484
+ }
1485
+ =>
1486
+ {
1487
+ :result :has :success-literal-29.
1488
+ }.
1489
+
1490
+ {} => {
1491
+ :test :contains :success-literal-29.
1492
+ }.
1493
+
1494
+ {
1495
+ :result :has :success-literal-29.
1496
+ }
1497
+ =>
1498
+ {
1499
+ :test :is true.
1500
+ }.
1501
+ `,
1502
+ expect: [/:result\s+:has\s+:success-literal-29\s*\./, /:test\s+:is\s+true\s*\./],
1503
+ },
1504
+
1505
+ {
1506
+ name: '52f regression: has works with list values in statement predicate position',
1507
+ opt: { proofComments: false },
1508
+ input: `@prefix : <http://example.org/> .
1509
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1510
+
1511
+ :thing :prop (1 2 3).
1512
+
1513
+ {
1514
+ :thing has :prop (1 2 3).
1515
+ }
1516
+ =>
1517
+ {
1518
+ :result :has :success-literal-30.
1519
+ }.
1520
+
1521
+ {} => {
1522
+ :test :contains :success-literal-30.
1523
+ }.
1524
+
1525
+ {
1526
+ :result :has :success-literal-30.
1527
+ }
1528
+ =>
1529
+ {
1530
+ :test :is true.
1531
+ }.
1532
+ `,
1533
+ expect: [/:result\s+:has\s+:success-literal-30\s*\./, /:test\s+:is\s+true\s*\./],
1534
+ },
1535
+
1428
1536
  {
1429
1537
  name: '53 --stream: prints prefixes used in input (not just derived output) before streaming triples',
1430
1538
  opt: ['--stream', '-n'],