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.
- package/dist/browser/eyeling.browser.js +18 -5
- package/examples/deck/rdf-message-flow.md +273 -0
- package/examples/input/rdf-message-microgrid.trig +39 -72
- package/examples/input/rdf-messages.trig +41 -84
- package/examples/output/rdf-message-microgrid.md +4 -6
- package/examples/output/rdf-messages.md +3 -5
- package/examples/rdf-message-microgrid.n3 +54 -68
- package/examples/rdf-messages.n3 +63 -76
- package/eyeling.js +18 -5
- package/lib/builtins.js +18 -5
- package/package.json +1 -1
- package/test/api.test.js +29 -0
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
35
|
-
:
|
|
36
|
-
:
|
|
37
|
-
:
|
|
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
|
-
:
|
|
41
|
-
:
|
|
42
|
-
:
|
|
43
|
-
:m002 rmsg:payloadKind :powerStatus .
|
|
44
|
-
:m002 rmsg:payloadGraph in:powerPayload .
|
|
24
|
+
:vaccineFridge a :ColdChainLoad ;
|
|
25
|
+
:requiresWatts 120 ;
|
|
26
|
+
:serves :vaccineColdChain .
|
|
45
27
|
|
|
46
|
-
:
|
|
47
|
-
:
|
|
48
|
-
:
|
|
49
|
-
:
|
|
50
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
:
|
|
60
|
-
:
|
|
37
|
+
# Message 2: current power status.
|
|
38
|
+
:batteryBank a :PowerReserve ;
|
|
39
|
+
:availableWatts 650 ;
|
|
40
|
+
:state :islanded .
|
|
61
41
|
|
|
62
|
-
|
|
63
|
-
:
|
|
64
|
-
:vaccineFridge :serves :vaccineColdChain .
|
|
65
|
-
}
|
|
42
|
+
:solarForecast a :NearTermForecast ;
|
|
43
|
+
:expectedWatts 150 .
|
|
66
44
|
|
|
67
|
-
|
|
68
|
-
:batteryBank a :PowerReserve .
|
|
69
|
-
:batteryBank :availableWatts 650 .
|
|
70
|
-
:batteryBank :state :islanded .
|
|
45
|
+
MESSAGE
|
|
71
46
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
47
|
+
# Message 3: flexible demand that can safely be shed.
|
|
48
|
+
:evChargers a :FlexibleLoad ;
|
|
49
|
+
:shedWatts 600 ;
|
|
50
|
+
:serves :staffVehicles .
|
|
75
51
|
|
|
76
|
-
|
|
77
|
-
:evChargers a :FlexibleLoad .
|
|
78
|
-
:evChargers :shedWatts 600 .
|
|
79
|
-
:evChargers :serves :staffVehicles .
|
|
80
|
-
}
|
|
52
|
+
MESSAGE
|
|
81
53
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
:
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
:
|
|
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
|
|
6
|
+
- [Input RDF Message Log](../input/rdf-message-microgrid.trig)
|
|
7
7
|
|
|
8
8
|
## Answer
|
|
9
|
-
Storm clinic microgrid accepted: 4 RDF
|
|
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
|
|
12
|
-
The input
|
|
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
|
|
6
|
+
- [Input RDF Message Log](../input/rdf-messages.trig)
|
|
7
7
|
|
|
8
8
|
## Answer
|
|
9
|
-
RDF Message
|
|
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
|
|
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
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# 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
|
|
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
|
|
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
|
-
?
|
|
45
|
-
|
|
46
|
-
?Message a
|
|
47
|
-
|
|
28
|
+
?Stream a eymsg:RDFMessageStream;
|
|
29
|
+
eymsg:envelope ?Message.
|
|
30
|
+
?Message a eymsg:MessageEnvelope;
|
|
31
|
+
eymsg:offset ?Offset.
|
|
48
32
|
} => {
|
|
49
|
-
?Message
|
|
50
|
-
?
|
|
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
|
|
57
|
-
|
|
40
|
+
?Message a eymsg:MessageEnvelope;
|
|
41
|
+
eymsg:payloadKind eymsg:empty.
|
|
58
42
|
} => {
|
|
59
|
-
?Message
|
|
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
|
|
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
|
|
74
|
-
:clinicMicrogrid
|
|
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
|
|
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
|
|
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
|
|
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
|
|
102
|
-
:clinicMicrogrid
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
117
|
-
:clinicMicrogrid
|
|
118
|
-
:clinicMicrogrid
|
|
119
|
-
:clinicMicrogrid
|
|
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
|
-
|
|
106
|
+
?Stream a eymsg:RDFMessageStream;
|
|
107
|
+
eymsg:orderedEnvelopes ?Messages;
|
|
108
|
+
eymsg:firstEnvelope ?M1.
|
|
126
109
|
?Messages list:length ?Count.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
}.
|
package/examples/rdf-messages.n3
CHANGED
|
@@ -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
|
|
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
|
|
18
|
-
#
|
|
19
|
-
# delimiters.
|
|
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
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
-
?
|
|
49
|
-
|
|
50
|
-
?
|
|
51
|
-
|
|
38
|
+
?Stream a eymsg:RDFMessageStream;
|
|
39
|
+
eymsg:envelope ?Envelope.
|
|
40
|
+
?Envelope a eymsg:MessageEnvelope;
|
|
41
|
+
eymsg:offset ?Offset.
|
|
52
42
|
} => {
|
|
53
|
-
?
|
|
54
|
-
?
|
|
43
|
+
?Envelope :boundaryExplicit true.
|
|
44
|
+
?Stream :replayContains ?Envelope.
|
|
55
45
|
}.
|
|
56
46
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
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
|
-
?
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
?
|
|
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
|
-
#
|
|
80
|
-
#
|
|
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
|
-
?
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
:
|
|
66
|
+
?Envelope :emptyMessageAllowed true.
|
|
67
|
+
:HeartbeatEvidence :accepted ?Envelope.
|
|
91
68
|
}.
|
|
92
69
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
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
|
-
?
|
|
97
|
-
|
|
98
|
-
?
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
?
|
|
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
|
|
107
|
-
# been derived from the
|
|
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
|
-
|
|
94
|
+
?Stream a eymsg:RDFMessageStream;
|
|
95
|
+
eymsg:orderedEnvelopes ?Messages;
|
|
96
|
+
eymsg:firstEnvelope ?First.
|
|
111
97
|
?Messages list:length ?Count.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
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 },
|