eyeling 1.24.31 → 1.24.33

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.
@@ -1207,25 +1207,36 @@ function parseXsdDateTerm(t) {
1207
1207
  return d;
1208
1208
  }
1209
1209
 
1210
+ function isXsdDateTimeDatatype(dt) {
1211
+ return dt === XSD_NS + 'dateTime' || dt === XSD_NS + 'dateTimeStamp';
1212
+ }
1213
+
1210
1214
  function parseXsdDatetimeTerm(t) {
1211
1215
  if (!(t instanceof Literal)) return null;
1212
1216
  const [lex, dt] = literalParts(t.value);
1213
- if (dt !== XSD_NS + 'dateTime') return null;
1217
+ if (!isXsdDateTimeDatatype(dt)) return null;
1214
1218
  const val = stripQuotes(lex);
1219
+
1220
+ // xsd:dateTimeStamp is a subtype of xsd:dateTime with a required timezone.
1221
+ // Keep xsd:dateTime's existing permissive behaviour, but reject stamp
1222
+ // lexicals that do not actually carry the required timezone.
1223
+ if (dt === XSD_NS + 'dateTimeStamp' && !/(Z|[+-]\d{2}:\d{2})$/.test(val)) return null;
1224
+
1215
1225
  const d = new Date(val);
1216
1226
  if (Number.isNaN(d.getTime())) return null;
1217
1227
  return d; // Date in local/UTC, we only use timestamp
1218
1228
  }
1219
1229
 
1220
1230
  function parseXsdDateTimeLexParts(t) {
1221
- // Parse *lexical* components of an xsd:dateTime literal without timezone normalization.
1231
+ // Parse *lexical* components of an xsd:dateTime/dateTimeStamp literal without timezone normalization.
1222
1232
  // Returns { yearStr, month, day, hour, minute, second, tz } or null.
1223
1233
  if (!(t instanceof Literal)) return null;
1224
1234
  const [lex, dt] = literalParts(t.value);
1225
- if (dt !== XSD_NS + 'dateTime') return null;
1235
+ if (!isXsdDateTimeDatatype(dt)) return null;
1226
1236
  const val = stripQuotes(lex);
1227
1237
 
1228
1238
  // xsd:dateTime lexical: YYYY-MM-DDThh:mm:ss(.s+)?(Z|(+|-)hh:mm)?
1239
+ // xsd:dateTimeStamp has the same lexical form, but with the timezone required.
1229
1240
  const m = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.exec(val);
1230
1241
  if (!m) return null;
1231
1242
 
@@ -1236,6 +1247,7 @@ function parseXsdDateTimeLexParts(t) {
1236
1247
  const minute = parseInt(m[5], 10);
1237
1248
  const second = parseInt(m[6], 10);
1238
1249
  const tz = m[7] || null;
1250
+ if (dt === XSD_NS + 'dateTimeStamp' && !tz) return null;
1239
1251
 
1240
1252
  if (!(month >= 1 && month <= 12)) return null;
1241
1253
  if (!(day >= 1 && day <= 31)) return null;
@@ -1302,7 +1314,8 @@ function parseIso8601DurationToSeconds(s) {
1302
1314
  }
1303
1315
 
1304
1316
  function parseNumericForCompareTerm(t) {
1305
- // Strict: only accept xsd numeric literals, xsd:duration, xsd:date, xsd:dateTime
1317
+ // Strict: only accept xsd numeric literals, xsd:duration, xsd:date,
1318
+ // xsd:dateTime, and xsd:dateTimeStamp.
1306
1319
  // (or untyped numeric tokens).
1307
1320
  const bi = parseIntLiteral(t);
1308
1321
  if (bi !== null) return { kind: 'bigint', value: bi };
@@ -1369,7 +1382,7 @@ function parseNumOrDuration(t) {
1369
1382
  }
1370
1383
  }
1371
1384
 
1372
- // xsd:date / xsd:dateTime
1385
+ // xsd:date / xsd:dateTime / xsd:dateTimeStamp
1373
1386
  const dtval = parseDatetimeLike(t);
1374
1387
  if (dtval !== null) {
1375
1388
  return dtval.getTime() / 1000.0;
@@ -0,0 +1,273 @@
1
+ # RDF Message Logs in Eyeling — from stream to reasoning
2
+
3
+ This deck explains the example `rdf-message-flow.n3` and its input file `input/rdf-message-flow.trig`.
4
+
5
+ The goal is to show, in plain language, how Eyeling can now read an RDF Message Log directly instead of asking the example data to describe its own message envelopes by hand.
6
+
7
+ ---
8
+
9
+ ## The everyday problem
10
+
11
+ Many real systems do not receive one big dataset.
12
+
13
+ They receive a stream of small updates:
14
+
15
+ - a sensor reading,
16
+ - a command,
17
+ - a status heartbeat,
18
+ - an alert,
19
+ - another sensor reading.
20
+
21
+ Each update matters as a separate communication event.
22
+
23
+ If we simply merge everything into one graph, we lose the order and the boundary between messages.
24
+
25
+ ---
26
+
27
+ ## A message is a sealed packet
28
+
29
+ Think of an RDF Message as a sealed packet of RDF data.
30
+
31
+ Inside the packet there may be triples or named graphs.
32
+
33
+ Outside the packet there is the stream order: first message, second message, third message, and so on.
34
+
35
+ The important idea is:
36
+
37
+ > The reasoner should know when one message ends and the next one begins.
38
+
39
+ ---
40
+
41
+ ## What an RDF Message Log adds
42
+
43
+ An RDF Message Log is a replayable record of a message stream.
44
+
45
+ Instead of saying “subscribe to this live channel”, the file says:
46
+
47
+ > Here are the messages that arrived, in order.
48
+
49
+ That makes it useful for examples, tests, audits, debugging, reproducible reasoning, and explanations.
50
+
51
+ ---
52
+
53
+ ## The new syntax in the input file
54
+
55
+ The input begins with:
56
+
57
+ ```trig
58
+ VERSION "1.2-messages"
59
+ ```
60
+
61
+ That tells Eyeling:
62
+
63
+ > This file contains message boundaries.
64
+
65
+ Then each boundary is written as:
66
+
67
+ ```trig
68
+ MESSAGE
69
+ ```
70
+
71
+ So the file can look like this:
72
+
73
+ ```trig
74
+ # message 1 data
75
+ :temperatureFlow :highThreshold 26 .
76
+ _:obs sosa:hasSimpleResult 21 .
77
+
78
+ MESSAGE
79
+
80
+ # message 2 data
81
+ _:obs sosa:hasSimpleResult 22 .
82
+
83
+ MESSAGE
84
+
85
+ # message 3: empty heartbeat
86
+ MESSAGE
87
+
88
+ # message 4 data
89
+ _:obs sosa:hasSimpleResult 28 .
90
+ ```
91
+
92
+ ---
93
+
94
+ ## What Eyeling does internally
95
+
96
+ Eyeling does not treat `MESSAGE` as an ordinary RDF term.
97
+
98
+ It handles it before normal N3 reasoning starts.
99
+
100
+ Internally, Eyeling turns the log into a replay view:
101
+
102
+ - one stream resource,
103
+ - one envelope per message,
104
+ - an offset for each envelope,
105
+ - a link to the next envelope,
106
+ - a payload graph for each non-empty message,
107
+ - and an explicit marker for empty messages.
108
+
109
+ The rules then reason over that replay view.
110
+
111
+ ---
112
+
113
+ ## Why this is better than hand-written envelopes
114
+
115
+ Before this change, the example input had to describe the stream manually:
116
+
117
+ - message `:m001`,
118
+ - message `:m002`,
119
+ - payload graph `in:payload001`,
120
+ - next message links,
121
+ - payload kind markers,
122
+ - offsets.
123
+
124
+ That worked, but it made the example bulky.
125
+
126
+ It also mixed two concerns:
127
+
128
+ 1. the message-log machinery, and
129
+ 2. the domain logic of routing temperature observations.
130
+
131
+ Now Eyeling handles the message-log machinery.
132
+
133
+ The N3 file can focus on the logic.
134
+
135
+ ---
136
+
137
+ ## What the temperature-flow example does
138
+
139
+ The example models a small stream processor.
140
+
141
+ Messages move through these stages:
142
+
143
+ 1. ingest,
144
+ 2. validate,
145
+ 3. interpret,
146
+ 4. route,
147
+ 5. sink.
148
+
149
+ The stream contains temperature readings and one empty heartbeat.
150
+
151
+ The rules route normal readings to an archive sink and high readings to an alert sink.
152
+
153
+ ---
154
+
155
+ ## The empty heartbeat matters
156
+
157
+ One message in the example contains no RDF triples.
158
+
159
+ That is not an error.
160
+
161
+ It represents a heartbeat:
162
+
163
+ > “The stream is still alive, even though there is no new observation payload.”
164
+
165
+ Eyeling still creates an envelope for it.
166
+
167
+ That means the empty message keeps its place in the ordered replay.
168
+
169
+ ---
170
+
171
+ ## Blank nodes stay message-local
172
+
173
+ The input deliberately reuses the same blank-node label in several messages:
174
+
175
+ ```trig
176
+ _:obs sosa:hasSimpleResult 21 .
177
+
178
+ MESSAGE
179
+
180
+ _:obs sosa:hasSimpleResult 22 .
181
+ ```
182
+
183
+ That does not mean both messages talk about the same blank node.
184
+
185
+ In a message log, blank-node labels are scoped to the message.
186
+
187
+ Eyeling rewrites them internally so each message gets its own blank nodes.
188
+
189
+ ---
190
+
191
+ ## How the N3 rules see the replay
192
+
193
+ The N3 rules do not see `MESSAGE` directly.
194
+
195
+ They see Eyeling’s replay vocabulary, `eymsg:`.
196
+
197
+ For example, a rule can ask:
198
+
199
+ ```n3
200
+ ?Stream a eymsg:RDFMessageStream;
201
+ eymsg:firstEnvelope ?Envelope.
202
+ ```
203
+
204
+ Another rule can inspect a payload:
205
+
206
+ ```n3
207
+ ?Envelope eymsg:payloadGraph ?Payload.
208
+ ?Payload log:nameOf ?PayloadContext.
209
+ ?PayloadContext log:includes {
210
+ ?Observation sosa:hasSimpleResult ?Result.
211
+ }.
212
+ ```
213
+
214
+ That keeps each message payload inside its own context.
215
+
216
+ ---
217
+
218
+ ## Back pressure in one sentence
219
+
220
+ The example releases only the first envelope at the start.
221
+
222
+ Each envelope must reach the sink before the next envelope is released.
223
+
224
+ That gives a simple form of ordered replay or back pressure:
225
+
226
+ > process this message, then release the next one.
227
+
228
+ ---
229
+
230
+ ## What the final answer says
231
+
232
+ When the example succeeds, Eyeling reports that five parser-replayed envelopes moved through the flow.
233
+
234
+ With a threshold of 26:
235
+
236
+ - 21 is archived,
237
+ - 22 is archived,
238
+ - the empty heartbeat is accepted,
239
+ - 28 becomes an alert,
240
+ - 29 becomes an alert.
241
+
242
+ The important part is not only the routing result.
243
+
244
+ The important part is that the result was derived while preserving message boundaries.
245
+
246
+ ---
247
+
248
+ ## Why a wide audience should care
249
+
250
+ This pattern is useful wherever data arrives over time:
251
+
252
+ - sensors,
253
+ - event logs,
254
+ - audit trails,
255
+ - clinical systems,
256
+ - energy systems,
257
+ - pub/sub channels,
258
+ - digital twins,
259
+ - provenance streams.
260
+
261
+ You can replay the stream, reason over each message atomically, and explain what happened without flattening the whole history into one graph.
262
+
263
+ ---
264
+
265
+ ## The takeaway
266
+
267
+ `MESSAGE` is the boundary.
268
+
269
+ Eyeling turns those boundaries into ordered replay envelopes.
270
+
271
+ The N3 rules consume the replay and focus on the domain logic.
272
+
273
+ That makes the example shorter, clearer, and closer to how a real pub/sub channel would be processed.
@@ -1,89 +1,56 @@
1
- # ===================================
2
- # RDF Message Microgrid input sidecar
3
- # ===================================
1
+ # ==================================
2
+ # RDF Message Microgrid message log
3
+ # ==================================
4
4
  #
5
5
  # This file is the data half of the runnable example:
6
6
  #
7
7
  # eyeling -r examples/rdf-message-microgrid.n3 examples/input/rdf-message-microgrid.trig
8
8
  #
9
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#> .
10
+ # data arrives as a true RDF Message Log: life-safety loads, power status,
11
+ # flexible demand, and an empty heartbeat. Eyeling parses the MESSAGE boundaries
12
+ # internally and exposes the replay to the N3 rules as eymsg: envelopes.
25
13
 
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" .
14
+ VERSION "1.2-messages"
15
+ PREFIX : <https://eyereasoner.github.io/eyeling/examples/rdf-message-microgrid#>
16
+ PREFIX see: <https://example.org/see#>
17
+ PREFIX in: <https://example.org/see/input#>
33
18
 
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 .
19
+ # Message 1: life-safety loads plus example metadata.
20
+ :oxygenConcentrator a :LifeSafetyLoad ;
21
+ :requiresWatts 500 ;
22
+ :serves :respiratoryCareRoom .
39
23
 
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 .
24
+ :vaccineFridge a :ColdChainLoad ;
25
+ :requiresWatts 120 ;
26
+ :serves :vaccineColdChain .
45
27
 
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 .
28
+ in:run a see:InputDataset ;
29
+ see:name "rdf_message_microgrid" ;
30
+ see:title "RDF Message Microgrid" ;
31
+ see:sourceFile "examples/rdf-message-microgrid.n3" ;
32
+ see:description "A true RDF 1.2 Message Log for a storm clinic microgrid. Eyeling parses MESSAGE delimiters into replayable internal message envelopes for bounded, explainable load-shedding." ;
33
+ see:compiler "Eyeling RDF Message Log input" .
51
34
 
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 .
35
+ MESSAGE
56
36
 
57
- in:lifeSafetyPayload {
58
- :oxygenConcentrator a :LifeSafetyLoad .
59
- :oxygenConcentrator :requiresWatts 500 .
60
- :oxygenConcentrator :serves :respiratoryCareRoom .
37
+ # Message 2: current power status.
38
+ :batteryBank a :PowerReserve ;
39
+ :availableWatts 650 ;
40
+ :state :islanded .
61
41
 
62
- :vaccineFridge a :ColdChainLoad .
63
- :vaccineFridge :requiresWatts 120 .
64
- :vaccineFridge :serves :vaccineColdChain .
65
- }
42
+ :solarForecast a :NearTermForecast ;
43
+ :expectedWatts 150 .
66
44
 
67
- in:powerPayload {
68
- :batteryBank a :PowerReserve .
69
- :batteryBank :availableWatts 650 .
70
- :batteryBank :state :islanded .
45
+ MESSAGE
71
46
 
72
- :solarForecast a :NearTermForecast .
73
- :solarForecast :expectedWatts 150 .
74
- }
47
+ # Message 3: flexible demand that can safely be shed.
48
+ :evChargers a :FlexibleLoad ;
49
+ :shedWatts 600 ;
50
+ :serves :staffVehicles .
75
51
 
76
- in:flexPayload {
77
- :evChargers a :FlexibleLoad .
78
- :evChargers :shedWatts 600 .
79
- :evChargers :serves :staffVehicles .
80
- }
52
+ MESSAGE
81
53
 
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
- }
54
+ # Message 4: empty heartbeat. The next MESSAGE delimiter finalizes this empty
55
+ # message and leaves no trailing payload.
56
+ MESSAGE
@@ -1,90 +1,47 @@
1
- # ==========================
2
- # RDF Messages input sidecar
3
- # ==========================
1
+ # ===============================
2
+ # RDF Messages parser-level input
3
+ # ===============================
4
4
  #
5
5
  # This file is the data half of the runnable example:
6
6
  #
7
7
  # eyeling -r examples/rdf-messages.n3 examples/input/rdf-messages.trig
8
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.
9
+ # It is a true RDF Message Log. VERSION "1.2-messages" tells Eyeling to parse
10
+ # MESSAGE delimiters as message boundaries before ordinary N3 reasoning starts.
11
+ # Eyeling then materializes an internal eymsg: replay view with ordered envelopes
12
+ # and one payload graph per non-empty message.
14
13
  #
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
-
28
- @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-messages#> .
29
- @prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-messages/vocab#> .
30
- @prefix prov: <http://www.w3.org/ns/prov#> .
31
- @prefix sosa: <http://www.w3.org/ns/sosa/> .
32
- @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
33
- @prefix math: <http://www.w3.org/2000/10/swap/math#> .
34
- @prefix list: <http://www.w3.org/2000/10/swap/list#> .
35
- @prefix log: <http://www.w3.org/2000/10/swap/log#> .
36
- @prefix string: <http://www.w3.org/2000/10/swap/string#> .
37
- @prefix see: <https://example.org/see#> .
38
- @prefix in: <https://example.org/see/input#> .
39
-
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 .
60
-
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 .
68
-
69
- in:payload001 {
70
- _:m001b0 a sosa:Observation .
71
- _:m001b0 sosa:madeBySensor :thermometerA .
72
- _:m001b0 sosa:resultTime "2026-05-12T18:20:00Z"^^xsd:dateTime .
73
- _:m001b0 sosa:hasSimpleResult 22 .
74
- }
75
-
76
- in:payload003 {
77
- _:m003b0 a sosa:Observation .
78
- _:m003b0 sosa:madeBySensor :thermometerA .
79
- _:m003b0 sosa:resultTime "2026-05-12T18:25:00Z"^^xsd:dateTime .
80
- _:m003b0 sosa:hasSimpleResult 23 .
81
- }
82
-
83
- in:metadata {
84
- in:run a see:InputDataset .
85
- in:run see:name "rdf_messages" .
86
- in:run see:title "RDF Messages" .
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." .
89
- in:run see:compiler "Eyeling RDF/TriG input sidecar" .
90
- }
14
+ # The second message is deliberately empty. The first and third messages both use
15
+ # the source-local blank-node label _:b0. Because blank-node labels are scoped to
16
+ # each RDF Message, Eyeling rewrites them into distinct internal blank nodes.
17
+
18
+ VERSION "1.2-messages"
19
+ PREFIX : <https://eyereasoner.github.io/eyeling/examples/rdf-messages#>
20
+ PREFIX sosa: <http://www.w3.org/ns/sosa/>
21
+ PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
22
+ PREFIX see: <https://example.org/see#>
23
+ PREFIX in: <https://example.org/see/input#>
24
+
25
+ # Message 1: first temperature observation plus example metadata.
26
+ _:b0 a sosa:Observation ;
27
+ sosa:madeBySensor :thermometerA ;
28
+ sosa:resultTime "2026-05-12T18:20:00Z"^^xsd:dateTime ;
29
+ sosa:hasSimpleResult 22 .
30
+
31
+ in:run a see:InputDataset ;
32
+ see:name "rdf_messages" ;
33
+ see:title "RDF Messages" ;
34
+ see:sourceFile "examples/rdf-messages.n3" ;
35
+ see:description "A true RDF 1.2 Message Log. Eyeling parses VERSION 1.2-messages and MESSAGE delimiters into replayable internal message envelopes." ;
36
+ see:compiler "Eyeling RDF Message Log input" .
37
+
38
+ MESSAGE
39
+
40
+ # Message 2: empty heartbeat.
41
+ MESSAGE
42
+
43
+ # Message 3: second observation. The _:b0 label is intentionally reused here.
44
+ _:b0 a sosa:Observation ;
45
+ sosa:madeBySensor :thermometerA ;
46
+ sosa:resultTime "2026-05-12T18:25:00Z"^^xsd:dateTime ;
47
+ sosa:hasSimpleResult 23 .
@@ -3,12 +3,10 @@
3
3
  ## Source files
4
4
 
5
5
  - [N3 rules](../rdf-message-microgrid.n3)
6
- - [Input TriG](../input/rdf-message-microgrid.trig)
6
+ - [Input RDF Message Log](../input/rdf-message-microgrid.trig)
7
7
 
8
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.
9
+ Storm clinic microgrid accepted: 4 parser-replayed RDF Messages were processed 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
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.
11
+ ## Why this is an RDF Message Log example
12
+ The input now uses VERSION \"1.2-messages\" and MESSAGE delimiters. Eyeling parses those boundaries internally into an eymsg: replay view, so the rules do not need hand-written application envelopes. Each non-empty message is inspected with log:includes inside its own payload formula, and the final delimiter-only message is replayed as an empty heartbeat. The decision combines only the derived conclusions needed for load shedding while keeping the explanation tied to explicit message boundaries.
@@ -3,12 +3,10 @@
3
3
  ## Source files
4
4
 
5
5
  - [N3 rules](../rdf-messages.n3)
6
- - [Input TriG](../input/rdf-messages.trig)
6
+ - [Input RDF Message Log](../input/rdf-messages.trig)
7
7
 
8
8
  ## Answer
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.
9
+ RDF Message Log accepted: 3 parser-replayed message boundaries are preserved. The middle message is an empty heartbeat, and the same source-local blank-node label is safely reused because Eyeling scopes blank nodes per message.
10
10
 
11
11
  ## Explanation
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.
12
+ The input now uses VERSION \"1.2-messages\" and MESSAGE delimiters instead of hand-written application envelope facts. Eyeling parses the log internally into an eymsg: replay view with ordered envelopes and one payload graph per non-empty message. The rules inspect each payload with log:includes inside its own message formula, 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 and remain contextualized by their message boundaries.
@@ -6,63 +6,46 @@
6
6
  #
7
7
  # eyeling -r examples/rdf-message-microgrid.n3 examples/input/rdf-message-microgrid.trig
8
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
9
+ # A clinic is running as an islanded microgrid after a storm. Several small
10
+ # systems send RDF data: one message describes life-safety loads, another
11
+ # describes battery and solar status, another describes flexible demand that may
14
12
  # be deferred, and a fourth message is an empty heartbeat.
15
13
  #
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.
14
+ # The companion input is now a true parser-level RDF Message Log. Eyeling handles
15
+ # VERSION "1.2-messages" and MESSAGE delimiters internally, then exposes a replay
16
+ # view using eymsg: ordered envelopes and payload graphs. The N3 rules therefore
17
+ # focus on the domain decision rather than recreating log replay by hand.
30
18
 
31
19
  @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#>.
20
+ @prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#>.
35
21
  @prefix math: <http://www.w3.org/2000/10/swap/math#>.
36
22
  @prefix list: <http://www.w3.org/2000/10/swap/list#>.
37
23
  @prefix log: <http://www.w3.org/2000/10/swap/log#>.
38
24
  @prefix string: <http://www.w3.org/2000/10/swap/string#>.
39
25
 
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.
26
+ # Every envelope in the replay view is an explicit parser-recognized boundary.
43
27
  {
44
- ?Log a rmsg:MessageLog;
45
- rmsg:message ?Message.
46
- ?Message a rmsg:MessageEnvelope;
47
- rmsg:offset ?Offset.
28
+ ?Stream a eymsg:RDFMessageStream;
29
+ eymsg:envelope ?Message.
30
+ ?Message a eymsg:MessageEnvelope;
31
+ eymsg:offset ?Offset.
48
32
  } => {
49
- ?Message rmsg:boundaryExplicit true.
50
- ?Log rmsg:replayContains ?Message.
33
+ ?Message :boundaryExplicit true.
34
+ ?Stream :replayContains ?Message.
51
35
  }.
52
36
 
53
37
  # Empty RDF Messages are valid. Here the fourth envelope is a heartbeat that
54
38
  # confirms the stream is still alive without adding any payload triples.
55
39
  {
56
- ?Message a rmsg:MessageEnvelope;
57
- rmsg:payloadKind :heartbeat.
40
+ ?Message a eymsg:MessageEnvelope;
41
+ eymsg:payloadKind eymsg:empty.
58
42
  } => {
59
- ?Message rmsg:emptyMessageAllowed true.
43
+ ?Message :emptyMessageAllowed true.
60
44
  }.
61
45
 
62
- # Inspect the life-safety message inside its own payload graph.
46
+ # Inspect the life-safety message inside its own parser-replayed payload graph.
63
47
  {
64
- ?Message rmsg:payloadKind :lifeSafetyLoads;
65
- rmsg:payloadGraph ?Payload.
48
+ ?Message eymsg:payloadGraph ?Payload.
66
49
  ?Payload log:nameOf ?PayloadContext.
67
50
  ?PayloadContext log:includes {
68
51
  :oxygenConcentrator :requiresWatts ?Oxygen.
@@ -70,14 +53,13 @@
70
53
  }.
71
54
  (?Oxygen ?Fridge) math:sum ?CriticalWatts.
72
55
  } => {
73
- :clinicMicrogrid rmsg:criticalWatts ?CriticalWatts.
74
- :clinicMicrogrid rmsg:mustKeep :oxygenConcentrator, :vaccineFridge.
56
+ :clinicMicrogrid :criticalWatts ?CriticalWatts.
57
+ :clinicMicrogrid :mustKeep :oxygenConcentrator, :vaccineFridge.
75
58
  }.
76
59
 
77
60
  # Inspect the power-status message atomically.
78
61
  {
79
- ?Message rmsg:payloadKind :powerStatus;
80
- rmsg:payloadGraph ?Payload.
62
+ ?Message eymsg:payloadGraph ?Payload.
81
63
  ?Payload log:nameOf ?PayloadContext.
82
64
  ?PayloadContext log:includes {
83
65
  :batteryBank :availableWatts ?BatteryWatts.
@@ -85,57 +67,61 @@
85
67
  }.
86
68
  (?BatteryWatts ?SolarWatts) math:sum ?AvailableWatts.
87
69
  } => {
88
- :clinicMicrogrid rmsg:availableWatts ?AvailableWatts.
70
+ :clinicMicrogrid :availableWatts ?AvailableWatts.
89
71
  }.
90
72
 
91
73
  # Inspect the flexible-demand message atomically. The EV chargers are useful, but
92
74
  # they are safe to defer so life-safety loads keep running.
93
75
  {
94
- ?Message rmsg:payloadKind :flexibleDemand;
95
- rmsg:payloadGraph ?Payload.
76
+ ?Message eymsg:payloadGraph ?Payload.
96
77
  ?Payload log:nameOf ?PayloadContext.
97
78
  ?PayloadContext log:includes {
98
79
  :evChargers :shedWatts ?ShedWatts.
99
80
  }.
100
81
  } => {
101
- :clinicMicrogrid rmsg:deferrableWatts ?ShedWatts.
102
- :clinicMicrogrid rmsg:mayDefer :evChargers.
82
+ :clinicMicrogrid :deferrableWatts ?ShedWatts.
83
+ :clinicMicrogrid :mayDefer :evChargers.
103
84
  }.
104
85
 
105
86
  # The decision combines conclusions from the separate messages, while preserving
106
87
  # the evidence that each conclusion came through an explicit message boundary.
107
88
  {
108
- :clinicMicrogrid rmsg:criticalWatts ?CriticalWatts;
109
- rmsg:availableWatts ?AvailableWatts;
110
- rmsg:deferrableWatts ?ShedWatts;
111
- rmsg:mustKeep :oxygenConcentrator, :vaccineFridge;
112
- rmsg:mayDefer :evChargers.
89
+ :clinicMicrogrid :criticalWatts ?CriticalWatts;
90
+ :availableWatts ?AvailableWatts;
91
+ :deferrableWatts ?ShedWatts;
92
+ :mustKeep :oxygenConcentrator, :vaccineFridge;
93
+ :mayDefer :evChargers.
113
94
  (?AvailableWatts ?ShedWatts) math:sum ?ProtectedBudget.
114
95
  ?ProtectedBudget math:greaterThan ?CriticalWatts.
115
96
  } => {
116
- :clinicMicrogrid rmsg:protectedBudgetWatts ?ProtectedBudget.
117
- :clinicMicrogrid rmsg:resilienceAction :protectClinic.
118
- :clinicMicrogrid rmsg:keeps :oxygenConcentrator, :vaccineFridge.
119
- :clinicMicrogrid rmsg:defers :evChargers.
97
+ :clinicMicrogrid :protectedBudgetWatts ?ProtectedBudget.
98
+ :clinicMicrogrid :resilienceAction :protectClinic.
99
+ :clinicMicrogrid :keeps :oxygenConcentrator, :vaccineFridge.
100
+ :clinicMicrogrid :defers :evChargers.
120
101
  }.
121
102
 
122
103
  # Emit the example report only when the message stream, empty heartbeat, atomic
123
104
  # payload inspection, and protection decision have all been derived.
124
105
  {
125
- :stormClinicLog rmsg:orderedMessages ?Messages.
106
+ ?Stream a eymsg:RDFMessageStream;
107
+ eymsg:orderedEnvelopes ?Messages;
108
+ eymsg:firstEnvelope ?M1.
126
109
  ?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.
110
+ ?M1 :boundaryExplicit true;
111
+ eymsg:nextEnvelope ?M2.
112
+ ?M2 :boundaryExplicit true;
113
+ eymsg:nextEnvelope ?M3.
114
+ ?M3 :boundaryExplicit true;
115
+ eymsg:nextEnvelope ?M4.
116
+ ?M4 :boundaryExplicit true;
117
+ :emptyMessageAllowed true.
118
+ :clinicMicrogrid :criticalWatts ?CriticalWatts;
119
+ :availableWatts ?AvailableWatts;
120
+ :deferrableWatts ?ShedWatts;
121
+ :protectedBudgetWatts ?ProtectedBudget;
122
+ :resilienceAction :protectClinic.
123
+ ("# rdf-message-microgrid\n\n## Source files\n\n- [N3 rules](../rdf-message-microgrid.n3)\n- [Input RDF Message Log](../input/rdf-message-microgrid.trig)\n\n## Answer\nStorm clinic microgrid accepted: %d parser-replayed RDF Messages were processed 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 Message Log example\nThe input now uses VERSION \\\"1.2-messages\\\" and MESSAGE delimiters. Eyeling parses those boundaries internally into an eymsg: replay view, so the rules do not need hand-written application envelopes. Each non-empty message is inspected with log:includes inside its own payload formula, and the final delimiter-only message is replayed as an empty heartbeat. The decision combines only the derived conclusions needed for load shedding while keeping the explanation tied to explicit message boundaries." ?Count ?CriticalWatts ?AvailableWatts ?ShedWatts ?ProtectedBudget) string:format ?Block.
138
124
  } => {
139
125
  :rdfMessageMicrogridExample log:outputString ?Block.
140
- :rdfMessageMicrogridExample :demonstrates :AtomicMessageContext, :EmptyHeartbeat, :ReplayableMessageLog, :ResilientDecisionSupport.
126
+ :rdfMessageMicrogridExample :demonstrates :ParserLevelMessageLog, :AtomicMessageContext, :EmptyHeartbeat, :ReplayableMessageLog, :ResilientDecisionSupport.
141
127
  }.
@@ -11,115 +11,102 @@
11
11
  # The RDF Messages draft defines an RDF Message as an RDF dataset interpreted
12
12
  # atomically, an RDF Message Stream as an ordered sequence of messages, and an
13
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
14
+ # that messages should not be combined by default and that blank-node labels are
15
15
  # scoped to the message in which they occur.
16
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:
17
+ # This example now uses the parser-level syntax directly: the companion input
18
+ # starts with VERSION "1.2-messages" and separates messages with MESSAGE
19
+ # delimiters. Eyeling handles those delimiters before N3 reasoning starts and
20
+ # exposes an internal eymsg: replay view: a stream resource, ordered envelopes,
21
+ # next-envelope links, payload kind, and one payload graph per non-empty message.
20
22
  #
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.
23
+ # The rules below validate that replay view. They never hand-model message
24
+ # envelopes in the TriG sidecar. They inspect payload graphs atomically, accept
25
+ # the delimiter-only heartbeat, and check that a reused source-local blank-node
26
+ # label becomes two distinct message-scoped blank nodes.
33
27
 
34
28
  @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-messages#>.
35
- @prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-messages/vocab#>.
36
- @prefix prov: <http://www.w3.org/ns/prov#>.
29
+ @prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#>.
37
30
  @prefix sosa: <http://www.w3.org/ns/sosa/>.
38
- @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
39
31
  @prefix math: <http://www.w3.org/2000/10/swap/math#>.
40
32
  @prefix list: <http://www.w3.org/2000/10/swap/list#>.
41
33
  @prefix log: <http://www.w3.org/2000/10/swap/log#>.
42
34
  @prefix string: <http://www.w3.org/2000/10/swap/string#>.
43
35
 
44
- # A message with an offset inside a log has an explicit replay boundary.
45
- # The envelope resource is application-local; the payload graph is the dataset
46
- # treated as the RDF Message.
36
+ # Every parser-replayed envelope is an explicit message boundary.
47
37
  {
48
- ?Log a rmsg:MessageLog;
49
- rmsg:message ?Message.
50
- ?Message a rmsg:MessageEnvelope;
51
- rmsg:offset ?Offset.
38
+ ?Stream a eymsg:RDFMessageStream;
39
+ eymsg:envelope ?Envelope.
40
+ ?Envelope a eymsg:MessageEnvelope;
41
+ eymsg:offset ?Offset.
52
42
  } => {
53
- ?Message rmsg:boundaryExplicit true.
54
- ?Log rmsg:replayContains ?Message.
43
+ ?Envelope :boundaryExplicit true.
44
+ ?Stream :replayContains ?Envelope.
55
45
  }.
56
46
 
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.
47
+ # Payload graphs are inspected with log:includes, so the observations remain
48
+ # inside their message contexts instead of being silently merged.
59
49
  {
60
- ?Message a rmsg:MessageEnvelope;
61
- rmsg:expectedResult ?Result;
62
- rmsg:payloadGraph ?Payload.
50
+ ?Envelope a eymsg:MessageEnvelope;
51
+ eymsg:payloadKind eymsg:nonEmpty;
52
+ eymsg:payloadGraph ?Payload.
63
53
  ?Payload log:nameOf ?PayloadContext.
64
54
  ?PayloadContext log:includes { ?Observation sosa:hasSimpleResult ?Result. }.
65
55
  } => {
66
- ?Message rmsg:payloadResult ?Result.
67
- }.
68
-
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.
71
- {
72
- ?Message a rmsg:MessageEnvelope;
73
- rmsg:payloadKind :heartbeat.
74
- } => {
75
- ?Message rmsg:emptyMessageAllowed true.
76
- :HeartbeatEvidence :accepted ?Message.
56
+ ?Envelope :payloadObservation ?Observation.
57
+ ?Envelope :payloadResult ?Result.
77
58
  }.
78
59
 
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.
60
+ # Empty messages are valid RDF Messages. The second envelope is a heartbeat even
61
+ # though it has no payload graph.
82
62
  {
83
- ?First a rmsg:MessageEnvelope;
84
- rmsg:localBlankLabel ?Label.
85
- ?Second a rmsg:MessageEnvelope;
86
- rmsg:localBlankLabel ?Label.
87
- ?First log:notEqualTo ?Second.
63
+ ?Envelope a eymsg:MessageEnvelope;
64
+ eymsg:payloadKind eymsg:empty.
88
65
  } => {
89
- :BlankNodeScope :reusedLabel ?Label.
90
- :BlankNodeScope :isPerMessage true.
66
+ ?Envelope :emptyMessageAllowed true.
67
+ :HeartbeatEvidence :accepted ?Envelope.
91
68
  }.
92
69
 
93
- # Different observations from the same sensor are not a global contradiction:
94
- # by default, message payloads remain separate communication contexts.
70
+ # The input deliberately reuses the same source-local blank-node label in message
71
+ # 1 and message 3. Eyeling rewrites labels per message, so these are distinct
72
+ # blank nodes after replay.
95
73
  {
96
- ?First a rmsg:MessageEnvelope;
97
- rmsg:payloadResult ?FirstResult.
98
- ?Second a rmsg:MessageEnvelope;
99
- rmsg:payloadResult ?SecondResult.
100
- ?FirstResult math:notEqualTo ?SecondResult.
101
- ?First log:notEqualTo ?Second.
74
+ ?Stream a eymsg:RDFMessageStream;
75
+ eymsg:firstEnvelope ?First.
76
+ ?First eymsg:nextEnvelope ?Second;
77
+ :payloadObservation ?FirstObservation;
78
+ :payloadResult ?FirstResult.
79
+ ?Second eymsg:nextEnvelope ?Third;
80
+ :emptyMessageAllowed true.
81
+ ?Third :payloadObservation ?ThirdObservation;
82
+ :payloadResult ?ThirdResult.
83
+ ?FirstObservation log:notEqualTo ?ThirdObservation.
84
+ ?FirstResult math:notEqualTo ?ThirdResult.
102
85
  } => {
86
+ :BlankNodeScope :reusedSourceLabelIsMessageScoped true.
103
87
  :MessageContext :differentObservationsStayContextual true.
104
88
  }.
105
89
 
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.
90
+ # The Eyeling verdict is emitted only when all parser-replayed message-specific
91
+ # validations have been derived from the stream, ordered envelopes, payload
92
+ # graphs, and built-ins.
109
93
  {
110
- :temperatureLog rmsg:orderedMessages ?Messages.
94
+ ?Stream a eymsg:RDFMessageStream;
95
+ eymsg:orderedEnvelopes ?Messages;
96
+ eymsg:firstEnvelope ?First.
111
97
  ?Messages list:length ?Count.
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.
118
- :BlankNodeScope :reusedLabel ?Label;
119
- :isPerMessage true.
98
+ ?First :boundaryExplicit true;
99
+ :payloadResult ?FirstResult;
100
+ eymsg:nextEnvelope ?Second.
101
+ ?Second :boundaryExplicit true;
102
+ :emptyMessageAllowed true;
103
+ eymsg:nextEnvelope ?Third.
104
+ ?Third :boundaryExplicit true;
105
+ :payloadResult ?SecondResult.
106
+ :BlankNodeScope :reusedSourceLabelIsMessageScoped true.
120
107
  :MessageContext :differentObservationsStayContextual true.
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.
108
+ ("# rdf-messages\n\n## Source files\n\n- [N3 rules](../rdf-messages.n3)\n- [Input RDF Message Log](../input/rdf-messages.trig)\n\n## Answer\nRDF Message Log accepted: %d parser-replayed message boundaries are preserved. The middle message is an empty heartbeat, and the same source-local blank-node label is safely reused because Eyeling scopes blank nodes per message.\n\n## Explanation\nThe input now uses VERSION \\\"1.2-messages\\\" and MESSAGE delimiters instead of hand-written application envelope facts. Eyeling parses the log internally into an eymsg: replay view with ordered envelopes and one payload graph per non-empty message. The rules inspect each payload with log:includes inside its own message formula, 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 and remain contextualized by their message boundaries." ?Count ?FirstResult ?SecondResult) string:format ?Block.
122
109
  } => {
123
110
  :rdfMessagesExample log:outputString ?Block.
124
- :rdfMessagesExample :demonstrates :ExplicitBoundaries, :AtomicMessageContext, :EmptyHeartbeat, :MessageScopedBlankNodes, :ReplayableMessageLog.
111
+ :rdfMessagesExample :demonstrates :ParserLevelMessageLog, :ExplicitBoundaries, :AtomicMessageContext, :EmptyHeartbeat, :MessageScopedBlankNodes.
125
112
  }.
package/eyeling.js CHANGED
@@ -1207,25 +1207,36 @@ function parseXsdDateTerm(t) {
1207
1207
  return d;
1208
1208
  }
1209
1209
 
1210
+ function isXsdDateTimeDatatype(dt) {
1211
+ return dt === XSD_NS + 'dateTime' || dt === XSD_NS + 'dateTimeStamp';
1212
+ }
1213
+
1210
1214
  function parseXsdDatetimeTerm(t) {
1211
1215
  if (!(t instanceof Literal)) return null;
1212
1216
  const [lex, dt] = literalParts(t.value);
1213
- if (dt !== XSD_NS + 'dateTime') return null;
1217
+ if (!isXsdDateTimeDatatype(dt)) return null;
1214
1218
  const val = stripQuotes(lex);
1219
+
1220
+ // xsd:dateTimeStamp is a subtype of xsd:dateTime with a required timezone.
1221
+ // Keep xsd:dateTime's existing permissive behaviour, but reject stamp
1222
+ // lexicals that do not actually carry the required timezone.
1223
+ if (dt === XSD_NS + 'dateTimeStamp' && !/(Z|[+-]\d{2}:\d{2})$/.test(val)) return null;
1224
+
1215
1225
  const d = new Date(val);
1216
1226
  if (Number.isNaN(d.getTime())) return null;
1217
1227
  return d; // Date in local/UTC, we only use timestamp
1218
1228
  }
1219
1229
 
1220
1230
  function parseXsdDateTimeLexParts(t) {
1221
- // Parse *lexical* components of an xsd:dateTime literal without timezone normalization.
1231
+ // Parse *lexical* components of an xsd:dateTime/dateTimeStamp literal without timezone normalization.
1222
1232
  // Returns { yearStr, month, day, hour, minute, second, tz } or null.
1223
1233
  if (!(t instanceof Literal)) return null;
1224
1234
  const [lex, dt] = literalParts(t.value);
1225
- if (dt !== XSD_NS + 'dateTime') return null;
1235
+ if (!isXsdDateTimeDatatype(dt)) return null;
1226
1236
  const val = stripQuotes(lex);
1227
1237
 
1228
1238
  // xsd:dateTime lexical: YYYY-MM-DDThh:mm:ss(.s+)?(Z|(+|-)hh:mm)?
1239
+ // xsd:dateTimeStamp has the same lexical form, but with the timezone required.
1229
1240
  const m = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.exec(val);
1230
1241
  if (!m) return null;
1231
1242
 
@@ -1236,6 +1247,7 @@ function parseXsdDateTimeLexParts(t) {
1236
1247
  const minute = parseInt(m[5], 10);
1237
1248
  const second = parseInt(m[6], 10);
1238
1249
  const tz = m[7] || null;
1250
+ if (dt === XSD_NS + 'dateTimeStamp' && !tz) return null;
1239
1251
 
1240
1252
  if (!(month >= 1 && month <= 12)) return null;
1241
1253
  if (!(day >= 1 && day <= 31)) return null;
@@ -1302,7 +1314,8 @@ function parseIso8601DurationToSeconds(s) {
1302
1314
  }
1303
1315
 
1304
1316
  function parseNumericForCompareTerm(t) {
1305
- // Strict: only accept xsd numeric literals, xsd:duration, xsd:date, xsd:dateTime
1317
+ // Strict: only accept xsd numeric literals, xsd:duration, xsd:date,
1318
+ // xsd:dateTime, and xsd:dateTimeStamp.
1306
1319
  // (or untyped numeric tokens).
1307
1320
  const bi = parseIntLiteral(t);
1308
1321
  if (bi !== null) return { kind: 'bigint', value: bi };
@@ -1369,7 +1382,7 @@ function parseNumOrDuration(t) {
1369
1382
  }
1370
1383
  }
1371
1384
 
1372
- // xsd:date / xsd:dateTime
1385
+ // xsd:date / xsd:dateTime / xsd:dateTimeStamp
1373
1386
  const dtval = parseDatetimeLike(t);
1374
1387
  if (dtval !== null) {
1375
1388
  return dtval.getTime() / 1000.0;
package/lib/builtins.js CHANGED
@@ -1196,25 +1196,36 @@ function parseXsdDateTerm(t) {
1196
1196
  return d;
1197
1197
  }
1198
1198
 
1199
+ function isXsdDateTimeDatatype(dt) {
1200
+ return dt === XSD_NS + 'dateTime' || dt === XSD_NS + 'dateTimeStamp';
1201
+ }
1202
+
1199
1203
  function parseXsdDatetimeTerm(t) {
1200
1204
  if (!(t instanceof Literal)) return null;
1201
1205
  const [lex, dt] = literalParts(t.value);
1202
- if (dt !== XSD_NS + 'dateTime') return null;
1206
+ if (!isXsdDateTimeDatatype(dt)) return null;
1203
1207
  const val = stripQuotes(lex);
1208
+
1209
+ // xsd:dateTimeStamp is a subtype of xsd:dateTime with a required timezone.
1210
+ // Keep xsd:dateTime's existing permissive behaviour, but reject stamp
1211
+ // lexicals that do not actually carry the required timezone.
1212
+ if (dt === XSD_NS + 'dateTimeStamp' && !/(Z|[+-]\d{2}:\d{2})$/.test(val)) return null;
1213
+
1204
1214
  const d = new Date(val);
1205
1215
  if (Number.isNaN(d.getTime())) return null;
1206
1216
  return d; // Date in local/UTC, we only use timestamp
1207
1217
  }
1208
1218
 
1209
1219
  function parseXsdDateTimeLexParts(t) {
1210
- // Parse *lexical* components of an xsd:dateTime literal without timezone normalization.
1220
+ // Parse *lexical* components of an xsd:dateTime/dateTimeStamp literal without timezone normalization.
1211
1221
  // Returns { yearStr, month, day, hour, minute, second, tz } or null.
1212
1222
  if (!(t instanceof Literal)) return null;
1213
1223
  const [lex, dt] = literalParts(t.value);
1214
- if (dt !== XSD_NS + 'dateTime') return null;
1224
+ if (!isXsdDateTimeDatatype(dt)) return null;
1215
1225
  const val = stripQuotes(lex);
1216
1226
 
1217
1227
  // xsd:dateTime lexical: YYYY-MM-DDThh:mm:ss(.s+)?(Z|(+|-)hh:mm)?
1228
+ // xsd:dateTimeStamp has the same lexical form, but with the timezone required.
1218
1229
  const m = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.exec(val);
1219
1230
  if (!m) return null;
1220
1231
 
@@ -1225,6 +1236,7 @@ function parseXsdDateTimeLexParts(t) {
1225
1236
  const minute = parseInt(m[5], 10);
1226
1237
  const second = parseInt(m[6], 10);
1227
1238
  const tz = m[7] || null;
1239
+ if (dt === XSD_NS + 'dateTimeStamp' && !tz) return null;
1228
1240
 
1229
1241
  if (!(month >= 1 && month <= 12)) return null;
1230
1242
  if (!(day >= 1 && day <= 31)) return null;
@@ -1291,7 +1303,8 @@ function parseIso8601DurationToSeconds(s) {
1291
1303
  }
1292
1304
 
1293
1305
  function parseNumericForCompareTerm(t) {
1294
- // Strict: only accept xsd numeric literals, xsd:duration, xsd:date, xsd:dateTime
1306
+ // Strict: only accept xsd numeric literals, xsd:duration, xsd:date,
1307
+ // xsd:dateTime, and xsd:dateTimeStamp.
1295
1308
  // (or untyped numeric tokens).
1296
1309
  const bi = parseIntLiteral(t);
1297
1310
  if (bi !== null) return { kind: 'bigint', value: bi };
@@ -1358,7 +1371,7 @@ function parseNumOrDuration(t) {
1358
1371
  }
1359
1372
  }
1360
1373
 
1361
- // xsd:date / xsd:dateTime
1374
+ // xsd:date / xsd:dateTime / xsd:dateTimeStamp
1362
1375
  const dtval = parseDatetimeLike(t);
1363
1376
  if (dtval !== null) {
1364
1377
  return dtval.getTime() / 1000.0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.24.31",
3
+ "version": "1.24.33",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -323,6 +323,35 @@ const cases = [
323
323
  assert.match(out, /\\"2023-04-01T18:06:04Z\\"\^\^xsd:dateTime \./);
324
324
  },
325
325
  },
326
+ {
327
+ name: '00d math comparison accepts xsd:dateTimeStamp with xsd:dateTime',
328
+ opt: { proofComments: false },
329
+ input: `
330
+ @prefix : <http://example.org/> .
331
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
332
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
333
+
334
+ {
335
+ "2026-05-30T00:00:00Z"^^xsd:dateTimeStamp math:notGreaterThan "2027-02-12T00:00:00Z"^^xsd:dateTime .
336
+ }
337
+ =>
338
+ {
339
+ :mixedDateTimeStampCompare :is true .
340
+ }.
341
+
342
+ {
343
+ "2026-05-30T00:00:00Z"^^xsd:dateTimeStamp math:notGreaterThan "2026-05-30T00:00:00Z"^^xsd:dateTimeStamp .
344
+ }
345
+ =>
346
+ {
347
+ :sameDateTimeStampCompare :is true .
348
+ }.
349
+ `,
350
+ expect: [
351
+ /:mixedDateTimeStampCompare\s+:is\s+true\s*\./,
352
+ /:sameDateTimeStampCompare\s+:is\s+true\s*\./,
353
+ ],
354
+ },
326
355
  {
327
356
  name: '01 forward rule: p -> q',
328
357
  opt: { proofComments: false },