eyeling 1.24.25 → 1.24.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/browser/eyeling.browser.js +76 -2
- package/examples/input/rdf-message-flow.trig +87 -59
- package/examples/input/rdf-message-microgrid.trig +89 -0
- package/examples/input/rdf-messages.trig +61 -40
- package/examples/output/rdf-message-flow.md +2 -2
- package/examples/output/rdf-message-microgrid.md +14 -0
- package/examples/output/rdf-messages.md +4 -2
- package/examples/rdf-message-flow.n3 +84 -69
- package/examples/rdf-message-microgrid.n3 +141 -0
- package/examples/rdf-messages.n3 +72 -52
- package/eyeling.js +76 -2
- package/lib/builtins.js +62 -2
- package/lib/parser.js +14 -0
- package/package.json +1 -1
- package/test/api.test.js +166 -29
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
# ================
|
|
2
2
|
# RDF Message Flow
|
|
3
3
|
# ================
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
4
|
+
#
|
|
5
|
+
# Run as:
|
|
6
|
+
#
|
|
7
|
+
# eyeling -r examples/rdf-message-flow.n3 examples/input/rdf-message-flow.trig
|
|
8
|
+
#
|
|
9
|
+
# Motivation
|
|
10
|
+
# ----------
|
|
11
|
+
# The RDF Messages draft describes an RDF Message as an RDF dataset interpreted
|
|
12
|
+
# atomically, and an RDF Message Stream as an ordered sequence of such messages.
|
|
13
|
+
# It also defines RDF Message Logs with explicit MESSAGE delimiters. This Eyeling
|
|
14
|
+
# example keeps the same message-level discipline while staying in ordinary
|
|
15
|
+
# N3/TriG that Eyeling can reason over today.
|
|
16
|
+
#
|
|
17
|
+
# The companion TriG file therefore separates two layers:
|
|
18
|
+
#
|
|
19
|
+
# 1. example-local envelope facts in the default graph, such as order,
|
|
20
|
+
# processing stage, and payload graph; and
|
|
21
|
+
# 2. one named graph per non-empty message payload, treated here as the RDF
|
|
22
|
+
# dataset/message that must be inspected atomically.
|
|
23
|
+
#
|
|
24
|
+
# The rules below deliberately do not merge all payload graphs into one global
|
|
25
|
+
# graph. Instead, each observation is checked with log:includes inside the named
|
|
26
|
+
# payload formula. The empty heartbeat has no payload graph but still advances
|
|
27
|
+
# through the same pipeline. The next message is released only after the current
|
|
28
|
+
# one reaches the sink, making the example a small back-pressure / flow-control
|
|
29
|
+
# story rather than a batch merge of all input triples.
|
|
9
30
|
|
|
10
31
|
@prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-message-flow#>.
|
|
11
|
-
@prefix
|
|
32
|
+
@prefix flow: <https://eyereasoner.github.io/eyeling/examples/rdf-message-flow/vocab#>.
|
|
12
33
|
@prefix prov: <http://www.w3.org/ns/prov#>.
|
|
13
34
|
@prefix sosa: <http://www.w3.org/ns/sosa/>.
|
|
14
35
|
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
|
|
@@ -17,126 +38,120 @@
|
|
|
17
38
|
@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
18
39
|
@prefix string: <http://www.w3.org/2000/10/swap/string#>.
|
|
19
40
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Empty heartbeat: it flows through the same pipeline and keeps the stream live.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# Stage 1: once a message has entered the stream, validate its envelope.
|
|
31
|
-
|
|
32
|
-
{ ?Message :atStage :ingest. } => {
|
|
33
|
-
?Message :atStage :validate.
|
|
41
|
+
# Stage 1: once an envelope has entered the stream, validate it.
|
|
42
|
+
{ ?Envelope :atStage :ingest. } => {
|
|
43
|
+
?Envelope :atStage :validate.
|
|
34
44
|
}.
|
|
35
45
|
|
|
36
|
-
# Stage 2: validation
|
|
46
|
+
# Stage 2: validation records that the envelope keeps an explicit message
|
|
47
|
+
# boundary before the payload is interpreted.
|
|
37
48
|
{
|
|
38
|
-
?
|
|
39
|
-
a
|
|
40
|
-
|
|
49
|
+
?Envelope :atStage :validate;
|
|
50
|
+
a flow:MessageEnvelope;
|
|
51
|
+
flow:offset ?Offset.
|
|
41
52
|
} => {
|
|
42
|
-
?
|
|
43
|
-
?
|
|
53
|
+
?Envelope flow:boundaryExplicit true.
|
|
54
|
+
?Envelope :atStage :interpret.
|
|
44
55
|
}.
|
|
45
56
|
|
|
46
|
-
# Stage 3a: observation payloads are inspected inside their
|
|
57
|
+
# Stage 3a: observation payloads are inspected inside their own payload graph.
|
|
58
|
+
# This models the RDF Messages idea that messages are separate atomic datasets.
|
|
47
59
|
{
|
|
48
|
-
?
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
?Envelope :atStage :interpret;
|
|
61
|
+
flow:payloadKind :observation;
|
|
62
|
+
flow:expectedResult ?Result;
|
|
63
|
+
flow:payloadGraph ?Payload.
|
|
52
64
|
?Payload log:nameOf ?PayloadContext.
|
|
53
65
|
?PayloadContext log:includes { ?Observation sosa:hasSimpleResult ?Result. }.
|
|
54
66
|
} => {
|
|
55
|
-
?
|
|
56
|
-
?
|
|
67
|
+
?Envelope flow:payloadResult ?Result.
|
|
68
|
+
?Envelope :atStage :route.
|
|
57
69
|
}.
|
|
58
70
|
|
|
59
|
-
# Stage 3b: empty heartbeats
|
|
71
|
+
# Stage 3b: empty heartbeats contain no quads, but RDF Messages explicitly allow
|
|
72
|
+
# empty messages, so the envelope still moves through the flow.
|
|
60
73
|
{
|
|
61
|
-
?
|
|
62
|
-
|
|
74
|
+
?Envelope :atStage :interpret;
|
|
75
|
+
flow:payloadKind :heartbeat.
|
|
63
76
|
} => {
|
|
64
|
-
?
|
|
65
|
-
?
|
|
77
|
+
?Envelope flow:emptyMessageAllowed true.
|
|
78
|
+
?Envelope :atStage :route.
|
|
66
79
|
}.
|
|
67
80
|
|
|
68
81
|
# Stage 4a: hot observations are routed to the alert sink.
|
|
69
82
|
{
|
|
70
|
-
?
|
|
71
|
-
|
|
83
|
+
?Envelope :atStage :route;
|
|
84
|
+
flow:payloadResult ?Result.
|
|
72
85
|
:temperatureFlow :highThreshold ?Threshold.
|
|
73
86
|
?Result math:greaterThan ?Threshold.
|
|
74
87
|
} => {
|
|
75
|
-
?
|
|
76
|
-
?
|
|
77
|
-
:alertSink :received ?
|
|
88
|
+
?Envelope :route :alertSink.
|
|
89
|
+
?Envelope :atStage :sink.
|
|
90
|
+
:alertSink :received ?Envelope.
|
|
78
91
|
}.
|
|
79
92
|
|
|
80
93
|
# Stage 4b: normal observations are routed to the archive sink.
|
|
81
94
|
{
|
|
82
|
-
?
|
|
83
|
-
|
|
95
|
+
?Envelope :atStage :route;
|
|
96
|
+
flow:payloadResult ?Result.
|
|
84
97
|
:temperatureFlow :highThreshold ?Threshold.
|
|
85
98
|
?Result math:notGreaterThan ?Threshold.
|
|
86
99
|
} => {
|
|
87
|
-
?
|
|
88
|
-
?
|
|
89
|
-
:archiveSink :received ?
|
|
100
|
+
?Envelope :route :archiveSink.
|
|
101
|
+
?Envelope :atStage :sink.
|
|
102
|
+
:archiveSink :received ?Envelope.
|
|
90
103
|
}.
|
|
91
104
|
|
|
92
105
|
# Stage 4c: heartbeats are routed separately from observations.
|
|
93
106
|
{
|
|
94
|
-
?
|
|
95
|
-
|
|
107
|
+
?Envelope :atStage :route;
|
|
108
|
+
flow:emptyMessageAllowed true.
|
|
96
109
|
} => {
|
|
97
|
-
?
|
|
98
|
-
?
|
|
99
|
-
:heartbeatSink :received ?
|
|
110
|
+
?Envelope :route :heartbeatSink.
|
|
111
|
+
?Envelope :atStage :sink.
|
|
112
|
+
:heartbeatSink :received ?Envelope.
|
|
100
113
|
}.
|
|
101
114
|
|
|
102
|
-
# Continuous-flow rule: reaching the sink releases the next
|
|
115
|
+
# Continuous-flow rule: reaching the sink releases the next envelope into ingress.
|
|
116
|
+
# This mirrors a consumer-visible ordered stream: later messages are not processed
|
|
117
|
+
# until the earlier message has completed the pipeline.
|
|
103
118
|
{
|
|
104
|
-
?
|
|
105
|
-
|
|
106
|
-
?Next a
|
|
119
|
+
?Envelope :atStage :sink;
|
|
120
|
+
flow:nextEnvelope ?Next.
|
|
121
|
+
?Next a flow:MessageEnvelope.
|
|
107
122
|
} => {
|
|
108
|
-
?
|
|
123
|
+
?Envelope :releases ?Next.
|
|
109
124
|
?Next :atStage :ingest.
|
|
110
125
|
}.
|
|
111
126
|
|
|
112
|
-
# The Eyeling verdict is emitted only after all five
|
|
113
|
-
# ingest, validation, interpretation, routing, and sink
|
|
127
|
+
# The Eyeling verdict is emitted only after all five envelopes have flowed
|
|
128
|
+
# through ingest, validation, interpretation, routing, and sink while preserving
|
|
129
|
+
# their message boundaries.
|
|
114
130
|
{
|
|
115
|
-
:temperatureFlow
|
|
131
|
+
:temperatureFlow flow:orderedEnvelopes ?Envelopes;
|
|
116
132
|
:highThreshold ?Threshold.
|
|
117
|
-
?
|
|
133
|
+
?Envelopes list:length ?Count.
|
|
118
134
|
:m001 :atStage :sink;
|
|
119
|
-
|
|
135
|
+
flow:payloadResult ?FirstResult;
|
|
120
136
|
:route :archiveSink;
|
|
121
137
|
:releases :m002.
|
|
122
138
|
:m002 :atStage :sink;
|
|
123
|
-
|
|
139
|
+
flow:payloadResult ?SecondResult;
|
|
124
140
|
:route :archiveSink;
|
|
125
141
|
:releases :m003.
|
|
126
142
|
:m003 :atStage :sink;
|
|
127
|
-
|
|
143
|
+
flow:emptyMessageAllowed true;
|
|
128
144
|
:route :heartbeatSink;
|
|
129
145
|
:releases :m004.
|
|
130
146
|
:m004 :atStage :sink;
|
|
131
|
-
|
|
147
|
+
flow:payloadResult ?FourthResult;
|
|
132
148
|
:route :alertSink;
|
|
133
149
|
:releases :m005.
|
|
134
150
|
:m005 :atStage :sink;
|
|
135
|
-
|
|
151
|
+
flow:payloadResult ?FifthResult;
|
|
136
152
|
:route :alertSink.
|
|
137
|
-
("# rdf-message-flow\n\n## Source files\n\n- [N3 rules](../rdf-message-flow.n3)\n- [Input TriG](../input/rdf-message-flow.trig)\n\n## Answer\nContinuous RDF Message flow accepted: %d ordered
|
|
153
|
+
("# rdf-message-flow\n\n## Source files\n\n- [N3 rules](../rdf-message-flow.n3)\n- [Input TriG](../input/rdf-message-flow.trig)\n\n## Answer\nContinuous RDF Message flow accepted: %d ordered message envelopes moved through the ingest → validate → interpret → route → sink pipeline. The threshold was %d, so results %s and %s were archived, the heartbeat kept the stream alive, and results %s and %s were emitted as alerts.\n\n## Explanation\nThe input is a single runnable example split across an N3 rule file and a TriG sidecar. The TriG file uses example-local envelope facts for stream order and processing state, while each named payload graph is treated as an atomic RDF Message dataset. Only :m001 starts at ingress; each envelope must reach :sink before the continuous-flow rule releases its flow:nextEnvelope. Observation payloads are inspected with log:includes inside their own payload formula, and the empty heartbeat advances without a payload graph. This keeps message boundaries visible to the reasoner instead of merging all payload triples into one global graph." ?Count ?Threshold ?FirstResult ?SecondResult ?FourthResult ?FifthResult) string:format ?Block.
|
|
138
154
|
} => {
|
|
139
155
|
:rdfMessageFlowExample log:outputString ?Block.
|
|
140
156
|
:rdfMessageFlowExample :demonstrates :ContinuousFlow, :BackPressureRelease, :AtomicMessageContext, :HeartbeatInFlow, :ThresholdRouting.
|
|
141
157
|
}.
|
|
142
|
-
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# =====================
|
|
2
|
+
# RDF Message Microgrid
|
|
3
|
+
# =====================
|
|
4
|
+
#
|
|
5
|
+
# Run as:
|
|
6
|
+
#
|
|
7
|
+
# eyeling -r examples/rdf-message-microgrid.n3 examples/input/rdf-message-microgrid.trig
|
|
8
|
+
#
|
|
9
|
+
# Motivation
|
|
10
|
+
# ----------
|
|
11
|
+
# A clinic has lost grid power during a storm. Several small systems continue to
|
|
12
|
+
# send RDF data: one message describes life-safety loads, another describes the
|
|
13
|
+
# current battery and solar situation, another describes flexible demand that may
|
|
14
|
+
# be deferred, and a fourth message is an empty heartbeat.
|
|
15
|
+
#
|
|
16
|
+
# The RDF Messages draft treats each message as an RDF dataset intended to be
|
|
17
|
+
# interpreted atomically. It also treats a message stream as an ordered sequence
|
|
18
|
+
# and allows empty messages. This example models those ideas in ordinary TriG/N3:
|
|
19
|
+
# the companion TriG file contains application-local envelope facts in the
|
|
20
|
+
# default graph, and each non-empty payload is placed in its own named graph.
|
|
21
|
+
#
|
|
22
|
+
# The point is not only technical. Atomic messages let a small reasoner make a
|
|
23
|
+
# careful decision under pressure: protect the oxygen concentrator and vaccine
|
|
24
|
+
# fridge, defer the EV chargers, and explain the action from the replayed message
|
|
25
|
+
# boundaries instead of silently merging every payload into one timeless graph.
|
|
26
|
+
#
|
|
27
|
+
# This is intentionally not a parser-level VERSION "1.2-messages" / MESSAGE
|
|
28
|
+
# delimiter test. It is a reasoning example over an already-materialized sidecar
|
|
29
|
+
# representation of a message log.
|
|
30
|
+
|
|
31
|
+
@prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-message-microgrid#>.
|
|
32
|
+
@prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-message-microgrid/vocab#>.
|
|
33
|
+
@prefix prov: <http://www.w3.org/ns/prov#>.
|
|
34
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
|
|
35
|
+
@prefix math: <http://www.w3.org/2000/10/swap/math#>.
|
|
36
|
+
@prefix list: <http://www.w3.org/2000/10/swap/list#>.
|
|
37
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
38
|
+
@prefix string: <http://www.w3.org/2000/10/swap/string#>.
|
|
39
|
+
|
|
40
|
+
# Every envelope with an offset is an explicit replay boundary. The envelope is
|
|
41
|
+
# application-local metadata; the payload graph is the dataset/message being
|
|
42
|
+
# interpreted atomically.
|
|
43
|
+
{
|
|
44
|
+
?Log a rmsg:MessageLog;
|
|
45
|
+
rmsg:message ?Message.
|
|
46
|
+
?Message a rmsg:MessageEnvelope;
|
|
47
|
+
rmsg:offset ?Offset.
|
|
48
|
+
} => {
|
|
49
|
+
?Message rmsg:boundaryExplicit true.
|
|
50
|
+
?Log rmsg:replayContains ?Message.
|
|
51
|
+
}.
|
|
52
|
+
|
|
53
|
+
# Empty RDF Messages are valid. Here the fourth envelope is a heartbeat that
|
|
54
|
+
# confirms the stream is still alive without adding any payload triples.
|
|
55
|
+
{
|
|
56
|
+
?Message a rmsg:MessageEnvelope;
|
|
57
|
+
rmsg:payloadKind :heartbeat.
|
|
58
|
+
} => {
|
|
59
|
+
?Message rmsg:emptyMessageAllowed true.
|
|
60
|
+
}.
|
|
61
|
+
|
|
62
|
+
# Inspect the life-safety message inside its own payload graph.
|
|
63
|
+
{
|
|
64
|
+
?Message rmsg:payloadKind :lifeSafetyLoads;
|
|
65
|
+
rmsg:payloadGraph ?Payload.
|
|
66
|
+
?Payload log:nameOf ?PayloadContext.
|
|
67
|
+
?PayloadContext log:includes {
|
|
68
|
+
:oxygenConcentrator :requiresWatts ?Oxygen.
|
|
69
|
+
:vaccineFridge :requiresWatts ?Fridge.
|
|
70
|
+
}.
|
|
71
|
+
(?Oxygen ?Fridge) math:sum ?CriticalWatts.
|
|
72
|
+
} => {
|
|
73
|
+
:clinicMicrogrid rmsg:criticalWatts ?CriticalWatts.
|
|
74
|
+
:clinicMicrogrid rmsg:mustKeep :oxygenConcentrator, :vaccineFridge.
|
|
75
|
+
}.
|
|
76
|
+
|
|
77
|
+
# Inspect the power-status message atomically.
|
|
78
|
+
{
|
|
79
|
+
?Message rmsg:payloadKind :powerStatus;
|
|
80
|
+
rmsg:payloadGraph ?Payload.
|
|
81
|
+
?Payload log:nameOf ?PayloadContext.
|
|
82
|
+
?PayloadContext log:includes {
|
|
83
|
+
:batteryBank :availableWatts ?BatteryWatts.
|
|
84
|
+
:solarForecast :expectedWatts ?SolarWatts.
|
|
85
|
+
}.
|
|
86
|
+
(?BatteryWatts ?SolarWatts) math:sum ?AvailableWatts.
|
|
87
|
+
} => {
|
|
88
|
+
:clinicMicrogrid rmsg:availableWatts ?AvailableWatts.
|
|
89
|
+
}.
|
|
90
|
+
|
|
91
|
+
# Inspect the flexible-demand message atomically. The EV chargers are useful, but
|
|
92
|
+
# they are safe to defer so life-safety loads keep running.
|
|
93
|
+
{
|
|
94
|
+
?Message rmsg:payloadKind :flexibleDemand;
|
|
95
|
+
rmsg:payloadGraph ?Payload.
|
|
96
|
+
?Payload log:nameOf ?PayloadContext.
|
|
97
|
+
?PayloadContext log:includes {
|
|
98
|
+
:evChargers :shedWatts ?ShedWatts.
|
|
99
|
+
}.
|
|
100
|
+
} => {
|
|
101
|
+
:clinicMicrogrid rmsg:deferrableWatts ?ShedWatts.
|
|
102
|
+
:clinicMicrogrid rmsg:mayDefer :evChargers.
|
|
103
|
+
}.
|
|
104
|
+
|
|
105
|
+
# The decision combines conclusions from the separate messages, while preserving
|
|
106
|
+
# the evidence that each conclusion came through an explicit message boundary.
|
|
107
|
+
{
|
|
108
|
+
:clinicMicrogrid rmsg:criticalWatts ?CriticalWatts;
|
|
109
|
+
rmsg:availableWatts ?AvailableWatts;
|
|
110
|
+
rmsg:deferrableWatts ?ShedWatts;
|
|
111
|
+
rmsg:mustKeep :oxygenConcentrator, :vaccineFridge;
|
|
112
|
+
rmsg:mayDefer :evChargers.
|
|
113
|
+
(?AvailableWatts ?ShedWatts) math:sum ?ProtectedBudget.
|
|
114
|
+
?ProtectedBudget math:greaterThan ?CriticalWatts.
|
|
115
|
+
} => {
|
|
116
|
+
:clinicMicrogrid rmsg:protectedBudgetWatts ?ProtectedBudget.
|
|
117
|
+
:clinicMicrogrid rmsg:resilienceAction :protectClinic.
|
|
118
|
+
:clinicMicrogrid rmsg:keeps :oxygenConcentrator, :vaccineFridge.
|
|
119
|
+
:clinicMicrogrid rmsg:defers :evChargers.
|
|
120
|
+
}.
|
|
121
|
+
|
|
122
|
+
# Emit the example report only when the message stream, empty heartbeat, atomic
|
|
123
|
+
# payload inspection, and protection decision have all been derived.
|
|
124
|
+
{
|
|
125
|
+
:stormClinicLog rmsg:orderedMessages ?Messages.
|
|
126
|
+
?Messages list:length ?Count.
|
|
127
|
+
:m001 rmsg:boundaryExplicit true.
|
|
128
|
+
:m002 rmsg:boundaryExplicit true.
|
|
129
|
+
:m003 rmsg:boundaryExplicit true.
|
|
130
|
+
:m004 rmsg:boundaryExplicit true;
|
|
131
|
+
rmsg:emptyMessageAllowed true.
|
|
132
|
+
:clinicMicrogrid rmsg:criticalWatts ?CriticalWatts;
|
|
133
|
+
rmsg:availableWatts ?AvailableWatts;
|
|
134
|
+
rmsg:deferrableWatts ?ShedWatts;
|
|
135
|
+
rmsg:protectedBudgetWatts ?ProtectedBudget;
|
|
136
|
+
rmsg:resilienceAction :protectClinic.
|
|
137
|
+
("# rdf-message-microgrid\n\n## Source files\n\n- [N3 rules](../rdf-message-microgrid.n3)\n- [Input TriG](../input/rdf-message-microgrid.trig)\n\n## Answer\nStorm clinic microgrid accepted: %d RDF Message envelopes were replayed atomically. Critical care needs %d W, current battery plus solar gives %d W, and deferring the EV chargers frees %d W, so the protected budget is %d W. The reasoned action is to keep the oxygen concentrator and vaccine fridge online, while deferring EV charging.\n\n## Why this is an RDF Messages example\nThe input is a single runnable example split across an N3 rule file and a TriG sidecar. The default graph records stream order, offsets, and envelope metadata. Each non-empty named graph is treated as an atomic message payload, and the fourth message is an empty heartbeat. The rules inspect each payload with log:includes inside its own formula, then combine only the derived conclusions needed for the microgrid decision. This keeps the explanation tied to message boundaries instead of silently flattening the stream into one global graph.\n\nThis is intentionally not a parser-level VERSION \\\"1.2-messages\\\" / MESSAGE delimiter test. It is a reasoning example over an already-materialized sidecar representation of a message log." ?Count ?CriticalWatts ?AvailableWatts ?ShedWatts ?ProtectedBudget) string:format ?Block.
|
|
138
|
+
} => {
|
|
139
|
+
:rdfMessageMicrogridExample log:outputString ?Block.
|
|
140
|
+
:rdfMessageMicrogridExample :demonstrates :AtomicMessageContext, :EmptyHeartbeat, :ReplayableMessageLog, :ResilientDecisionSupport.
|
|
141
|
+
}.
|
package/examples/rdf-messages.n3
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
# ============
|
|
2
2
|
# RDF Messages
|
|
3
3
|
# ============
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
4
|
+
#
|
|
5
|
+
# Run as:
|
|
6
|
+
#
|
|
7
|
+
# eyeling -r examples/rdf-messages.n3 examples/input/rdf-messages.trig
|
|
8
|
+
#
|
|
9
|
+
# Motivation
|
|
10
|
+
# ----------
|
|
11
|
+
# The RDF Messages draft defines an RDF Message as an RDF dataset interpreted
|
|
12
|
+
# atomically, an RDF Message Stream as an ordered sequence of messages, and an
|
|
13
|
+
# RDF Message Log as a replayable record of such a stream. The draft also says
|
|
14
|
+
# that messages should not be combined by default and that blank node labels are
|
|
15
|
+
# scoped to the message in which they occur.
|
|
16
|
+
#
|
|
17
|
+
# This Eyeling example demonstrates those ideas in ordinary N3/TriG rather than
|
|
18
|
+
# in a parser-level RDF Message Log with VERSION "1.2-messages" and MESSAGE
|
|
19
|
+
# delimiters. The companion TriG file therefore has two layers:
|
|
20
|
+
#
|
|
21
|
+
# 1. application-local envelope facts in the default graph, such as message
|
|
22
|
+
# order, offset, expected result, and the textual blank-node label seen in
|
|
23
|
+
# the original stream; and
|
|
24
|
+
# 2. one named graph per non-empty message payload, treated as the atomic RDF
|
|
25
|
+
# dataset/message to be inspected.
|
|
26
|
+
#
|
|
27
|
+
# The rules below validate a replay archive with three message envelopes. The
|
|
28
|
+
# middle message is an empty heartbeat. The first and third messages both record
|
|
29
|
+
# the local blank-node label "_:b0"; because this sidecar is normal TriG, the
|
|
30
|
+
# actual TriG blank nodes are unique, while the envelope records show the
|
|
31
|
+
# message-scoped label reuse that a true RDF Message Log parser would preserve
|
|
32
|
+
# by resetting blank-node scope at each MESSAGE delimiter.
|
|
11
33
|
|
|
12
34
|
@prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-messages#>.
|
|
13
|
-
@prefix
|
|
35
|
+
@prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-messages/vocab#>.
|
|
14
36
|
@prefix prov: <http://www.w3.org/ns/prov#>.
|
|
15
37
|
@prefix sosa: <http://www.w3.org/ns/sosa/>.
|
|
16
38
|
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
|
|
@@ -19,51 +41,49 @@
|
|
|
19
41
|
@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
20
42
|
@prefix string: <http://www.w3.org/2000/10/swap/string#>.
|
|
21
43
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Empty RDF Message: a legal heartbeat/keep-alive event.
|
|
25
|
-
|
|
26
|
-
|
|
27
44
|
# A message with an offset inside a log has an explicit replay boundary.
|
|
28
|
-
|
|
45
|
+
# The envelope resource is application-local; the payload graph is the dataset
|
|
46
|
+
# treated as the RDF Message.
|
|
29
47
|
{
|
|
30
|
-
?Log a
|
|
31
|
-
|
|
32
|
-
?Message a
|
|
33
|
-
|
|
48
|
+
?Log a rmsg:MessageLog;
|
|
49
|
+
rmsg:message ?Message.
|
|
50
|
+
?Message a rmsg:MessageEnvelope;
|
|
51
|
+
rmsg:offset ?Offset.
|
|
34
52
|
} => {
|
|
35
|
-
?Message
|
|
36
|
-
?Log
|
|
53
|
+
?Message rmsg:boundaryExplicit true.
|
|
54
|
+
?Log rmsg:replayContains ?Message.
|
|
37
55
|
}.
|
|
38
56
|
|
|
39
|
-
# The payload is a dataset-like
|
|
40
|
-
#
|
|
57
|
+
# The payload is a dataset-like named graph. Inspect it with log:includes instead
|
|
58
|
+
# of merging all payload triples into the global graph.
|
|
41
59
|
{
|
|
42
|
-
?Message a
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
?Message a rmsg:MessageEnvelope;
|
|
61
|
+
rmsg:expectedResult ?Result;
|
|
62
|
+
rmsg:payloadGraph ?Payload.
|
|
45
63
|
?Payload log:nameOf ?PayloadContext.
|
|
46
64
|
?PayloadContext log:includes { ?Observation sosa:hasSimpleResult ?Result. }.
|
|
47
65
|
} => {
|
|
48
|
-
?Message
|
|
66
|
+
?Message rmsg:payloadResult ?Result.
|
|
49
67
|
}.
|
|
50
68
|
|
|
51
|
-
#
|
|
69
|
+
# Empty messages are legal RDF Messages. Here the empty second envelope acts as
|
|
70
|
+
# a heartbeat/keep-alive and still occupies a boundary in the replay archive.
|
|
52
71
|
{
|
|
53
|
-
?Message a
|
|
54
|
-
|
|
72
|
+
?Message a rmsg:MessageEnvelope;
|
|
73
|
+
rmsg:payloadKind :heartbeat.
|
|
55
74
|
} => {
|
|
56
|
-
?Message
|
|
75
|
+
?Message rmsg:emptyMessageAllowed true.
|
|
57
76
|
:HeartbeatEvidence :accepted ?Message.
|
|
58
77
|
}.
|
|
59
78
|
|
|
60
|
-
# The same blank-node label may recur in
|
|
61
|
-
#
|
|
79
|
+
# The same local blank-node label may recur in different messages. In this
|
|
80
|
+
# plain-TriG sidecar the real blank nodes are unique, and the envelope metadata
|
|
81
|
+
# records the per-message label visible in the source stream.
|
|
62
82
|
{
|
|
63
|
-
?First a
|
|
64
|
-
|
|
65
|
-
?Second a
|
|
66
|
-
|
|
83
|
+
?First a rmsg:MessageEnvelope;
|
|
84
|
+
rmsg:localBlankLabel ?Label.
|
|
85
|
+
?Second a rmsg:MessageEnvelope;
|
|
86
|
+
rmsg:localBlankLabel ?Label.
|
|
67
87
|
?First log:notEqualTo ?Second.
|
|
68
88
|
} => {
|
|
69
89
|
:BlankNodeScope :reusedLabel ?Label.
|
|
@@ -71,35 +91,35 @@
|
|
|
71
91
|
}.
|
|
72
92
|
|
|
73
93
|
# Different observations from the same sensor are not a global contradiction:
|
|
74
|
-
#
|
|
94
|
+
# by default, message payloads remain separate communication contexts.
|
|
75
95
|
{
|
|
76
|
-
?First a
|
|
77
|
-
|
|
78
|
-
?Second a
|
|
79
|
-
|
|
96
|
+
?First a rmsg:MessageEnvelope;
|
|
97
|
+
rmsg:payloadResult ?FirstResult.
|
|
98
|
+
?Second a rmsg:MessageEnvelope;
|
|
99
|
+
rmsg:payloadResult ?SecondResult.
|
|
80
100
|
?FirstResult math:notEqualTo ?SecondResult.
|
|
81
101
|
?First log:notEqualTo ?Second.
|
|
82
102
|
} => {
|
|
83
103
|
:MessageContext :differentObservationsStayContextual true.
|
|
84
104
|
}.
|
|
85
105
|
|
|
86
|
-
# The Eyeling verdict is emitted only when all message-specific validations have
|
|
87
|
-
# derived from the log, message
|
|
106
|
+
# The Eyeling verdict is emitted only when all message-specific validations have
|
|
107
|
+
# been derived from the log metadata, ordered message list, payload graphs, and
|
|
108
|
+
# built-ins.
|
|
88
109
|
{
|
|
89
|
-
:temperatureLog
|
|
110
|
+
:temperatureLog rmsg:orderedMessages ?Messages.
|
|
90
111
|
?Messages list:length ?Count.
|
|
91
|
-
:m001
|
|
92
|
-
|
|
93
|
-
:m002
|
|
94
|
-
|
|
95
|
-
:m003
|
|
96
|
-
|
|
112
|
+
:m001 rmsg:boundaryExplicit true;
|
|
113
|
+
rmsg:payloadResult ?FirstResult.
|
|
114
|
+
:m002 rmsg:boundaryExplicit true;
|
|
115
|
+
rmsg:emptyMessageAllowed true.
|
|
116
|
+
:m003 rmsg:boundaryExplicit true;
|
|
117
|
+
rmsg:payloadResult ?SecondResult.
|
|
97
118
|
:BlankNodeScope :reusedLabel ?Label;
|
|
98
119
|
:isPerMessage true.
|
|
99
120
|
:MessageContext :differentObservationsStayContextual true.
|
|
100
|
-
("# rdf-messages\n\n## Source files\n\n- [N3 rules](../rdf-messages.n3)\n- [Input TriG](../input/rdf-messages.trig)\n\n## Answer\nRDF Message
|
|
121
|
+
("# rdf-messages\n\n## Source files\n\n- [N3 rules](../rdf-messages.n3)\n- [Input TriG](../input/rdf-messages.trig)\n\n## Answer\nRDF Message replay archive accepted: %d explicit message boundaries are preserved. Message :m002 is an empty heartbeat, and the local blank-node label %s is safely reused in separate message envelopes.\n\n## Explanation\nThe input is a single runnable example split across an N3 rule file and a TriG sidecar. The TriG file uses application-local envelope facts for stream order and replay metadata, while each non-empty named payload graph is treated as an atomic RDF Message dataset. Payloads are inspected with log:includes inside their own formulas, so the observation data stays inside the message boundary instead of being treated as one global graph. The two temperature results, %s and %s, are different observations from the same stream but are contextualized by their message boundaries.\n\nThis is intentionally not a parser-level VERSION \\\"1.2-messages\\\" / MESSAGE delimiter test. It is a reasoning example over an already-materialized sidecar representation of a message log." ?Count ?Label ?FirstResult ?SecondResult) string:format ?Block.
|
|
101
122
|
} => {
|
|
102
123
|
:rdfMessagesExample log:outputString ?Block.
|
|
103
124
|
:rdfMessagesExample :demonstrates :ExplicitBoundaries, :AtomicMessageContext, :EmptyHeartbeat, :MessageScopedBlankNodes, :ReplayableMessageLog.
|
|
104
125
|
}.
|
|
105
|
-
|
package/eyeling.js
CHANGED
|
@@ -844,6 +844,62 @@ function compileSwapRegex(pattern, extraFlags) {
|
|
|
844
844
|
}
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
+
function expandSwapRegexReplacement(template, captures) {
|
|
848
|
+
// SWAP/N3 examples use $1-style capture references, but tests also rely on
|
|
849
|
+
// Perl-ish escaping in replacement strings: \\$ means a literal dollar sign
|
|
850
|
+
// and \\ means a literal backslash. JavaScript's replacement strings do not
|
|
851
|
+
// interpret those escapes, so expand the replacement explicitly in a callback.
|
|
852
|
+
let out = '';
|
|
853
|
+
const text = String(template);
|
|
854
|
+
|
|
855
|
+
for (let i = 0; i < text.length; i++) {
|
|
856
|
+
const ch = text[i];
|
|
857
|
+
|
|
858
|
+
if (ch === '\\') {
|
|
859
|
+
if (i + 1 < text.length && (text[i + 1] === '$' || text[i + 1] === '\\')) {
|
|
860
|
+
out += text[++i];
|
|
861
|
+
} else {
|
|
862
|
+
out += ch;
|
|
863
|
+
}
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (ch === '$') {
|
|
868
|
+
if (i + 1 < text.length && text[i + 1] === '$') {
|
|
869
|
+
out += '$';
|
|
870
|
+
i++;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
let j = i + 1;
|
|
875
|
+
while (j < text.length && /[0-9]/.test(text[j])) j++;
|
|
876
|
+
if (j > i + 1) {
|
|
877
|
+
const digits = text.slice(i + 1, j);
|
|
878
|
+
let chosen = null;
|
|
879
|
+
for (let k = digits.length; k > 0; k--) {
|
|
880
|
+
const n = Number(digits.slice(0, k));
|
|
881
|
+
if (n > 0 && n < captures.length) {
|
|
882
|
+
chosen = { n, len: k };
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (chosen) {
|
|
887
|
+
out += captures[chosen.n] == null ? '' : String(captures[chosen.n]);
|
|
888
|
+
out += digits.slice(chosen.len);
|
|
889
|
+
} else {
|
|
890
|
+
out += '$' + digits;
|
|
891
|
+
}
|
|
892
|
+
i = j - 1;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
out += ch;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return out;
|
|
901
|
+
}
|
|
902
|
+
|
|
847
903
|
// -----------------------------------------------------------------------------
|
|
848
904
|
// Strict numeric literal parsing for math: builtins
|
|
849
905
|
// -----------------------------------------------------------------------------
|
|
@@ -1699,7 +1755,7 @@ function __pushBuiltinDeltaLimited(out, delta, maxResults) {
|
|
|
1699
1755
|
|
|
1700
1756
|
function __collectListLikeTermsFromTerm(t, out, seen) {
|
|
1701
1757
|
if (t instanceof ListTerm || t instanceof OpenListTerm) {
|
|
1702
|
-
const k = termFastKey
|
|
1758
|
+
const k = termFastKey(t);
|
|
1703
1759
|
if (k === null || !seen.has(k)) {
|
|
1704
1760
|
if (k !== null) seen.add(k);
|
|
1705
1761
|
out.push(t);
|
|
@@ -4222,7 +4278,11 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
4222
4278
|
const re = compileSwapRegex(searchStr, 'g');
|
|
4223
4279
|
if (!re) return [];
|
|
4224
4280
|
|
|
4225
|
-
const outStr = dataStr.replace(re,
|
|
4281
|
+
const outStr = dataStr.replace(re, (...args) => {
|
|
4282
|
+
const captureEnd = args.length - (typeof args.at(-1) === 'object' ? 3 : 2);
|
|
4283
|
+
const captures = args.slice(0, Math.max(1, captureEnd));
|
|
4284
|
+
return expandSwapRegexReplacement(replStr, captures);
|
|
4285
|
+
});
|
|
4226
4286
|
const lit = makeStringLiteral(outStr);
|
|
4227
4287
|
|
|
4228
4288
|
if (g.o instanceof Var) {
|
|
@@ -11498,6 +11558,20 @@ class Parser {
|
|
|
11498
11558
|
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
11499
11559
|
this.next();
|
|
11500
11560
|
pred = internIri(RDF_NS + 'type');
|
|
11561
|
+
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
|
|
11562
|
+
// N3 syntactic sugar is also valid in predicate-object lists,
|
|
11563
|
+
// including blank node property lists: [ has :p :o ] means _:b :p :o.
|
|
11564
|
+
this.next();
|
|
11565
|
+
pred = this.parseTerm();
|
|
11566
|
+
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
|
|
11567
|
+
// N3 syntactic sugar: [ is :p of :s ] means :s :p _:b.
|
|
11568
|
+
this.next();
|
|
11569
|
+
pred = this.parseTerm();
|
|
11570
|
+
if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
|
|
11571
|
+
this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
|
|
11572
|
+
}
|
|
11573
|
+
this.next();
|
|
11574
|
+
invert = true;
|
|
11501
11575
|
} else if (this.peek().typ === 'OpPredInvert') {
|
|
11502
11576
|
this.next();
|
|
11503
11577
|
pred = this.parseTerm();
|