eyeling 1.5.31 → 1.5.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -239,7 +239,7 @@ As soon as the premise is provable, `eyeling` exits with status code `2`.
239
239
  - **list**: `list:append` `list:first` `list:firstRest` `list:in` `list:iterate` `list:last` `list:length` `list:map` `list:member` `list:memberAt` `list:notMember` `list:remove` `list:rest` `list:reverse` `list:sort`
240
240
  - **log**: `log:collectAllIn` `log:equalTo` `log:forAllIn` `log:impliedBy` `log:implies` `log:notEqualTo` `log:notIncludes` `log:skolem` `log:uri`
241
241
  - **math**: `math:absoluteValue` `math:acos` `math:asin` `math:atan` `math:cos` `math:cosh` `math:degrees` `math:difference` `math:equalTo` `math:exponentiation` `math:greaterThan` `math:integerQuotient` `math:lessThan` `math:negation` `math:notEqualTo` `math:notGreaterThan` `math:notLessThan` `math:product` `math:quotient` `math:remainder` `math:rounded` `math:sin` `math:sinh` `math:sum` `math:tan` `math:tanh`
242
- - **string**: `string:concatenation` `string:contains` `string:containsIgnoringCase` `string:endsWith` `string:equalIgnoringCase` `string:format` `string:greaterThan` `string:lessThan` `string:matches` `string:notEqualIgnoringCase` `string:notGreaterThan` `string:notLessThan` `string:notMatches` `string:replace` `string:scrape` `string:startsWith`
242
+ - **string**: `string:concatenation` `string:contains` `string:containsIgnoringCase` `string:endsWith` `string:equalIgnoringCase` `string:format` `string:greaterThan` `string:jsonPointer` `string:lessThan` `string:matches` `string:notEqualIgnoringCase` `string:notGreaterThan` `string:notLessThan` `string:notMatches` `string:replace` `string:scrape` `string:startsWith`
243
243
  - **time**: `time:localTime`
244
244
 
245
245
  ## License
@@ -0,0 +1,70 @@
1
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
2
+ @prefix string: <http://www.w3.org/2000/10/swap/string#> .
3
+ @prefix list: <http://www.w3.org/2000/10/swap/list#> .
4
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
5
+ @prefix ex: <http://example.org/> .
6
+
7
+ ex:doc ex:json """{
8
+ "users": [
9
+ { "id": "u1", "email": "ada@example.org", "profile/name": "Ada Lovelace" },
10
+ { "id": "u2", "email": "bob@evil.invalid", "profile/name": "Bob Mallory" }
11
+ ],
12
+ "policy": { "allowedDomains": ["example.org", "example.com"] }
13
+ }"""^^rdf:JSON .
14
+
15
+ # Sanity check: key contains "/" so JSON Pointer must use "~1"
16
+ {
17
+ ex:doc ex:json ?J .
18
+ (?J "/users/0/profile~1name") string:jsonPointer "Ada Lovelace" .
19
+ } => {
20
+ ex:checks ex:firstUserNameOk true .
21
+ } .
22
+
23
+ # Allowed users: email domain is in policy.allowedDomains
24
+ {
25
+ ex:doc ex:json ?J .
26
+ (?J "/policy/allowedDomains") string:jsonPointer ?Allowed .
27
+ (?J "/users") string:jsonPointer ?Users .
28
+
29
+ ?Users list:iterate (?Idx ?UserJson) .
30
+ (?UserJson "/id") string:jsonPointer ?Id .
31
+ (?UserJson "/email") string:jsonPointer ?Email .
32
+ (?UserJson "/profile~1name") string:jsonPointer ?Name .
33
+
34
+ (?Email "@([^@]+)$") string:scrape ?EmailDomain .
35
+ ?Allowed list:member ?EmailDomain .
36
+
37
+ ("urn:example:user:%s" ?Id) string:format ?UriStr .
38
+ ?User log:uri ?UriStr .
39
+ } => {
40
+ ?User a ex:AllowedUser ;
41
+ ex:name ?Name ;
42
+ ex:email ?Email ;
43
+ ex:emailDomain ?EmailDomain ;
44
+ ex:userIndex ?Idx .
45
+ } .
46
+
47
+ # Blocked users: email domain is NOT in the allowlist
48
+ {
49
+ ex:doc ex:json ?J .
50
+ (?J "/policy/allowedDomains") string:jsonPointer ?Allowed .
51
+ (?J "/users") string:jsonPointer ?Users .
52
+
53
+ ?Users list:iterate (?Idx ?UserJson) .
54
+ (?UserJson "/id") string:jsonPointer ?Id .
55
+ (?UserJson "/email") string:jsonPointer ?Email .
56
+ (?UserJson "/profile~1name") string:jsonPointer ?Name .
57
+
58
+ (?Email "@([^@]+)$") string:scrape ?EmailDomain .
59
+ ?Allowed list:notMember ?EmailDomain .
60
+
61
+ ("urn:example:user:%s" ?Id) string:format ?UriStr .
62
+ ?User log:uri ?UriStr .
63
+ } => {
64
+ ?User a ex:BlockedUser ;
65
+ ex:name ?Name ;
66
+ ex:email ?Email ;
67
+ ex:emailDomain ?EmailDomain ;
68
+ ex:userIndex ?Idx .
69
+ } .
70
+
@@ -0,0 +1,356 @@
1
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
2
+ @prefix string: <http://www.w3.org/2000/10/swap/string#> .
3
+ @prefix list: <http://www.w3.org/2000/10/swap/list#> .
4
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
5
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
6
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
7
+ @prefix ex: <http://example.org/> .
8
+
9
+ # ------------------------------------------------------------------
10
+ # Master data (canonical N3 graph)
11
+ # ------------------------------------------------------------------
12
+
13
+ ex:policy ex:euCountries ("BE" "NL" "DE" "FR" "LU") .
14
+
15
+ ex:Widget a ex:Product ;
16
+ ex:sku "WID-001" ;
17
+ ex:listPriceCents 999 ;
18
+ ex:vatBps 2100 .
19
+
20
+ ex:Gadget a ex:Product ;
21
+ ex:sku "GAD-002" ;
22
+ ex:listPriceCents 1500 ;
23
+ ex:vatBps 600 .
24
+
25
+ # Registry customer keyed by normalized VAT key (alnum only)
26
+ ex:RegistryNL
27
+ ex:vatKey "NL999999999B01" ;
28
+ ex:legalName "Example BV" .
29
+
30
+ # ------------------------------------------------------------------
31
+ # JSON feed (operational system)
32
+ # ------------------------------------------------------------------
33
+
34
+ ex:feed ex:json """{
35
+ "meta~data": { "batch": "2025-12-18" },
36
+ "orders": [
37
+ {
38
+ "orderId": "A100",
39
+ "shipTo": { "country": "BE" },
40
+ "customer": { "email": "Ada@Example.ORG", "vatId": null },
41
+ "lines": [
42
+ { "sku": "wid-001", "qty": 2, "unitCents": 999 },
43
+ { "sku": "GAD-002", "qty": 1, "unitCents": 1500 }
44
+ ],
45
+ "declared": { "netCents": 3498, "vatCents": 509, "grossCents": 4007 }
46
+ },
47
+ {
48
+ "orderId": "A200",
49
+ "shipTo": { "country": "US" },
50
+ "customer": { "email": "bob@example.org", "vatId": null },
51
+ "lines": [
52
+ { "sku": "WID-001", "qty": 2, "unitCents": 999 },
53
+ { "sku": "GAD-002", "qty": 1, "unitCents": 1500 }
54
+ ],
55
+ "declared": { "netCents": 3498, "vatCents": 0, "grossCents": 3498 }
56
+ },
57
+ {
58
+ "orderId": "A300",
59
+ "shipTo": { "country": "NL" },
60
+ "customer": { "email": "acct@company.nl", "vatId": "NL999999999B01" },
61
+ "lines": [
62
+ { "sku": "WID-001", "qty": 2, "unitCents": 999 },
63
+ { "sku": "GAD-002", "qty": 1, "unitCents": 1500 }
64
+ ],
65
+ "declared": { "netCents": 3498, "vatCents": 0, "grossCents": 3498 }
66
+ },
67
+ {
68
+ "orderId": "A301",
69
+ "shipTo": { "country": "NL" },
70
+ "customer": { "email": "billing@company.nl", "vatId": "NL999999999B01" },
71
+ "lines": [
72
+ { "sku": "WID-001", "qty": 2, "unitCents": 999 },
73
+ { "sku": "GAD-002", "qty": 1, "unitCents": 1500 }
74
+ ],
75
+ "declared": { "netCents": 3498, "vatCents": 0, "grossCents": 3498 }
76
+ },
77
+ {
78
+ "orderId": "A400",
79
+ "shipTo": { "country": "BE" },
80
+ "customer": { "email": "ada@other.example", "vatId": null },
81
+ "lines": [
82
+ { "sku": "WID-001", "qty": 2, "unitCents": 999 },
83
+ { "sku": "GAD-002", "qty": 1, "unitCents": 1500 }
84
+ ],
85
+ "declared": { "netCents": 3498, "vatCents": 508, "grossCents": 4006 }
86
+ }
87
+ ]
88
+ }"""^^rdf:JSON .
89
+
90
+ # ------------------------------------------------------------------
91
+ # Sanity checks for JSON Pointer edge-cases
92
+ # ------------------------------------------------------------------
93
+
94
+ {
95
+ ex:feed ex:json ?J .
96
+ (?J "#/orders/0/shipTo/country") string:jsonPointer "BE" .
97
+ } => {
98
+ ex:checks ex:fragmentPointerWorks true .
99
+ } .
100
+
101
+ {
102
+ ex:feed ex:json ?J .
103
+ (?J "/meta~0data/batch") string:jsonPointer "2025-12-18" .
104
+ } => {
105
+ ex:checks ex:tildeEscapeWorks true .
106
+ } .
107
+
108
+ # ------------------------------------------------------------------
109
+ # 1) Materialize orders (JSON -> RDF-ish facts)
110
+ # ------------------------------------------------------------------
111
+
112
+ {
113
+ ex:feed ex:json ?J .
114
+ (?J "/orders") string:jsonPointer ?Orders .
115
+ ?Orders list:iterate (?OrderIndex ?OrderJson) .
116
+
117
+ (?OrderJson "/orderId") string:jsonPointer ?OrderId .
118
+ (?OrderJson "/shipTo/country") string:jsonPointer ?Country .
119
+
120
+ (?OrderJson "/customer") string:jsonPointer ?CustJson .
121
+ (?CustJson "/email") string:jsonPointer ?Email .
122
+ (?CustJson "/vatId") string:jsonPointer ?VatIdRaw .
123
+
124
+ (?OrderJson "/declared") string:jsonPointer ?Decl .
125
+ (?Decl "/netCents") string:jsonPointer ?NetD .
126
+ (?Decl "/vatCents") string:jsonPointer ?VatD .
127
+ (?Decl "/grossCents") string:jsonPointer ?GrossD .
128
+
129
+ ("urn:example:order:%s" ?OrderId) string:format ?OrderUriStr .
130
+ ?Order log:uri ?OrderUriStr .
131
+ } => {
132
+ ?Order a ex:Order ;
133
+ ex:orderId ?OrderId ;
134
+ ex:orderIndex ?OrderIndex ;
135
+ ex:shipCountry ?Country ;
136
+ ex:email ?Email ;
137
+ ex:vatIdRaw ?VatIdRaw ;
138
+ ex:declaredNetCents ?NetD ;
139
+ ex:declaredVatCents ?VatD ;
140
+ ex:declaredGrossCents ?GrossD .
141
+ } .
142
+
143
+ # ------------------------------------------------------------------
144
+ # 2) Materialize customers
145
+ # - If vatId is present -> customer IRI from normalized VAT key
146
+ # - Else -> customer IRI is log:skolem(email)
147
+ # ------------------------------------------------------------------
148
+
149
+ # VAT customer
150
+ {
151
+ ?Order a ex:Order ; ex:vatIdRaw ?VatIdRaw ; ex:email ?Email .
152
+ ?VatIdRaw string:notEqualIgnoringCase "null" .
153
+ (?VatIdRaw "[^0-9A-Za-z]" "") string:replace ?VatKey .
154
+
155
+ ("urn:example:customer:vat:%s" ?VatKey) string:format ?CustUriStr .
156
+ ?Cust log:uri ?CustUriStr .
157
+ } => {
158
+ ?Cust a ex:Customer ;
159
+ ex:vatKey ?VatKey ;
160
+ ex:vatIdRaw ?VatIdRaw ;
161
+ ex:email ?Email .
162
+ ?Order ex:customer ?Cust .
163
+ } .
164
+
165
+ # Non-VAT customer
166
+ {
167
+ ?Order a ex:Order ; ex:vatIdRaw "null" ; ex:email ?Email .
168
+ ?Email log:skolem ?Cust .
169
+ } => {
170
+ ?Cust a ex:Customer ;
171
+ ex:vatIdRaw "null" ;
172
+ ex:email ?Email .
173
+ ?Order ex:customer ?Cust .
174
+ } .
175
+
176
+ # Registry reconciliation (VAT customers only)
177
+ {
178
+ ?Cust a ex:Customer ; ex:vatKey ?VatKey .
179
+ ?Reg ex:vatKey ?VatKey .
180
+ } => {
181
+ ?Cust owl:sameAs ?Reg .
182
+ } .
183
+
184
+ # Detect conflicting emails for the SAME VAT-keyed customer
185
+ # (Use string:lessThan to avoid duplicate symmetric pairs.)
186
+ {
187
+ ?Cust a ex:Customer ; ex:vatKey ?VatKey ; ex:email ?E1 ; ex:email ?E2 .
188
+ ?E1 string:lessThan ?E2 .
189
+ ?E1 string:notEqualIgnoringCase ?E2 .
190
+ } => {
191
+ ?Cust a ex:CustomerEmailConflict ;
192
+ ex:conflictingEmail ?E1, ?E2 .
193
+ } .
194
+
195
+ # ------------------------------------------------------------------
196
+ # 3) Materialize order lines + map SKU -> canonical product + compute money
197
+ # VAT logic:
198
+ # - ship outside EU -> VAT = 0 (export)
199
+ # - ship in EU and customer has VAT -> VAT = 0 (reverse charge)
200
+ # - ship in EU and customer has no VAT -> VAT = product vatBps
201
+ # ------------------------------------------------------------------
202
+
203
+ # Helper macro: create line IRI + compute netCents
204
+ {
205
+ ex:feed ex:json ?J .
206
+ (?J "/orders") string:jsonPointer ?Orders .
207
+ ?Orders list:iterate (?OrderIndex ?OrderJson) .
208
+
209
+ (?OrderJson "/orderId") string:jsonPointer ?OrderId .
210
+ ("urn:example:order:%s" ?OrderId) string:format ?OrderUriStr .
211
+ ?Order log:uri ?OrderUriStr .
212
+
213
+ (?OrderJson "/lines") string:jsonPointer ?Lines .
214
+ ?Lines list:iterate (?LineIndex ?LineJson) .
215
+
216
+ (?LineJson "/sku") string:jsonPointer ?SkuJson .
217
+ (?LineJson "/qty") string:jsonPointer ?Qty .
218
+ (?LineJson "/unitCents") string:jsonPointer ?UnitCents .
219
+
220
+ # SKU reconciliation is case-insensitive
221
+ # (Guard with a type, otherwise already-derived OrderLine nodes can start acting as “products”.)
222
+ ?Product a ex:Product ; ex:sku ?SkuCat .
223
+ ?SkuJson string:equalIgnoringCase ?SkuCat .
224
+
225
+ (?Qty ?UnitCents) math:product ?NetCents .
226
+
227
+ ("urn:example:order:%s#line:%s" ?OrderId ?LineIndex) string:format ?LineUriStr .
228
+ ?Line log:uri ?LineUriStr .
229
+ } => {
230
+ ?Order ex:line ?Line .
231
+
232
+ ?Line a ex:OrderLine ;
233
+ ex:lineIndex ?LineIndex ;
234
+ ex:product ?Product ;
235
+ ex:sku ?SkuCat ;
236
+ ex:qty ?Qty ;
237
+ ex:unitCents ?UnitCents ;
238
+ ex:netCents ?NetCents .
239
+ } .
240
+
241
+ # Export (outside EU): VAT=0
242
+ {
243
+ ?Order a ex:Order ; ex:shipCountry ?Country .
244
+ ex:policy ex:euCountries ?EU .
245
+ ?EU list:notMember ?Country .
246
+
247
+ ?Order ex:line ?Line .
248
+ ?Line ex:netCents ?NetCents .
249
+ } => {
250
+ ?Line ex:vatBps 0 ;
251
+ ex:vatCents 0 ;
252
+ ex:grossCents ?NetCents ;
253
+ ex:vatReason ex:Export .
254
+ } .
255
+
256
+ # Reverse charge (EU + VAT id present): VAT=0
257
+ {
258
+ ?Order a ex:Order ; ex:shipCountry ?Country ; ex:vatIdRaw ?VatIdRaw .
259
+ ex:policy ex:euCountries ?EU .
260
+ ?EU list:member ?Country .
261
+ ?VatIdRaw string:notEqualIgnoringCase "null" .
262
+
263
+ ?Order ex:line ?Line .
264
+ ?Line ex:netCents ?NetCents .
265
+ } => {
266
+ ?Line ex:vatBps 0 ;
267
+ ex:vatCents 0 ;
268
+ ex:grossCents ?NetCents ;
269
+ ex:vatReason ex:ReverseCharge .
270
+ } .
271
+
272
+ # Domestic EU consumer (EU + no VAT): VAT = product vatBps
273
+ {
274
+ ?Order a ex:Order ; ex:shipCountry ?Country ; ex:vatIdRaw "null" .
275
+ ex:policy ex:euCountries ?EU .
276
+ ?EU list:member ?Country .
277
+
278
+ ?Order ex:line ?Line .
279
+ ?Line ex:product ?Product ; ex:netCents ?NetCents .
280
+ ?Product ex:vatBps ?VatBps .
281
+
282
+ (?NetCents ?VatBps) math:product ?NetTimesBps .
283
+ (?NetTimesBps 10000) math:integerQuotient ?VatCents .
284
+ (?NetCents ?VatCents) math:sum ?GrossCents .
285
+ } => {
286
+ ?Line ex:vatBps ?VatBps ;
287
+ ex:vatCents ?VatCents ;
288
+ ex:grossCents ?GrossCents ;
289
+ ex:vatReason ex:Domestic .
290
+ } .
291
+
292
+ # ------------------------------------------------------------------
293
+ # 4) Reconcile computed totals vs declared totals
294
+ # (Assumes 2 lines per order: index 0 and 1 — on purpose, to keep the example finite.)
295
+ # ------------------------------------------------------------------
296
+
297
+ {
298
+ ?Order a ex:Order .
299
+
300
+ ?Order ex:line ?L0 . ?L0 ex:lineIndex 0 ; ex:netCents ?N0 ; ex:vatCents ?V0 ; ex:grossCents ?G0 .
301
+ ?Order ex:line ?L1 . ?L1 ex:lineIndex 1 ; ex:netCents ?N1 ; ex:vatCents ?V1 ; ex:grossCents ?G1 .
302
+
303
+ (?N0 ?N1) math:sum ?Net .
304
+ (?V0 ?V1) math:sum ?Vat .
305
+ (?G0 ?G1) math:sum ?Gross .
306
+ } => {
307
+ ?Order ex:computedNetCents ?Net ;
308
+ ex:computedVatCents ?Vat ;
309
+ ex:computedGrossCents ?Gross .
310
+ } .
311
+
312
+ # Mark OK orders
313
+ {
314
+ ?Order a ex:Order ;
315
+ ex:declaredNetCents ?DN ; ex:declaredVatCents ?DV ; ex:declaredGrossCents ?DG ;
316
+ ex:computedNetCents ?CN ; ex:computedVatCents ?CV ; ex:computedGrossCents ?CG .
317
+
318
+ ?DN math:equalTo ?CN .
319
+ ?DV math:equalTo ?CV .
320
+ ?DG math:equalTo ?CG .
321
+ } => {
322
+ ?Order a ex:OrderOk .
323
+ } .
324
+
325
+ # Emit issues for VAT mismatch
326
+ {
327
+ ?Order a ex:Order ; ex:orderId ?Oid ; ex:declaredVatCents ?DV ; ex:computedVatCents ?CV .
328
+ ?DV math:notEqualTo ?CV .
329
+
330
+ ("urn:example:issue:%s:vat" ?Oid) string:format ?IssueUriStr .
331
+ ?Issue log:uri ?IssueUriStr .
332
+ } => {
333
+ ?Issue a ex:Issue ;
334
+ ex:onOrder ?Order ;
335
+ ex:field "vatCents" ;
336
+ ex:declared ?DV ;
337
+ ex:computed ?CV .
338
+ ?Order ex:hasIssue ?Issue .
339
+ } .
340
+
341
+ # Emit issues for GROSS mismatch
342
+ {
343
+ ?Order a ex:Order ; ex:orderId ?Oid ; ex:declaredGrossCents ?DG ; ex:computedGrossCents ?CG .
344
+ ?DG math:notEqualTo ?CG .
345
+
346
+ ("urn:example:issue:%s:gross" ?Oid) string:format ?IssueUriStr .
347
+ ?Issue log:uri ?IssueUriStr .
348
+ } => {
349
+ ?Issue a ex:Issue ;
350
+ ex:onOrder ?Order ;
351
+ ex:field "grossCents" ;
352
+ ex:declared ?DG ;
353
+ ex:computed ?CG .
354
+ ?Order ex:hasIssue ?Issue .
355
+ } .
356
+
@@ -4,10 +4,10 @@
4
4
  # Proof for derived triple:
5
5
  # :test :is true .
6
6
  # It holds because the following instance of the rule body is provable:
7
- # :patH :ageAbove "P80Y"^^<http://www.w3.org/2001/XMLSchema#duration> .
7
+ # :patH :ageAbove "P80Y"^^xsd:duration .
8
8
  # via the schematic forward rule:
9
9
  # {
10
- # ?S :ageAbove "P80Y"^^<http://www.w3.org/2001/XMLSchema#duration> .
10
+ # ?S :ageAbove "P80Y"^^xsd:duration .
11
11
  # } => {
12
12
  # :test :is true .
13
13
  # } .