eyeling 1.8.3 → 1.8.5

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 CHANGED
@@ -21,7 +21,7 @@ Try it here:
21
21
  - Load an N3 program from a URL (in the "Load N3 from URL" box or as ?url=...).
22
22
  - Share a link with the program encoded in the URL fragment (`#...`).
23
23
 
24
- - Streaming demo
24
+ - Streaming demo tab
25
25
  - Browse a Wikidata entity, load its facts, and see Eyeling’s **deductive closure appear incrementally** as triples are derived.
26
26
  - Edit **N3 rules live** and re-run to watch how different inference rules change what gets derived.
27
27
  - Demo **CORS-safe dynamic fetching**: derived “fetch requests” can trigger extra facts (e.g., Wikiquote extracts) that are injected and re-reasoned.
@@ -0,0 +1,182 @@
1
+ # =============================================================================
2
+ # AIR-style policy reasoning with a paraconsistent conflict and deterministic
3
+ # conflict resolution.
4
+ #
5
+ # Scenario:
6
+ # - Jos has Flu; Flu is a Disease -> Jos is Sick.
7
+ # - Doctor says Jos can do ProgrammingWork.
8
+ # - Policy: Sick + Work => deny Work (general).
9
+ # - Policy: Sick + Work + Office => deny Work (avoid infecting colleagues).
10
+ # - Resolution: If conflicted:
11
+ # * Work => Deny
12
+ # * Home+ProgrammingWork => Permit
13
+ # * otherwise => Undecided
14
+ #
15
+ # Technique:
16
+ # - Keep facts+rules in a quoted KB formula, compute closure with log:conclusion.
17
+ # - Use log:includes / log:notIncludes for scoped checks (SNAF).
18
+ # =============================================================================
19
+
20
+ @prefix : <http://example.org/#>.
21
+ @prefix air: <http://dig.csail.mit.edu/TAMI/2007/amord/air#>.
22
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
23
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
24
+
25
+ # -----------------------------------------------------------------------------
26
+ # 1) KB formula
27
+ # -----------------------------------------------------------------------------
28
+
29
+ :kb :hasFormula {
30
+
31
+ # Facts
32
+ :Jos a :Person ; :has :Flu .
33
+ :Flu :typeOf :Disease.
34
+
35
+ :Home a :Location.
36
+ :Office a :Location.
37
+
38
+ :ProgrammingWork rdfs:subClassOf :Work.
39
+
40
+ # Requests
41
+ :Request_Jos_Prog_Home a :Request ;
42
+ :agent :Jos ;
43
+ :job :ProgrammingWork ;
44
+ :location :Home .
45
+
46
+ :Request_Jos_Prog_Office a :Request ;
47
+ :agent :Jos ;
48
+ :job :ProgrammingWork ;
49
+ :location :Office .
50
+
51
+ # Doctor said (quoted)
52
+ :Doctor :says { :Jos :canDo :ProgrammingWork. }.
53
+
54
+ # Lift quoted statement
55
+ { ?doc :says ?G.
56
+ ?G log:includes { ?x :canDo ?job. }.
57
+ } => { ?x :canDo ?job. }.
58
+
59
+ # Flu -> Disease
60
+ { ?x :has ?a. ?a :typeOf ?b. } => { ?x :has ?b. }.
61
+
62
+ # Disease -> Sick (context)
63
+ { ?x :has :Disease. } => { ?x :healthStatus :Sick. }.
64
+
65
+ # Minimal subclass closure helper
66
+ { ?a rdfs:subClassOf ?b. } => { ?a :isKindOf ?b. }.
67
+ { ?a :isKindOf ?b. ?b :isKindOf ?c. } => { ?a :isKindOf ?c. }.
68
+
69
+ # canDo propagates to supertypes
70
+ { ?x :canDo ?job. ?job rdfs:subClassOf ?super. } => { ?x :canDo ?super. }.
71
+
72
+ # ----------------------------------------------------------
73
+ # AIR-annotated rules (documentation) + executable counterparts
74
+ # ----------------------------------------------------------
75
+
76
+ # Doctor: if canDo requested job => Permit
77
+ :R_DoctorPermit a air:Rule;
78
+ air:if { ?req a :Request; :agent ?x; :job ?job. ?x :canDo ?job. };
79
+ air:then { ?req :decision :Permit. }.
80
+
81
+ { ?req a :Request; :agent ?x; :job ?job. ?x :canDo ?job. }
82
+ => { ?req :decision :Permit. }.
83
+
84
+ # Health: Sick + Work => Deny
85
+ :R_SickNoWork a air:Rule;
86
+ air:if { ?req a :Request; :agent ?x; :job ?job. ?x :healthStatus :Sick. ?job :isKindOf :Work. };
87
+ air:then { ?req :decision :Deny. }.
88
+
89
+ { ?req a :Request; :agent ?x; :job ?job. ?x :healthStatus :Sick. ?job :isKindOf :Work. }
90
+ => { ?req :decision :Deny. }.
91
+
92
+ # Health: Sick + Work + Office => Deny (avoid infecting colleagues)
93
+ :R_SickWork a air:Rule;
94
+ air:if { ?req a :Request; :agent ?x; :job ?job; :location :Office.
95
+ ?x :healthStatus :Sick. ?job :isKindOf :Office. };
96
+ air:then { ?req :decision :Deny. ?x :avoidInfectingColleagues true. }.
97
+
98
+ { ?req a :Request; :agent ?x; :job ?job; :location :Office.
99
+ ?x :healthStatus :Sick. ?job :isKindOf :Office. }
100
+ => { ?req :decision :Deny. ?x :avoidInfectingColleagues true. }.
101
+
102
+ # Resolution hint: Sick + ProgrammingWork at Home => Permit
103
+ :R_SickProgHome a air:Rule;
104
+ air:if { ?req a :Request; :agent ?x; :job :ProgrammingWork; :location :Home. ?x :healthStatus :Sick. };
105
+ air:then { ?req :decision :Permit. ?x :remoteWorkRecommended true. }.
106
+
107
+ { ?req a :Request; :agent ?x; :job :ProgrammingWork; :location :Home. ?x :healthStatus :Sick. }
108
+ => { ?req :decision :Permit. ?x :remoteWorkRecommended true. }.
109
+ }.
110
+
111
+ # -----------------------------------------------------------------------------
112
+ # 2) Lift request facts out of the closure (small, deterministic output)
113
+ # -----------------------------------------------------------------------------
114
+
115
+ { :kb :hasFormula ?F. ?F log:conclusion ?C.
116
+ ?C log:includes { ?req a :Request; :agent ?who; :job ?job; :location ?loc. }.
117
+ } => { ?req :agent ?who; :job ?job; :location ?loc. }.
118
+
119
+ # -----------------------------------------------------------------------------
120
+ # 3) decisionStatus via scoped checks on closure
121
+ # -----------------------------------------------------------------------------
122
+
123
+ { :kb :hasFormula ?F. ?F log:conclusion ?C.
124
+ ?C log:includes { ?req :decision :Permit. }.
125
+ ?C log:includes { ?req :decision :Deny. }.
126
+ } => { ?req :decisionStatus :BothPermitDeny. }.
127
+
128
+ { :kb :hasFormula ?F. ?F log:conclusion ?C.
129
+ ?C log:includes { ?req :decision :Permit. }.
130
+ ?C log:notIncludes { ?req :decision :Deny. }.
131
+ } => { ?req :decisionStatus :PermitOnly. }.
132
+
133
+ { :kb :hasFormula ?F. ?F log:conclusion ?C.
134
+ ?C log:includes { ?req :decision :Deny. }.
135
+ ?C log:notIncludes { ?req :decision :Permit. }.
136
+ } => { ?req :decisionStatus :DenyOnly. }.
137
+
138
+ { :kb :hasFormula ?F. ?F log:conclusion ?C.
139
+ ?C log:notIncludes { ?req :decision :Permit. }.
140
+ ?C log:notIncludes { ?req :decision :Deny. }.
141
+ } => { ?req :decisionStatus :Neither. }.
142
+
143
+ # -----------------------------------------------------------------------------
144
+ # 4) effectiveDecision (mutually exclusive; no extra Undecided)
145
+ # -----------------------------------------------------------------------------
146
+
147
+ # Non-conflicts
148
+ { ?req :decisionStatus :PermitOnly. } => { ?req :effectiveDecision :Permit. }.
149
+ { ?req :decisionStatus :DenyOnly. } => { ?req :effectiveDecision :Deny. }.
150
+ { ?req :decisionStatus :Neither. } => { ?req :effectiveDecision :NotSpecified. }.
151
+
152
+ # Conflict + Work => Deny (+ flag in same rule)
153
+ { :kb :hasFormula ?F. ?F log:conclusion ?C.
154
+ ?C log:includes { ?req :decision :Permit. }.
155
+ ?C log:includes { ?req :decision :Deny. }.
156
+ ?C log:includes { ?req :location :Office }.
157
+ } => { ?req :effectiveDecision :Deny. }.
158
+
159
+ # Conflict + Home + ProgrammingWork => Permit (+ flag)
160
+ { :kb :hasFormula ?F. ?F log:conclusion ?C.
161
+ ?C log:includes { ?req :decision :Permit. }.
162
+ ?C log:includes { ?req :decision :Deny. }.
163
+ ?C log:includes { ?req :location :Home. }.
164
+ ?C log:includes { ?req :job :ProgrammingWork. }.
165
+ } => { ?req :effectiveDecision :Permit. }.
166
+
167
+ # Conflict that matches neither resolver => Undecided (+ flag)
168
+ { :kb :hasFormula ?F. ?F log:conclusion ?C.
169
+ ?C log:includes { ?req :decision :Permit. }.
170
+ ?C log:includes { ?req :decision :Deny. }.
171
+ ?C log:notIncludes { ?req :location :Office. }.
172
+ ?C log:notIncludes { ?req :location :Home. ?req :job :ProgrammingWork. }.
173
+ } => { ?req :effectiveDecision :Undecided. }.
174
+
175
+ # -----------------------------------------------------------------------------
176
+ # 5) Summary: if Home permitted + Work denied => RemoteWorkOnly
177
+ # -----------------------------------------------------------------------------
178
+
179
+ { ?rH :agent ?who; :location :Home; :effectiveDecision :Permit.
180
+ ?rC :agent ?who; :location :Office; :effectiveDecision :Deny.
181
+ } => { ?who :workDecision :RemoteWorkOnly. }.
182
+
@@ -0,0 +1,379 @@
1
+ # =============================================================================
2
+ # This example illustrates “eventual interoperability”:
3
+ # 1) Start with rich, internally coherent source semantics (my:*).
4
+ # 2) Add interoperability later as explicit, reusable alignments when a
5
+ # concrete interaction/business case exists.
6
+ # 3) Alignments can be versioned and swapped without changing the source.
7
+ # 4) Same source can be aligned one-to-many: different partners, different
8
+ # target shapes.
9
+ #
10
+ # The file models a dataset owner publishing a roster using their own rich
11
+ # vocabulary (my:) without forcing early agreement on a shared schema.
12
+ # When two concrete reuse needs appear (exporting to Partner A and Partner B),
13
+ # each integration is handled by introducing an interaction pattern:
14
+ # alignment artefact (mappings) + an expected output shape.
15
+ # The reasoner selects the active alignment for each partner, applies
16
+ # class/property/inverse-property mappings to produce each partner’s expected
17
+ # shape, and then emits “ready” roster views (:PartnerA_Roster, :PartnerB_Roster).
18
+ # In other words: local semantics first, interoperability later, and one dataset
19
+ # can be aligned to multiple consumers without changing the producer’s model.
20
+ #
21
+ # SHACL note:
22
+ # SHACL NodeShapes describe each partner’s expected output shape and act as a
23
+ # consumer-facing “contract” for the aligned view (they can also be validated).
24
+ #
25
+ # Inspired by: https://pietercolpaert.be/interoperability/2026/01/08/eventual-interoperability
26
+ # =============================================================================
27
+
28
+ @prefix my: <http://example.org/my#>.
29
+ @prefix a: <http://example.org/partnerA#>.
30
+ @prefix b: <http://example.org/partnerB#>.
31
+ @prefix align: <http://example.org/align#>.
32
+ @prefix : <http://example.org/demo#>.
33
+
34
+ @prefix list: <http://www.w3.org/2000/10/swap/list#>.
35
+ @prefix math: <http://www.w3.org/2000/10/swap/math#>.
36
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
37
+ @prefix owl: <http://www.w3.org/2002/07/owl#>.
38
+ @prefix sh: <http://www.w3.org/ns/shacl#>.
39
+
40
+ # ----------------------------------------------------------
41
+ # 1) Producer data (rich, local semantics)
42
+ # ----------------------------------------------------------
43
+
44
+ my:Course_KG a my:Course ;
45
+ my:courseCode "KG-101" ;
46
+ my:title "Knowledge Graphs" .
47
+
48
+ my:Student_A a my:Student ;
49
+ my:givenName "Ava" ;
50
+ my:familyName "Ng" ;
51
+ my:displayName "Ava Ng" ;
52
+ my:studentID "S123" ;
53
+ my:enrolledInCourse my:Course_KG ;
54
+ my:enrollmentRole my:TeachingAssistant .
55
+
56
+ my:Student_B a my:Student ;
57
+ my:givenName "Bram" ;
58
+ my:familyName "Peeters" ;
59
+ my:displayName "Bram Peeters" ;
60
+ my:studentID "S124" ;
61
+ my:enrolledInCourse my:Course_KG ;
62
+ my:enrollmentRole my:Student .
63
+
64
+ # ----------------------------------------------------------
65
+ # 2) Interaction patterns (business cases)
66
+ # ----------------------------------------------------------
67
+
68
+ :RosterExportToPartnerA a :InteractionPattern ;
69
+ :enabled true ;
70
+ :usesAlignmentFamily :AlignFamily_PartnerA ;
71
+ # SHACL shapes used by this interaction pattern:
72
+ :usesShape :AStudentShape, :ACourseShape .
73
+
74
+ :RosterExportToPartnerB a :InteractionPattern ;
75
+ :enabled true ;
76
+ :usesAlignmentFamily :AlignFamily_PartnerB ;
77
+ # SHACL shapes used by this interaction pattern:
78
+ :usesShape :BLearnerShape, :BModuleShape .
79
+
80
+ # ----------------------------------------------------------
81
+ # 3) SHACL shapes (descriptions of what partners expect)
82
+ # ----------------------------------------------------------
83
+
84
+ # Partner A expects:
85
+ # - Students have identifier, givenName, familyName
86
+ # - Courses have at least one hasStudent, and those values are Students
87
+
88
+ :AStudentShape a sh:NodeShape ;
89
+ sh:targetClass a:Student ;
90
+ sh:property [
91
+ a sh:PropertyShape ;
92
+ sh:path a:identifier ;
93
+ sh:minCount 1
94
+ ] ;
95
+ sh:property [
96
+ a sh:PropertyShape ;
97
+ sh:path a:givenName ;
98
+ sh:minCount 1
99
+ ] ;
100
+ sh:property [
101
+ a sh:PropertyShape ;
102
+ sh:path a:familyName ;
103
+ sh:minCount 1
104
+ ] .
105
+
106
+ :ACourseShape a sh:NodeShape ;
107
+ sh:targetClass a:Course ;
108
+ sh:property [
109
+ a sh:PropertyShape ;
110
+ sh:path a:hasStudent ;
111
+ sh:minCount 1 ;
112
+ sh:class a:Student
113
+ ] .
114
+
115
+ # Partner B expects:
116
+ # - Learners have learnerId and fullName
117
+ # - Modules have moduleCode and at least one hasLearner (values are Learners)
118
+
119
+ :BLearnerShape a sh:NodeShape ;
120
+ sh:targetClass b:Learner ;
121
+ sh:property [
122
+ a sh:PropertyShape ;
123
+ sh:path b:learnerId ;
124
+ sh:minCount 1
125
+ ] ;
126
+ sh:property [
127
+ a sh:PropertyShape ;
128
+ sh:path b:fullName ;
129
+ sh:minCount 1
130
+ ] .
131
+
132
+ :BModuleShape a sh:NodeShape ;
133
+ sh:targetClass b:Module ;
134
+ sh:property [
135
+ a sh:PropertyShape ;
136
+ sh:path b:moduleCode ;
137
+ sh:minCount 1
138
+ ] ;
139
+ sh:property [
140
+ a sh:PropertyShape ;
141
+ sh:path b:hasLearner ;
142
+ sh:minCount 1 ;
143
+ sh:class b:Learner
144
+ ] .
145
+
146
+ # ----------------------------------------------------------
147
+ # 4) Alignment artefacts (kept separate from producer model)
148
+ # Demonstrates incremental/versioned alignments.
149
+ # ----------------------------------------------------------
150
+
151
+ :Align_A_v1 a :Alignment ; :active false ; :hasFormula {
152
+ # Minimal mapping: only identifier and enrollment link
153
+ my:studentID owl:equivalentProperty a:identifier .
154
+ my:enrolledInCourse owl:inverseOf a:hasStudent .
155
+ my:Student owl:equivalentClass a:Student .
156
+ my:Course owl:equivalentClass a:Course .
157
+ }.
158
+
159
+ :Align_A_v2 a :Alignment ; :active true ; :hasFormula {
160
+ # More complete mapping (later, when the business case is clear)
161
+ my:studentID owl:equivalentProperty a:identifier .
162
+ my:givenName owl:equivalentProperty a:givenName .
163
+ my:familyName owl:equivalentProperty a:familyName .
164
+ my:enrolledInCourse owl:inverseOf a:hasStudent .
165
+ my:Student owl:equivalentClass a:Student .
166
+ my:Course owl:equivalentClass a:Course .
167
+ }.
168
+
169
+ :AlignFamily_PartnerA :hasMember :Align_A_v1, :Align_A_v2.
170
+
171
+ :Align_B_v1 a :Alignment ; :active true ; :hasFormula {
172
+ # Partner B wants a different shape
173
+ my:Student owl:equivalentClass b:Learner .
174
+ my:studentID owl:equivalentProperty b:learnerId .
175
+ my:displayName owl:equivalentProperty b:fullName .
176
+ my:enrolledInCourse owl:inverseOf b:hasLearner .
177
+ my:Course owl:equivalentClass b:Module .
178
+ my:courseCode owl:equivalentProperty b:moduleCode .
179
+ }.
180
+
181
+ :AlignFamily_PartnerB :hasMember :Align_B_v1.
182
+
183
+ # ----------------------------------------------------------
184
+ # 5) Generic “eventual interoperability” engine
185
+ # - pick the active alignment for the interaction pattern
186
+ # - apply vocabulary-level alignments
187
+ # ----------------------------------------------------------
188
+
189
+ # Pick the active alignment for a pattern from its family
190
+ {
191
+ ?pat a :InteractionPattern ;
192
+ :enabled true ;
193
+ :usesAlignmentFamily ?fam .
194
+ ?fam :hasMember ?al .
195
+ ?al :active true .
196
+ } => {
197
+ ?pat :usesAlignment ?al .
198
+ }.
199
+
200
+ # Equivalent class mapping
201
+ {
202
+ ?pat :usesAlignment ?al .
203
+ ?al :hasFormula ?A.
204
+ ?A log:includes { ?c1 owl:equivalentClass ?c2. }.
205
+ ?x a ?c1.
206
+ } => {
207
+ ?x a ?c2.
208
+ ?x :alignedBy ?pat.
209
+ }.
210
+
211
+ # Equivalent property mapping
212
+ {
213
+ ?pat :usesAlignment ?al .
214
+ ?al :hasFormula ?A.
215
+ ?A log:includes { ?p1 owl:equivalentProperty ?p2. }.
216
+ ?s ?p1 ?o.
217
+ } => {
218
+ ?s ?p2 ?o.
219
+ ?s :alignedBy ?pat.
220
+ }.
221
+
222
+ # Inverse property mapping (shape translation)
223
+ {
224
+ ?pat :usesAlignment ?al .
225
+ ?al :hasFormula ?A.
226
+ ?A log:includes { ?p1 owl:inverseOf ?p2. }.
227
+ ?s ?p1 ?o.
228
+ } => {
229
+ ?o ?p2 ?s.
230
+ ?o :alignedBy ?pat.
231
+ }.
232
+
233
+ # ----------------------------------------------------------
234
+ # 6) Interaction-pattern outputs (same checks as original)
235
+ # These rules correspond directly to the SHACL shapes above.
236
+ # ----------------------------------------------------------
237
+
238
+ # Partner A “ready” (corresponds to :AStudentShape + :ACourseShape)
239
+ {
240
+ ?pat a :InteractionPattern ; :enabled true .
241
+ ?pat :usesShape :AStudentShape, :ACourseShape.
242
+
243
+ ?student a a:Student ;
244
+ a:identifier ?id ;
245
+ a:givenName ?gn ;
246
+ a:familyName ?fn .
247
+ ?course a a:Course ;
248
+ a:hasStudent ?student .
249
+ ?student :alignedBy ?pat.
250
+ } => {
251
+ :PartnerA_Roster :fromPattern ?pat ;
252
+ :includesCourse ?course ;
253
+ :includesStudent ?student ;
254
+ :ready true .
255
+ }.
256
+
257
+ # Partner B “ready” (corresponds to :BLearnerShape + :BModuleShape)
258
+ {
259
+ ?pat a :InteractionPattern ; :enabled true .
260
+ ?pat :usesShape :BLearnerShape, :BModuleShape.
261
+
262
+ ?learner a b:Learner ;
263
+ b:learnerId ?id ;
264
+ b:fullName ?name .
265
+ ?module a b:Module ;
266
+ b:moduleCode ?code ;
267
+ b:hasLearner ?learner .
268
+ ?learner :alignedBy ?pat.
269
+ } => {
270
+ :PartnerB_Roster :fromPattern ?pat ;
271
+ :includesModule ?module ;
272
+ :includesLearner ?learner ;
273
+ :ready true .
274
+ }.
275
+
276
+ # ----------------------------------------------------------
277
+ # 7) SHACL validation in N3 (minimal subset)
278
+ # - sh:minCount 1
279
+ # - sh:class
280
+ #
281
+ # Reports are per interaction pattern.
282
+ # ----------------------------------------------------------
283
+
284
+ # Reports (stable IRIs; keeps things simple/deterministic)
285
+ :PartnerA_Report a sh:ValidationReport ;
286
+ :forPattern :RosterExportToPartnerA ;
287
+ :forRoster :PartnerA_Roster .
288
+
289
+ :PartnerB_Report a sh:ValidationReport ;
290
+ :forPattern :RosterExportToPartnerB ;
291
+ :forRoster :PartnerB_Roster .
292
+
293
+ # --------------------------
294
+ # minCount violations
295
+ # --------------------------
296
+
297
+ {
298
+ ?rep a sh:ValidationReport ;
299
+ :forPattern ?pat .
300
+
301
+ # this pattern uses this shape
302
+ ?pat :usesShape ?shape .
303
+
304
+ # shape definition
305
+ ?shape a sh:NodeShape ;
306
+ sh:targetClass ?class ;
307
+ sh:property ?ps .
308
+
309
+ ?ps sh:path ?path ;
310
+ sh:minCount ?min .
311
+
312
+ # focus nodes: nodes aligned for this pattern and of the target class
313
+ ?focus a ?class ;
314
+ :alignedBy ?pat .
315
+ # count values in the closure for (focus,path)
316
+ ( 1 { ?focus ?path ?v. } ?vals ) log:collectAllIn ?SCOPE.
317
+ ?vals list:length ?count .
318
+
319
+ # violation if count < min
320
+ ?count math:lessThan ?min .
321
+ } => {
322
+ ?rep sh:result [
323
+ a sh:ValidationResult ;
324
+ sh:focusNode ?focus ;
325
+ sh:resultPath ?path ;
326
+ sh:sourceShape ?shape ;
327
+ sh:sourceConstraintComponent sh:MinCountConstraintComponent ;
328
+ sh:resultMessage "Missing required property (minCount)."
329
+ ] .
330
+ }.
331
+
332
+ # --------------------------
333
+ # sh:class violations
334
+ # --------------------------
335
+
336
+ {
337
+ ?rep a sh:ValidationReport ;
338
+ :forPattern ?pat .
339
+
340
+ ?pat :usesShape ?shape .
341
+
342
+ ?shape a sh:NodeShape ;
343
+ sh:targetClass ?class ;
344
+ sh:property ?ps .
345
+
346
+ ?ps sh:path ?path ;
347
+ sh:class ?requiredClass .
348
+
349
+ ?focus a ?class ;
350
+ :alignedBy ?pat ;
351
+ ?path ?value .
352
+
353
+ # scoped NAF: value does NOT have the required rdf:type
354
+ ( 1 { ?value a ?requiredClass. } () ) log:collectAllIn ?SCOPE.
355
+ } => {
356
+ ?rep sh:result [
357
+ a sh:ValidationResult ;
358
+ sh:focusNode ?focus ;
359
+ sh:resultPath ?path ;
360
+ sh:sourceShape ?shape ;
361
+ sh:sourceConstraintComponent sh:ClassConstraintComponent ;
362
+ sh:resultMessage "Value does not have required sh:class."
363
+ ] .
364
+ }.
365
+
366
+ # --------------------------
367
+ # sh:conforms
368
+ # --------------------------
369
+
370
+ # If any result exists (in the current graph) -> conforms false
371
+ { ?rep a sh:ValidationReport.
372
+ ?rep sh:result ?r.
373
+ } => { ?rep sh:conforms false. }.
374
+
375
+ # If no result exists (SNAF in current scope) -> conforms true
376
+ { ?rep a sh:ValidationReport.
377
+ (1 { ?rep sh:result ?r. } () ) log:collectAllIn 2.
378
+ } => { ?rep sh:conforms true. }.
379
+
@@ -0,0 +1,13 @@
1
+ @prefix : <http://example.org/#> .
2
+
3
+ :Request_Jos_Prog_Home :agent :Jos .
4
+ :Request_Jos_Prog_Home :job :ProgrammingWork .
5
+ :Request_Jos_Prog_Home :location :Home .
6
+ :Request_Jos_Prog_Office :agent :Jos .
7
+ :Request_Jos_Prog_Office :job :ProgrammingWork .
8
+ :Request_Jos_Prog_Office :location :Office .
9
+ :Request_Jos_Prog_Home :decisionStatus :BothPermitDeny .
10
+ :Request_Jos_Prog_Office :decisionStatus :BothPermitDeny .
11
+ :Request_Jos_Prog_Office :effectiveDecision :Deny .
12
+ :Request_Jos_Prog_Home :effectiveDecision :Permit .
13
+ :Jos :workDecision :RemoteWorkOnly .
@@ -0,0 +1,47 @@
1
+ @prefix : <http://example.org/demo#> .
2
+ @prefix a: <http://example.org/partnerA#> .
3
+ @prefix b: <http://example.org/partnerB#> .
4
+ @prefix my: <http://example.org/my#> .
5
+ @prefix sh: <http://www.w3.org/ns/shacl#> .
6
+
7
+ :RosterExportToPartnerA :usesAlignment :Align_A_v2 .
8
+ :RosterExportToPartnerB :usesAlignment :Align_B_v1 .
9
+ my:Student_A a a:Student .
10
+ my:Student_A :alignedBy :RosterExportToPartnerA .
11
+ my:Student_B a a:Student .
12
+ my:Student_B :alignedBy :RosterExportToPartnerA .
13
+ my:Course_KG a a:Course .
14
+ my:Course_KG :alignedBy :RosterExportToPartnerA .
15
+ my:Student_A a b:Learner .
16
+ my:Student_A :alignedBy :RosterExportToPartnerB .
17
+ my:Student_B a b:Learner .
18
+ my:Student_B :alignedBy :RosterExportToPartnerB .
19
+ my:Course_KG a b:Module .
20
+ my:Course_KG :alignedBy :RosterExportToPartnerB .
21
+ my:Student_A a:identifier "S123" .
22
+ my:Student_B a:identifier "S124" .
23
+ my:Student_A a:givenName "Ava" .
24
+ my:Student_B a:givenName "Bram" .
25
+ my:Student_A a:familyName "Ng" .
26
+ my:Student_B a:familyName "Peeters" .
27
+ my:Student_A b:learnerId "S123" .
28
+ my:Student_B b:learnerId "S124" .
29
+ my:Student_A b:fullName "Ava Ng" .
30
+ my:Student_B b:fullName "Bram Peeters" .
31
+ my:Course_KG b:moduleCode "KG-101" .
32
+ my:Course_KG a:hasStudent my:Student_A .
33
+ my:Course_KG a:hasStudent my:Student_B .
34
+ my:Course_KG b:hasLearner my:Student_A .
35
+ my:Course_KG b:hasLearner my:Student_B .
36
+ :PartnerA_Roster :fromPattern :RosterExportToPartnerA .
37
+ :PartnerA_Roster :includesCourse my:Course_KG .
38
+ :PartnerA_Roster :includesStudent my:Student_A .
39
+ :PartnerA_Roster :ready true .
40
+ :PartnerA_Roster :includesStudent my:Student_B .
41
+ :PartnerB_Roster :fromPattern :RosterExportToPartnerB .
42
+ :PartnerB_Roster :includesModule my:Course_KG .
43
+ :PartnerB_Roster :includesLearner my:Student_A .
44
+ :PartnerB_Roster :ready true .
45
+ :PartnerB_Roster :includesLearner my:Student_B .
46
+ :PartnerA_Report sh:conforms true .
47
+ :PartnerB_Report sh:conforms true .
@@ -0,0 +1,5 @@
1
+ @prefix : <http://example.org/demo#> .
2
+ @prefix sh: <http://www.w3.org/ns/shacl#> .
3
+
4
+ :PartnerA_Report sh:conforms true .
5
+ :PartnerB_Report sh:conforms true .
@@ -0,0 +1,24 @@
1
+ # --------------------------
2
+ # sh:conforms example
3
+ # --------------------------
4
+
5
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
6
+ @prefix sh: <http://www.w3.org/ns/shacl#>.
7
+ @prefix : <http://example.org/demo#>.
8
+
9
+ # If any result exists (in the closure) -> conforms false
10
+ { <https://eyereasoner.github.io/eyeling/examples/eventual-interoperability-interaction-patterns.n3> log:semantics ?formula.
11
+ #<file:///home/jdroo/github.com/eyereasoner/eyeling/examples/eventual-interoperability-interaction-patterns.n3> log:semantics ?formula.
12
+ ?formula log:conclusion ?closure.
13
+ ?closure log:includes { ?rep a sh:ValidationReport. }.
14
+ ?closure log:includes { ?rep sh:result ?r. }.
15
+ } => { ?rep sh:conforms false. }.
16
+
17
+ # If no result exists (in the closure) -> conforms true
18
+ { <https://eyereasoner.github.io/eyeling/examples/eventual-interoperability-interaction-patterns.n3> log:semantics ?formula.
19
+ #<file:///home/jdroo/github.com/eyereasoner/eyeling/examples/eventual-interoperability-interaction-patterns.n3> log:semantics ?formula.
20
+ ?formula log:conclusion ?closure.
21
+ ?closure log:includes { ?rep a sh:ValidationReport. }.
22
+ ?closure log:notIncludes { ?rep sh:result ?r. }.
23
+ } => { ?rep sh:conforms true. }.
24
+
package/eyeling.js CHANGED
@@ -4454,6 +4454,30 @@ function evalCryptoHashBuiltin(g, subst, algo) {
4454
4454
  return s2 !== null ? [s2] : [];
4455
4455
  }
4456
4456
 
4457
+ // ---------------------------------------------------------------------------
4458
+ // log: scoped-closure priority helper
4459
+ // ---------------------------------------------------------------------------
4460
+ // When log:collectAllIn / log:forAllIn are used with an object that is a
4461
+ // natural number literal, that number is treated as a *priority* (closure level).
4462
+ // See the adapted semantics near those builtins.
4463
+ function __logNaturalPriorityFromTerm(t) {
4464
+ const info = parseNumericLiteralInfo(t);
4465
+ if (!info) return null;
4466
+ if (info.dt !== XSD_INTEGER_DT) return null;
4467
+
4468
+ const v = info.value;
4469
+ if (typeof v === 'bigint') {
4470
+ if (v < 0n) return null;
4471
+ if (v > BigInt(Number.MAX_SAFE_INTEGER)) return null;
4472
+ return Number(v);
4473
+ }
4474
+ if (typeof v === 'number') {
4475
+ if (!Number.isInteger(v) || v < 0) return null;
4476
+ return v;
4477
+ }
4478
+ return null;
4479
+ }
4480
+
4457
4481
  // ===========================================================================
4458
4482
  // Builtin evaluation
4459
4483
  // ===========================================================================
@@ -6103,6 +6127,15 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
6103
6127
  const [valueTempl, clauseTerm, listTerm] = g.s.elems;
6104
6128
  if (!(clauseTerm instanceof GraphTerm)) return [];
6105
6129
 
6130
+ // Priority / closure semantics:
6131
+ // - object = GraphTerm: explicit scope, run immediately (no closure gating)
6132
+ // - object = natural number literal N: delay until saturated closure level >= N
6133
+ // * N = 0 => run immediately (use the live fact store)
6134
+ // * N >= 1 => run only when a scoped snapshot exists at closure level >= N
6135
+ // - object = Var: treat as priority 1 (do not bind)
6136
+ // - any other object: backward-compatible default priority 1
6137
+
6138
+ let outSubst = { ...subst };
6106
6139
  let scopeFacts = null;
6107
6140
  let scopeBackRules = backRules;
6108
6141
 
@@ -6114,15 +6147,39 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
6114
6147
  enumerable: false,
6115
6148
  writable: true,
6116
6149
  });
6150
+ const lvlHere = facts && typeof facts.__scopedClosureLevel === 'number' ? facts.__scopedClosureLevel : 0;
6151
+ Object.defineProperty(scopeFacts, '__scopedClosureLevel', {
6152
+ value: lvlHere,
6153
+ enumerable: false,
6154
+ writable: true,
6155
+ });
6117
6156
  scopeBackRules = [];
6118
6157
  } else {
6119
- scopeFacts = facts.__scopedSnapshot || null;
6120
- if (!scopeFacts) return []; // DELAY until snapshot exists
6158
+ let prio = 1;
6159
+ if (g.o instanceof Var) {
6160
+ // Unbound var: behave as priority 1 (do not bind)
6161
+ prio = 1;
6162
+ } else {
6163
+ const p0 = __logNaturalPriorityFromTerm(g.o);
6164
+ if (p0 !== null) prio = p0;
6165
+ }
6166
+
6167
+ if (prio === 0) {
6168
+ // Immediate: use live closure during the current fixpoint phase.
6169
+ scopeFacts = facts;
6170
+ scopeBackRules = backRules;
6171
+ } else {
6172
+ const snap = facts.__scopedSnapshot || null;
6173
+ const lvl = (facts && typeof facts.__scopedClosureLevel === 'number' && facts.__scopedClosureLevel) || 0;
6174
+ if (!snap) return []; // DELAY until snapshot exists
6175
+ if (lvl < prio) return []; // DELAY until saturated closure prio exists
6176
+ scopeFacts = snap;
6177
+ }
6121
6178
  }
6122
6179
 
6123
6180
  // If sols is a blank node succeed without collecting/binding.
6124
6181
  if (listTerm instanceof Blank) {
6125
- return [{ ...subst }];
6182
+ return [outSubst];
6126
6183
  }
6127
6184
 
6128
6185
  const visited2 = [];
@@ -6139,7 +6196,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
6139
6196
  const collected = sols.map((sBody) => applySubstTerm(valueTempl, sBody));
6140
6197
  const collectedList = new ListTerm(collected);
6141
6198
 
6142
- const s2 = unifyTerm(listTerm, collectedList, { ...subst });
6199
+ const s2 = unifyTerm(listTerm, collectedList, outSubst);
6143
6200
  return s2 ? [s2] : [];
6144
6201
  }
6145
6202
 
@@ -6149,6 +6206,9 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
6149
6206
  const [whereClause, thenClause] = g.s.elems;
6150
6207
  if (!(whereClause instanceof GraphTerm) || !(thenClause instanceof GraphTerm)) return [];
6151
6208
 
6209
+ // See log:collectAllIn above for the priority / closure semantics.
6210
+
6211
+ let outSubst = { ...subst };
6152
6212
  let scopeFacts = null;
6153
6213
  let scopeBackRules = backRules;
6154
6214
 
@@ -6160,10 +6220,33 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
6160
6220
  enumerable: false,
6161
6221
  writable: true,
6162
6222
  });
6223
+ const lvlHere = facts && typeof facts.__scopedClosureLevel === 'number' ? facts.__scopedClosureLevel : 0;
6224
+ Object.defineProperty(scopeFacts, '__scopedClosureLevel', {
6225
+ value: lvlHere,
6226
+ enumerable: false,
6227
+ writable: true,
6228
+ });
6163
6229
  scopeBackRules = [];
6164
6230
  } else {
6165
- scopeFacts = facts.__scopedSnapshot || null;
6166
- if (!scopeFacts) return []; // DELAY until snapshot exists
6231
+ let prio = 1;
6232
+ if (g.o instanceof Var) {
6233
+ // Unbound var: behave as priority 1 (do not bind)
6234
+ prio = 1;
6235
+ } else {
6236
+ const p0 = __logNaturalPriorityFromTerm(g.o);
6237
+ if (p0 !== null) prio = p0;
6238
+ }
6239
+
6240
+ if (prio === 0) {
6241
+ scopeFacts = facts;
6242
+ scopeBackRules = backRules;
6243
+ } else {
6244
+ const snap = facts.__scopedSnapshot || null;
6245
+ const lvl = (facts && typeof facts.__scopedClosureLevel === 'number' && facts.__scopedClosureLevel) || 0;
6246
+ if (!snap) return []; // DELAY until snapshot exists
6247
+ if (lvl < prio) return []; // DELAY until saturated closure prio exists
6248
+ scopeFacts = snap;
6249
+ }
6167
6250
  }
6168
6251
 
6169
6252
  const visited1 = [];
@@ -6190,7 +6273,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
6190
6273
  );
6191
6274
  if (!sols2.length) return [];
6192
6275
  }
6193
- return [{ ...subst }];
6276
+ return [outSubst];
6194
6277
  }
6195
6278
 
6196
6279
  // log:skolem
@@ -6844,7 +6927,45 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
6844
6927
  backRules.__allForwardRules = forwardRules;
6845
6928
  backRules.__allBackwardRules = backRules;
6846
6929
 
6847
- function setScopedSnapshot(snap) {
6930
+ // Closure level counter used by log:collectAllIn/log:forAllIn priority gating.
6931
+ // Level 0 means "no frozen snapshot" (during Phase A of each outer iteration).
6932
+ let scopedClosureLevel = 0;
6933
+
6934
+ // Scan known rules for the maximum requested closure priority in
6935
+ // log:collectAllIn / log:forAllIn goals.
6936
+ function computeMaxScopedClosurePriorityNeeded() {
6937
+ let maxP = 0;
6938
+ function scanTriple(tr) {
6939
+ if (!(tr && tr.p instanceof Iri)) return;
6940
+ const pv = tr.p.value;
6941
+ if (pv !== LOG_NS + 'collectAllIn' && pv !== LOG_NS + 'forAllIn') return;
6942
+ // Explicit scope graphs are immediate and do not require a closure.
6943
+ if (tr.o instanceof GraphTerm) return;
6944
+ // Variable or non-numeric object => default priority 1 (if used).
6945
+ if (tr.o instanceof Var) {
6946
+ if (maxP < 1) maxP = 1;
6947
+ return;
6948
+ }
6949
+ const p0 = __logNaturalPriorityFromTerm(tr.o);
6950
+ if (p0 !== null) {
6951
+ if (p0 > maxP) maxP = p0;
6952
+ } else {
6953
+ if (maxP < 1) maxP = 1;
6954
+ }
6955
+ }
6956
+
6957
+ for (const r of forwardRules) {
6958
+ for (const tr of r.premise) scanTriple(tr);
6959
+ }
6960
+ for (const r of backRules) {
6961
+ for (const tr of r.premise) scanTriple(tr);
6962
+ }
6963
+ return maxP;
6964
+ }
6965
+
6966
+ let maxScopedClosurePriorityNeeded = computeMaxScopedClosurePriorityNeeded();
6967
+
6968
+ function setScopedSnapshot(snap, level) {
6848
6969
  if (!Object.prototype.hasOwnProperty.call(facts, '__scopedSnapshot')) {
6849
6970
  Object.defineProperty(facts, '__scopedSnapshot', {
6850
6971
  value: snap,
@@ -6855,6 +6976,17 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
6855
6976
  } else {
6856
6977
  facts.__scopedSnapshot = snap;
6857
6978
  }
6979
+
6980
+ if (!Object.prototype.hasOwnProperty.call(facts, '__scopedClosureLevel')) {
6981
+ Object.defineProperty(facts, '__scopedClosureLevel', {
6982
+ value: level,
6983
+ enumerable: false,
6984
+ writable: true,
6985
+ configurable: true,
6986
+ });
6987
+ } else {
6988
+ facts.__scopedClosureLevel = level;
6989
+ }
6858
6990
  }
6859
6991
 
6860
6992
  function makeScopedSnapshot() {
@@ -6866,6 +6998,13 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
6866
6998
  writable: true,
6867
6999
  configurable: true,
6868
7000
  });
7001
+ // Propagate closure level so nested scoped builtins can see it.
7002
+ Object.defineProperty(snap, '__scopedClosureLevel', {
7003
+ value: scopedClosureLevel,
7004
+ enumerable: false,
7005
+ writable: true,
7006
+ configurable: true,
7007
+ });
6869
7008
  return snap;
6870
7009
  }
6871
7010
 
@@ -7045,20 +7184,26 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
7045
7184
 
7046
7185
  while (true) {
7047
7186
  // Phase A: scoped builtins disabled => they “delay” (fail) during saturation
7048
- setScopedSnapshot(null);
7187
+ setScopedSnapshot(null, 0);
7049
7188
  const changedA = runFixpoint();
7050
7189
 
7051
7190
  // Freeze saturated scope
7191
+ scopedClosureLevel += 1;
7052
7192
  const snap = makeScopedSnapshot();
7053
7193
 
7054
7194
  // Phase B: scoped builtins enabled, but they query only `snap`
7055
- setScopedSnapshot(snap);
7195
+ setScopedSnapshot(snap, scopedClosureLevel);
7056
7196
  const changedB = runFixpoint();
7057
7197
 
7058
- if (!changedA && !changedB) break;
7198
+ // Rules may have been added dynamically (rule-producing triples), possibly
7199
+ // introducing higher closure priorities. Keep iterating until we have
7200
+ // reached the maximum requested priority and no further changes occur.
7201
+ maxScopedClosurePriorityNeeded = Math.max(maxScopedClosurePriorityNeeded, computeMaxScopedClosurePriorityNeeded());
7202
+
7203
+ if (!changedA && !changedB && scopedClosureLevel >= maxScopedClosurePriorityNeeded) break;
7059
7204
  }
7060
7205
 
7061
- setScopedSnapshot(null);
7206
+ setScopedSnapshot(null, 0);
7062
7207
 
7063
7208
  return derivedForward;
7064
7209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [