eyeling 1.24.31 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/HANDBOOK.md CHANGED
@@ -793,7 +793,7 @@ Implementation: deterministic Skolem IDs live in `lib/skolem.js`; the per-firing
793
793
  A rule whose conclusion is `false` is treated as a hard failure. During forward chaining:
794
794
 
795
795
  - Eyeling proves the premise (it only needs one solution)
796
- - if the premise is provable, it prints a message and exits with status code 2
796
+ - if the premise is provable, it prints a message and exits with status code 65 (`EX_DATAERR` in Unix `sysexits.h` terminology)
797
797
 
798
798
  This is Eyeling’s way to express hard consistency checks and detect inconsistencies.
799
799
 
@@ -3417,7 +3417,7 @@ So Eyeling is not only implementing the semantics document; it is also defining
3417
3417
 
3418
3418
  #### G.2.4 Inference fuses (`=> false`) are an engine-level procedural feature
3419
3419
 
3420
- The semantics document discusses `false` in relation to implication and constraints. Eyeling turns `{ ... } => false` into an engine-level hard failure with a visible message and failing exit status. That is a practical tooling feature: it lets a rule act like a checked invariant.
3420
+ The semantics document discusses `false` in relation to implication and constraints. Eyeling turns `{ ... } => false` into an engine-level hard failure with a visible message and exit status 65 (`EX_DATAERR`). That is a practical tooling feature: it lets a rule act like a checked invariant.
3421
3421
 
3422
3422
  This is very useful in real programs, but it is an operational behavior of the reasoner, not something a model-theoretic semantics “executes.”
3423
3423
 
@@ -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;
@@ -5504,6 +5517,10 @@ const {
5504
5517
  copyQuotedGraphMetadata,
5505
5518
  } = require('./prelude');
5506
5519
 
5520
+ // Inference fuses use sysexits.h EX_DATAERR (65): input/rules made a
5521
+ // forbidden condition provable, rather than a generic usage/runtime error.
5522
+ const INFERENCE_FUSE_EXIT_CODE = 65;
5523
+
5507
5524
  // In N3/Turtle, rdf:nil is the canonical IRI for the empty RDF list.
5508
5525
  // Eyeling represents list literals with ListTerm; ensure rdf:nil unifies with ().
5509
5526
  const RDF_NIL_IRI = RDF_NS + 'nil';
@@ -8432,7 +8449,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8432
8449
  // Allow dynamic fuses: ... => ?X. where ?X becomes false
8433
8450
  if (dynTerm instanceof Literal && dynTerm.value === 'false') {
8434
8451
  __printTriggeredFuse(r, opts && opts.prefixes, s, 'Dynamic head resolved to false.');
8435
- __exitReasoning(2, 'Inference fuse triggered.');
8452
+ __exitReasoning(INFERENCE_FUSE_EXIT_CODE, 'Inference fuse triggered.');
8436
8453
  }
8437
8454
 
8438
8455
  const dynTriples = __graphTriplesOrTrue(dynTerm);
@@ -8593,7 +8610,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8593
8610
  // Inference fuse
8594
8611
  if (r.isFuse && sols.length) {
8595
8612
  __printTriggeredFuse(r, opts && opts.prefixes, sols[0]);
8596
- __exitReasoning(2, 'Inference fuse triggered.');
8613
+ __exitReasoning(INFERENCE_FUSE_EXIT_CODE, 'Inference fuse triggered.');
8597
8614
  }
8598
8615
 
8599
8616
  for (const s of sols) {
@@ -9130,6 +9147,7 @@ module.exports = {
9130
9147
  registerBuiltinModule,
9131
9148
  loadBuiltinModule,
9132
9149
  listBuiltinIris,
9150
+ INFERENCE_FUSE_EXIT_CODE,
9133
9151
  };
9134
9152
 
9135
9153
  };
@@ -9157,6 +9175,7 @@ module.exports = {
9157
9175
  rdfjs: dataFactory,
9158
9176
  main: engine.main,
9159
9177
  version: engine.version,
9178
+ INFERENCE_FUSE_EXIT_CODE: engine.INFERENCE_FUSE_EXIT_CODE,
9160
9179
 
9161
9180
  // internals for playground.html
9162
9181
  lex: engine.lex,
@@ -10,6 +10,8 @@ function getBrowserApi() {
10
10
  return api;
11
11
  }
12
12
 
13
+ export const INFERENCE_FUSE_EXIT_CODE = 65;
14
+
13
15
  export function reasonStream(input, opts) {
14
16
  return getBrowserApi().reasonStream(input, opts);
15
17
  }
@@ -64,6 +66,7 @@ const eyeling = {
64
66
  get version() {
65
67
  return getBrowserApi().version;
66
68
  },
69
+ INFERENCE_FUSE_EXIT_CODE,
67
70
  reasonStream,
68
71
  reasonRdfJs,
69
72
  rdfjs,
@@ -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.
package/examples/fuse.n3 CHANGED
@@ -2,7 +2,7 @@
2
2
  # Inference fuse
3
3
  # ==============
4
4
 
5
- # expect-exit: 2
5
+ # expect-exit: 65
6
6
 
7
7
  @prefix : <https://eyereasoner.github.io/ns#>.
8
8
 
@@ -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