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.
@@ -1,14 +1,35 @@
1
1
  # ================
2
2
  # RDF Message Flow
3
3
  # ================
4
- # A companion to rdf_messages.n3. This example focuses on a live stream where
5
- # RDF Messages continuously flow through a small processing pipeline. The next
6
- # message is released only after the current message reaches the sink, so the
7
- # stream behaves as an ordered, replayable flow rather than a single merged RDF
8
- # graph.
4
+ #
5
+ # Run as:
6
+ #
7
+ # eyeling -r examples/rdf-message-flow.n3 examples/input/rdf-message-flow.trig
8
+ #
9
+ # Motivation
10
+ # ----------
11
+ # The RDF Messages draft describes an RDF Message as an RDF dataset interpreted
12
+ # atomically, and an RDF Message Stream as an ordered sequence of such messages.
13
+ # It also defines RDF Message Logs with explicit MESSAGE delimiters. This Eyeling
14
+ # example keeps the same message-level discipline while staying in ordinary
15
+ # N3/TriG that Eyeling can reason over today.
16
+ #
17
+ # The companion TriG file therefore separates two layers:
18
+ #
19
+ # 1. example-local envelope facts in the default graph, such as order,
20
+ # processing stage, and payload graph; and
21
+ # 2. one named graph per non-empty message payload, treated here as the RDF
22
+ # dataset/message that must be inspected atomically.
23
+ #
24
+ # The rules below deliberately do not merge all payload graphs into one global
25
+ # graph. Instead, each observation is checked with log:includes inside the named
26
+ # payload formula. The empty heartbeat has no payload graph but still advances
27
+ # through the same pipeline. The next message is released only after the current
28
+ # one reaches the sink, making the example a small back-pressure / flow-control
29
+ # story rather than a batch merge of all input triples.
9
30
 
10
31
  @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-message-flow#>.
11
- @prefix msg: <https://example.org/msg#>.
32
+ @prefix flow: <https://eyereasoner.github.io/eyeling/examples/rdf-message-flow/vocab#>.
12
33
  @prefix prov: <http://www.w3.org/ns/prov#>.
13
34
  @prefix sosa: <http://www.w3.org/ns/sosa/>.
14
35
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@@ -17,126 +38,120 @@
17
38
  @prefix log: <http://www.w3.org/2000/10/swap/log#>.
18
39
  @prefix string: <http://www.w3.org/2000/10/swap/string#>.
19
40
 
20
-
21
- # The stream starts with just the first message at ingress. Later messages are
22
- # not pre-loaded; each one is released by the previous message after sink.
23
-
24
-
25
-
26
- # Empty heartbeat: it flows through the same pipeline and keeps the stream live.
27
-
28
-
29
-
30
- # Stage 1: once a message has entered the stream, validate its envelope.
31
-
32
- { ?Message :atStage :ingest. } => {
33
- ?Message :atStage :validate.
41
+ # Stage 1: once an envelope has entered the stream, validate it.
42
+ { ?Envelope :atStage :ingest. } => {
43
+ ?Envelope :atStage :validate.
34
44
  }.
35
45
 
36
- # Stage 2: validation preserves the message boundary before interpretation.
46
+ # Stage 2: validation records that the envelope keeps an explicit message
47
+ # boundary before the payload is interpreted.
37
48
  {
38
- ?Message :atStage :validate;
39
- a msg:RDFMessage;
40
- msg:offset ?Offset.
49
+ ?Envelope :atStage :validate;
50
+ a flow:MessageEnvelope;
51
+ flow:offset ?Offset.
41
52
  } => {
42
- ?Message msg:boundaryExplicit true.
43
- ?Message :atStage :interpret.
53
+ ?Envelope flow:boundaryExplicit true.
54
+ ?Envelope :atStage :interpret.
44
55
  }.
45
56
 
46
- # Stage 3a: observation payloads are inspected inside their message formula.
57
+ # Stage 3a: observation payloads are inspected inside their own payload graph.
58
+ # This models the RDF Messages idea that messages are separate atomic datasets.
47
59
  {
48
- ?Message :atStage :interpret;
49
- msg:payloadKind :observation;
50
- msg:expectedResult ?Result;
51
- msg:payload ?Payload.
60
+ ?Envelope :atStage :interpret;
61
+ flow:payloadKind :observation;
62
+ flow:expectedResult ?Result;
63
+ flow:payloadGraph ?Payload.
52
64
  ?Payload log:nameOf ?PayloadContext.
53
65
  ?PayloadContext log:includes { ?Observation sosa:hasSimpleResult ?Result. }.
54
66
  } => {
55
- ?Message msg:payloadResult ?Result.
56
- ?Message :atStage :route.
67
+ ?Envelope flow:payloadResult ?Result.
68
+ ?Envelope :atStage :route.
57
69
  }.
58
70
 
59
- # Stage 3b: empty heartbeats have no payload, but still move through the flow.
71
+ # Stage 3b: empty heartbeats contain no quads, but RDF Messages explicitly allow
72
+ # empty messages, so the envelope still moves through the flow.
60
73
  {
61
- ?Message :atStage :interpret;
62
- msg:payloadKind :heartbeat.
74
+ ?Envelope :atStage :interpret;
75
+ flow:payloadKind :heartbeat.
63
76
  } => {
64
- ?Message msg:emptyMessageAllowed true.
65
- ?Message :atStage :route.
77
+ ?Envelope flow:emptyMessageAllowed true.
78
+ ?Envelope :atStage :route.
66
79
  }.
67
80
 
68
81
  # Stage 4a: hot observations are routed to the alert sink.
69
82
  {
70
- ?Message :atStage :route;
71
- msg:payloadResult ?Result.
83
+ ?Envelope :atStage :route;
84
+ flow:payloadResult ?Result.
72
85
  :temperatureFlow :highThreshold ?Threshold.
73
86
  ?Result math:greaterThan ?Threshold.
74
87
  } => {
75
- ?Message :route :alertSink.
76
- ?Message :atStage :sink.
77
- :alertSink :received ?Message.
88
+ ?Envelope :route :alertSink.
89
+ ?Envelope :atStage :sink.
90
+ :alertSink :received ?Envelope.
78
91
  }.
79
92
 
80
93
  # Stage 4b: normal observations are routed to the archive sink.
81
94
  {
82
- ?Message :atStage :route;
83
- msg:payloadResult ?Result.
95
+ ?Envelope :atStage :route;
96
+ flow:payloadResult ?Result.
84
97
  :temperatureFlow :highThreshold ?Threshold.
85
98
  ?Result math:notGreaterThan ?Threshold.
86
99
  } => {
87
- ?Message :route :archiveSink.
88
- ?Message :atStage :sink.
89
- :archiveSink :received ?Message.
100
+ ?Envelope :route :archiveSink.
101
+ ?Envelope :atStage :sink.
102
+ :archiveSink :received ?Envelope.
90
103
  }.
91
104
 
92
105
  # Stage 4c: heartbeats are routed separately from observations.
93
106
  {
94
- ?Message :atStage :route;
95
- msg:emptyMessageAllowed true.
107
+ ?Envelope :atStage :route;
108
+ flow:emptyMessageAllowed true.
96
109
  } => {
97
- ?Message :route :heartbeatSink.
98
- ?Message :atStage :sink.
99
- :heartbeatSink :received ?Message.
110
+ ?Envelope :route :heartbeatSink.
111
+ ?Envelope :atStage :sink.
112
+ :heartbeatSink :received ?Envelope.
100
113
  }.
101
114
 
102
- # Continuous-flow rule: reaching the sink releases the next message into ingress.
115
+ # Continuous-flow rule: reaching the sink releases the next envelope into ingress.
116
+ # This mirrors a consumer-visible ordered stream: later messages are not processed
117
+ # until the earlier message has completed the pipeline.
103
118
  {
104
- ?Message :atStage :sink;
105
- msg:nextMessage ?Next.
106
- ?Next a msg:RDFMessage.
119
+ ?Envelope :atStage :sink;
120
+ flow:nextEnvelope ?Next.
121
+ ?Next a flow:MessageEnvelope.
107
122
  } => {
108
- ?Message :releases ?Next.
123
+ ?Envelope :releases ?Next.
109
124
  ?Next :atStage :ingest.
110
125
  }.
111
126
 
112
- # The Eyeling verdict is emitted only after all five messages have flowed through
113
- # ingest, validation, interpretation, routing, and sink.
127
+ # The Eyeling verdict is emitted only after all five envelopes have flowed
128
+ # through ingest, validation, interpretation, routing, and sink while preserving
129
+ # their message boundaries.
114
130
  {
115
- :temperatureFlow msg:orderedMessages ?Messages;
131
+ :temperatureFlow flow:orderedEnvelopes ?Envelopes;
116
132
  :highThreshold ?Threshold.
117
- ?Messages list:length ?Count.
133
+ ?Envelopes list:length ?Count.
118
134
  :m001 :atStage :sink;
119
- msg:payloadResult ?FirstResult;
135
+ flow:payloadResult ?FirstResult;
120
136
  :route :archiveSink;
121
137
  :releases :m002.
122
138
  :m002 :atStage :sink;
123
- msg:payloadResult ?SecondResult;
139
+ flow:payloadResult ?SecondResult;
124
140
  :route :archiveSink;
125
141
  :releases :m003.
126
142
  :m003 :atStage :sink;
127
- msg:emptyMessageAllowed true;
143
+ flow:emptyMessageAllowed true;
128
144
  :route :heartbeatSink;
129
145
  :releases :m004.
130
146
  :m004 :atStage :sink;
131
- msg:payloadResult ?FourthResult;
147
+ flow:payloadResult ?FourthResult;
132
148
  :route :alertSink;
133
149
  :releases :m005.
134
150
  :m005 :atStage :sink;
135
- msg:payloadResult ?FifthResult;
151
+ flow:payloadResult ?FifthResult;
136
152
  :route :alertSink.
137
- ("# rdf-message-flow\n\n## Source files\n\n- [N3 rules](../rdf-message-flow.n3)\n- [Input TriG](../input/rdf-message-flow.trig)\n\n## Answer\nContinuous RDF Message flow accepted: %d ordered messages moved through the ingest → validate → interpret → route → sink pipeline. The threshold was %d, so results %s and %s were archived, the heartbeat kept the stream alive, and results %s and %s were emitted as alerts.\n\n## Explanation\nThe N3 source starts only :m001 at ingress. Each message must reach :sink before the continuous-flow rule releases its msg:nextMessage. Observation payloads are inspected with log:includes inside each message formula, while the empty heartbeat uses the same envelope and routing stages without a payload. This models messages flowing through a live stream while preserving message boundaries." ?Count ?Threshold ?FirstResult ?SecondResult ?FourthResult ?FifthResult) string:format ?Block.
153
+ ("# rdf-message-flow\n\n## Source files\n\n- [N3 rules](../rdf-message-flow.n3)\n- [Input TriG](../input/rdf-message-flow.trig)\n\n## Answer\nContinuous RDF Message flow accepted: %d ordered message envelopes moved through the ingest → validate → interpret → route → sink pipeline. The threshold was %d, so results %s and %s were archived, the heartbeat kept the stream alive, and results %s and %s were emitted as alerts.\n\n## Explanation\nThe input is a single runnable example split across an N3 rule file and a TriG sidecar. The TriG file uses example-local envelope facts for stream order and processing state, while each named payload graph is treated as an atomic RDF Message dataset. Only :m001 starts at ingress; each envelope must reach :sink before the continuous-flow rule releases its flow:nextEnvelope. Observation payloads are inspected with log:includes inside their own payload formula, and the empty heartbeat advances without a payload graph. This keeps message boundaries visible to the reasoner instead of merging all payload triples into one global graph." ?Count ?Threshold ?FirstResult ?SecondResult ?FourthResult ?FifthResult) string:format ?Block.
138
154
  } => {
139
155
  :rdfMessageFlowExample log:outputString ?Block.
140
156
  :rdfMessageFlowExample :demonstrates :ContinuousFlow, :BackPressureRelease, :AtomicMessageContext, :HeartbeatInFlow, :ThresholdRouting.
141
157
  }.
142
-
@@ -0,0 +1,141 @@
1
+ # =====================
2
+ # RDF Message Microgrid
3
+ # =====================
4
+ #
5
+ # Run as:
6
+ #
7
+ # eyeling -r examples/rdf-message-microgrid.n3 examples/input/rdf-message-microgrid.trig
8
+ #
9
+ # Motivation
10
+ # ----------
11
+ # A clinic has lost grid power during a storm. Several small systems continue to
12
+ # send RDF data: one message describes life-safety loads, another describes the
13
+ # current battery and solar situation, another describes flexible demand that may
14
+ # be deferred, and a fourth message is an empty heartbeat.
15
+ #
16
+ # The RDF Messages draft treats each message as an RDF dataset intended to be
17
+ # interpreted atomically. It also treats a message stream as an ordered sequence
18
+ # and allows empty messages. This example models those ideas in ordinary TriG/N3:
19
+ # the companion TriG file contains application-local envelope facts in the
20
+ # default graph, and each non-empty payload is placed in its own named graph.
21
+ #
22
+ # The point is not only technical. Atomic messages let a small reasoner make a
23
+ # careful decision under pressure: protect the oxygen concentrator and vaccine
24
+ # fridge, defer the EV chargers, and explain the action from the replayed message
25
+ # boundaries instead of silently merging every payload into one timeless graph.
26
+ #
27
+ # This is intentionally not a parser-level VERSION "1.2-messages" / MESSAGE
28
+ # delimiter test. It is a reasoning example over an already-materialized sidecar
29
+ # representation of a message log.
30
+
31
+ @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-message-microgrid#>.
32
+ @prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-message-microgrid/vocab#>.
33
+ @prefix prov: <http://www.w3.org/ns/prov#>.
34
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
35
+ @prefix math: <http://www.w3.org/2000/10/swap/math#>.
36
+ @prefix list: <http://www.w3.org/2000/10/swap/list#>.
37
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
38
+ @prefix string: <http://www.w3.org/2000/10/swap/string#>.
39
+
40
+ # Every envelope with an offset is an explicit replay boundary. The envelope is
41
+ # application-local metadata; the payload graph is the dataset/message being
42
+ # interpreted atomically.
43
+ {
44
+ ?Log a rmsg:MessageLog;
45
+ rmsg:message ?Message.
46
+ ?Message a rmsg:MessageEnvelope;
47
+ rmsg:offset ?Offset.
48
+ } => {
49
+ ?Message rmsg:boundaryExplicit true.
50
+ ?Log rmsg:replayContains ?Message.
51
+ }.
52
+
53
+ # Empty RDF Messages are valid. Here the fourth envelope is a heartbeat that
54
+ # confirms the stream is still alive without adding any payload triples.
55
+ {
56
+ ?Message a rmsg:MessageEnvelope;
57
+ rmsg:payloadKind :heartbeat.
58
+ } => {
59
+ ?Message rmsg:emptyMessageAllowed true.
60
+ }.
61
+
62
+ # Inspect the life-safety message inside its own payload graph.
63
+ {
64
+ ?Message rmsg:payloadKind :lifeSafetyLoads;
65
+ rmsg:payloadGraph ?Payload.
66
+ ?Payload log:nameOf ?PayloadContext.
67
+ ?PayloadContext log:includes {
68
+ :oxygenConcentrator :requiresWatts ?Oxygen.
69
+ :vaccineFridge :requiresWatts ?Fridge.
70
+ }.
71
+ (?Oxygen ?Fridge) math:sum ?CriticalWatts.
72
+ } => {
73
+ :clinicMicrogrid rmsg:criticalWatts ?CriticalWatts.
74
+ :clinicMicrogrid rmsg:mustKeep :oxygenConcentrator, :vaccineFridge.
75
+ }.
76
+
77
+ # Inspect the power-status message atomically.
78
+ {
79
+ ?Message rmsg:payloadKind :powerStatus;
80
+ rmsg:payloadGraph ?Payload.
81
+ ?Payload log:nameOf ?PayloadContext.
82
+ ?PayloadContext log:includes {
83
+ :batteryBank :availableWatts ?BatteryWatts.
84
+ :solarForecast :expectedWatts ?SolarWatts.
85
+ }.
86
+ (?BatteryWatts ?SolarWatts) math:sum ?AvailableWatts.
87
+ } => {
88
+ :clinicMicrogrid rmsg:availableWatts ?AvailableWatts.
89
+ }.
90
+
91
+ # Inspect the flexible-demand message atomically. The EV chargers are useful, but
92
+ # they are safe to defer so life-safety loads keep running.
93
+ {
94
+ ?Message rmsg:payloadKind :flexibleDemand;
95
+ rmsg:payloadGraph ?Payload.
96
+ ?Payload log:nameOf ?PayloadContext.
97
+ ?PayloadContext log:includes {
98
+ :evChargers :shedWatts ?ShedWatts.
99
+ }.
100
+ } => {
101
+ :clinicMicrogrid rmsg:deferrableWatts ?ShedWatts.
102
+ :clinicMicrogrid rmsg:mayDefer :evChargers.
103
+ }.
104
+
105
+ # The decision combines conclusions from the separate messages, while preserving
106
+ # the evidence that each conclusion came through an explicit message boundary.
107
+ {
108
+ :clinicMicrogrid rmsg:criticalWatts ?CriticalWatts;
109
+ rmsg:availableWatts ?AvailableWatts;
110
+ rmsg:deferrableWatts ?ShedWatts;
111
+ rmsg:mustKeep :oxygenConcentrator, :vaccineFridge;
112
+ rmsg:mayDefer :evChargers.
113
+ (?AvailableWatts ?ShedWatts) math:sum ?ProtectedBudget.
114
+ ?ProtectedBudget math:greaterThan ?CriticalWatts.
115
+ } => {
116
+ :clinicMicrogrid rmsg:protectedBudgetWatts ?ProtectedBudget.
117
+ :clinicMicrogrid rmsg:resilienceAction :protectClinic.
118
+ :clinicMicrogrid rmsg:keeps :oxygenConcentrator, :vaccineFridge.
119
+ :clinicMicrogrid rmsg:defers :evChargers.
120
+ }.
121
+
122
+ # Emit the example report only when the message stream, empty heartbeat, atomic
123
+ # payload inspection, and protection decision have all been derived.
124
+ {
125
+ :stormClinicLog rmsg:orderedMessages ?Messages.
126
+ ?Messages list:length ?Count.
127
+ :m001 rmsg:boundaryExplicit true.
128
+ :m002 rmsg:boundaryExplicit true.
129
+ :m003 rmsg:boundaryExplicit true.
130
+ :m004 rmsg:boundaryExplicit true;
131
+ rmsg:emptyMessageAllowed true.
132
+ :clinicMicrogrid rmsg:criticalWatts ?CriticalWatts;
133
+ rmsg:availableWatts ?AvailableWatts;
134
+ rmsg:deferrableWatts ?ShedWatts;
135
+ rmsg:protectedBudgetWatts ?ProtectedBudget;
136
+ rmsg:resilienceAction :protectClinic.
137
+ ("# rdf-message-microgrid\n\n## Source files\n\n- [N3 rules](../rdf-message-microgrid.n3)\n- [Input TriG](../input/rdf-message-microgrid.trig)\n\n## Answer\nStorm clinic microgrid accepted: %d RDF Message envelopes were replayed atomically. Critical care needs %d W, current battery plus solar gives %d W, and deferring the EV chargers frees %d W, so the protected budget is %d W. The reasoned action is to keep the oxygen concentrator and vaccine fridge online, while deferring EV charging.\n\n## Why this is an RDF Messages example\nThe input is a single runnable example split across an N3 rule file and a TriG sidecar. The default graph records stream order, offsets, and envelope metadata. Each non-empty named graph is treated as an atomic message payload, and the fourth message is an empty heartbeat. The rules inspect each payload with log:includes inside its own formula, then combine only the derived conclusions needed for the microgrid decision. This keeps the explanation tied to message boundaries instead of silently flattening the stream into one global graph.\n\nThis is intentionally not a parser-level VERSION \\\"1.2-messages\\\" / MESSAGE delimiter test. It is a reasoning example over an already-materialized sidecar representation of a message log." ?Count ?CriticalWatts ?AvailableWatts ?ShedWatts ?ProtectedBudget) string:format ?Block.
138
+ } => {
139
+ :rdfMessageMicrogridExample log:outputString ?Block.
140
+ :rdfMessageMicrogridExample :demonstrates :AtomicMessageContext, :EmptyHeartbeat, :ReplayableMessageLog, :ResilientDecisionSupport.
141
+ }.
@@ -1,16 +1,38 @@
1
1
  # ============
2
2
  # RDF Messages
3
3
  # ============
4
- # This Eyeling example models the main idea from
5
- # https://pietercolpaert.be/papers/eswc2026-rdf-messages/:
6
- # a message stream/log is not just one freely mergeable RDF graph. It is an
7
- # ordered sequence of RDF Datasets that are interpreted atomically, one message
8
- # at a time. The middle message is deliberately empty to model a heartbeat, and
9
- # the local blank-node label "_:b0" is deliberately reused by two messages to
10
- # show message-scoped blank nodes.
4
+ #
5
+ # Run as:
6
+ #
7
+ # eyeling -r examples/rdf-messages.n3 examples/input/rdf-messages.trig
8
+ #
9
+ # Motivation
10
+ # ----------
11
+ # The RDF Messages draft defines an RDF Message as an RDF dataset interpreted
12
+ # atomically, an RDF Message Stream as an ordered sequence of messages, and an
13
+ # RDF Message Log as a replayable record of such a stream. The draft also says
14
+ # that messages should not be combined by default and that blank node labels are
15
+ # scoped to the message in which they occur.
16
+ #
17
+ # This Eyeling example demonstrates those ideas in ordinary N3/TriG rather than
18
+ # in a parser-level RDF Message Log with VERSION "1.2-messages" and MESSAGE
19
+ # delimiters. The companion TriG file therefore has two layers:
20
+ #
21
+ # 1. application-local envelope facts in the default graph, such as message
22
+ # order, offset, expected result, and the textual blank-node label seen in
23
+ # the original stream; and
24
+ # 2. one named graph per non-empty message payload, treated as the atomic RDF
25
+ # dataset/message to be inspected.
26
+ #
27
+ # The rules below validate a replay archive with three message envelopes. The
28
+ # middle message is an empty heartbeat. The first and third messages both record
29
+ # the local blank-node label "_:b0"; because this sidecar is normal TriG, the
30
+ # actual TriG blank nodes are unique, while the envelope records show the
31
+ # message-scoped label reuse that a true RDF Message Log parser would preserve
32
+ # by resetting blank-node scope at each MESSAGE delimiter.
11
33
 
12
34
  @prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-messages#>.
13
- @prefix msg: <https://w3c-cg.github.io/rsp/spec/messages#>.
35
+ @prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-messages/vocab#>.
14
36
  @prefix prov: <http://www.w3.org/ns/prov#>.
15
37
  @prefix sosa: <http://www.w3.org/ns/sosa/>.
16
38
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@@ -19,51 +41,49 @@
19
41
  @prefix log: <http://www.w3.org/2000/10/swap/log#>.
20
42
  @prefix string: <http://www.w3.org/2000/10/swap/string#>.
21
43
 
22
-
23
-
24
- # Empty RDF Message: a legal heartbeat/keep-alive event.
25
-
26
-
27
44
  # A message with an offset inside a log has an explicit replay boundary.
28
-
45
+ # The envelope resource is application-local; the payload graph is the dataset
46
+ # treated as the RDF Message.
29
47
  {
30
- ?Log a msg:MessageLog;
31
- msg:message ?Message.
32
- ?Message a msg:RDFMessage;
33
- msg:offset ?Offset.
48
+ ?Log a rmsg:MessageLog;
49
+ rmsg:message ?Message.
50
+ ?Message a rmsg:MessageEnvelope;
51
+ rmsg:offset ?Offset.
34
52
  } => {
35
- ?Message msg:boundaryExplicit true.
36
- ?Log msg:replayContains ?Message.
53
+ ?Message rmsg:boundaryExplicit true.
54
+ ?Log rmsg:replayContains ?Message.
37
55
  }.
38
56
 
39
- # The payload is a dataset-like formula. We can inspect it without merging it
40
- # into the global graph, preserving the message as the unit of interpretation.
57
+ # The payload is a dataset-like named graph. Inspect it with log:includes instead
58
+ # of merging all payload triples into the global graph.
41
59
  {
42
- ?Message a msg:RDFMessage;
43
- msg:expectedResult ?Result;
44
- msg:payload ?Payload.
60
+ ?Message a rmsg:MessageEnvelope;
61
+ rmsg:expectedResult ?Result;
62
+ rmsg:payloadGraph ?Payload.
45
63
  ?Payload log:nameOf ?PayloadContext.
46
64
  ?PayloadContext log:includes { ?Observation sosa:hasSimpleResult ?Result. }.
47
65
  } => {
48
- ?Message msg:payloadResult ?Result.
66
+ ?Message rmsg:payloadResult ?Result.
49
67
  }.
50
68
 
51
- # Heartbeat/keep-alive messages can be empty and still occupy a boundary.
69
+ # Empty messages are legal RDF Messages. Here the empty second envelope acts as
70
+ # a heartbeat/keep-alive and still occupies a boundary in the replay archive.
52
71
  {
53
- ?Message a msg:RDFMessage;
54
- msg:payloadKind :heartbeat.
72
+ ?Message a rmsg:MessageEnvelope;
73
+ rmsg:payloadKind :heartbeat.
55
74
  } => {
56
- ?Message msg:emptyMessageAllowed true.
75
+ ?Message rmsg:emptyMessageAllowed true.
57
76
  :HeartbeatEvidence :accepted ?Message.
58
77
  }.
59
78
 
60
- # The same blank-node label may recur in a message log because blank nodes are
61
- # scoped to their RDF Message, not to the whole replay archive.
79
+ # The same local blank-node label may recur in different messages. In this
80
+ # plain-TriG sidecar the real blank nodes are unique, and the envelope metadata
81
+ # records the per-message label visible in the source stream.
62
82
  {
63
- ?First a msg:RDFMessage;
64
- msg:localBlankLabel ?Label.
65
- ?Second a msg:RDFMessage;
66
- msg:localBlankLabel ?Label.
83
+ ?First a rmsg:MessageEnvelope;
84
+ rmsg:localBlankLabel ?Label.
85
+ ?Second a rmsg:MessageEnvelope;
86
+ rmsg:localBlankLabel ?Label.
67
87
  ?First log:notEqualTo ?Second.
68
88
  } => {
69
89
  :BlankNodeScope :reusedLabel ?Label.
@@ -71,35 +91,35 @@
71
91
  }.
72
92
 
73
93
  # Different observations from the same sensor are not a global contradiction:
74
- # each message is interpreted in its own communication context.
94
+ # by default, message payloads remain separate communication contexts.
75
95
  {
76
- ?First a msg:RDFMessage;
77
- msg:payloadResult ?FirstResult.
78
- ?Second a msg:RDFMessage;
79
- msg:payloadResult ?SecondResult.
96
+ ?First a rmsg:MessageEnvelope;
97
+ rmsg:payloadResult ?FirstResult.
98
+ ?Second a rmsg:MessageEnvelope;
99
+ rmsg:payloadResult ?SecondResult.
80
100
  ?FirstResult math:notEqualTo ?SecondResult.
81
101
  ?First log:notEqualTo ?Second.
82
102
  } => {
83
103
  :MessageContext :differentObservationsStayContextual true.
84
104
  }.
85
105
 
86
- # The Eyeling verdict is emitted only when all message-specific validations have been
87
- # derived from the log, message metadata, payload formulas, and built-ins.
106
+ # The Eyeling verdict is emitted only when all message-specific validations have
107
+ # been derived from the log metadata, ordered message list, payload graphs, and
108
+ # built-ins.
88
109
  {
89
- :temperatureLog msg:orderedMessages ?Messages.
110
+ :temperatureLog rmsg:orderedMessages ?Messages.
90
111
  ?Messages list:length ?Count.
91
- :m001 msg:boundaryExplicit true;
92
- msg:payloadResult ?FirstResult.
93
- :m002 msg:boundaryExplicit true;
94
- msg:emptyMessageAllowed true.
95
- :m003 msg:boundaryExplicit true;
96
- msg:payloadResult ?SecondResult.
112
+ :m001 rmsg:boundaryExplicit true;
113
+ rmsg:payloadResult ?FirstResult.
114
+ :m002 rmsg:boundaryExplicit true;
115
+ rmsg:emptyMessageAllowed true.
116
+ :m003 rmsg:boundaryExplicit true;
117
+ rmsg:payloadResult ?SecondResult.
97
118
  :BlankNodeScope :reusedLabel ?Label;
98
119
  :isPerMessage true.
99
120
  :MessageContext :differentObservationsStayContextual true.
100
- ("# rdf-messages\n\n## Source files\n\n- [N3 rules](../rdf-messages.n3)\n- [Input TriG](../input/rdf-messages.trig)\n\n## Answer\nRDF Message log accepted: %d explicit message boundaries are preserved. Message :m002 is an empty heartbeat, and the local blank-node label %s is safely reused in separate messages.\n\n## Explanation\nThe N3 source models an RDF Message Log as an ordered sequence of RDF Messages. Each non-empty message has a formula-valued payload that is inspected with log:includes, so the observation data stays inside the message boundary instead of being treated as one global graph. The two temperature results, %s and %s, are different observations from the same stream but are contextualized by their message boundaries." ?Count ?Label ?FirstResult ?SecondResult) string:format ?Block.
121
+ ("# rdf-messages\n\n## Source files\n\n- [N3 rules](../rdf-messages.n3)\n- [Input TriG](../input/rdf-messages.trig)\n\n## Answer\nRDF Message replay archive accepted: %d explicit message boundaries are preserved. Message :m002 is an empty heartbeat, and the local blank-node label %s is safely reused in separate message envelopes.\n\n## Explanation\nThe input is a single runnable example split across an N3 rule file and a TriG sidecar. The TriG file uses application-local envelope facts for stream order and replay metadata, while each non-empty named payload graph is treated as an atomic RDF Message dataset. Payloads are inspected with log:includes inside their own formulas, so the observation data stays inside the message boundary instead of being treated as one global graph. The two temperature results, %s and %s, are different observations from the same stream but are contextualized by their message boundaries.\n\nThis is intentionally not a parser-level VERSION \\\"1.2-messages\\\" / MESSAGE delimiter test. It is a reasoning example over an already-materialized sidecar representation of a message log." ?Count ?Label ?FirstResult ?SecondResult) string:format ?Block.
101
122
  } => {
102
123
  :rdfMessagesExample log:outputString ?Block.
103
124
  :rdfMessagesExample :demonstrates :ExplicitBoundaries, :AtomicMessageContext, :EmptyHeartbeat, :MessageScopedBlankNodes, :ReplayableMessageLog.
104
125
  }.
105
-
package/eyeling.js CHANGED
@@ -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 ? termFastKey(t) : null;
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, replStr);
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();