eyeling 1.10.20 → 1.10.21

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.
Files changed (45) hide show
  1. package/HANDBOOK.md +88 -2
  2. package/examples/bind-builtins.n3 +11 -0
  3. package/examples/bind.n3 +7 -0
  4. package/examples/brussels-brew-club.n3 +119 -0
  5. package/examples/builtins-string-math.n3 +11 -0
  6. package/examples/builtins-triple-termtests.n3 +7 -0
  7. package/examples/family.n3 +10 -0
  8. package/examples/filter-demorgan.n3 +9 -0
  9. package/examples/filter-in-notin.n3 +10 -0
  10. package/examples/filter-nested-or.n3 +10 -0
  11. package/examples/filter.n3 +8 -0
  12. package/examples/input/bind-builtins.srl +30 -0
  13. package/examples/input/bind.srl +12 -0
  14. package/examples/input/builtins-string-math.srl +38 -0
  15. package/examples/input/builtins-triple-termtests.srl +27 -0
  16. package/examples/input/family.srl +12 -0
  17. package/examples/input/filter-demorgan.srl +15 -0
  18. package/examples/input/filter-in-notin.srl +15 -0
  19. package/examples/input/filter-nested-or.srl +15 -0
  20. package/examples/input/filter.srl +9 -0
  21. package/examples/input/snaf.srl +6 -0
  22. package/examples/json-pointer.n3 +75 -0
  23. package/examples/json-reconcile-vat.n3 +361 -0
  24. package/examples/output/bind-builtins.n3 +9 -0
  25. package/examples/output/bind.n3 +3 -0
  26. package/examples/output/brussels-brew-club.n3 +22 -0
  27. package/examples/output/builtins-string-math.n3 +0 -0
  28. package/examples/output/builtins-triple-termtests.n3 +0 -0
  29. package/examples/output/family.n3 +13 -0
  30. package/examples/output/filter-demorgan.n3 +3 -0
  31. package/examples/output/filter-in-notin.n3 +4 -0
  32. package/examples/output/filter-nested-or.n3 +4 -0
  33. package/examples/output/filter.n3 +3 -0
  34. package/examples/output/json-pointer.n3 +13 -0
  35. package/examples/output/json-reconcile-vat.n3 +226 -0
  36. package/examples/output/snaf.n3 +3 -0
  37. package/examples/snaf.n3 +6 -0
  38. package/eyeling-builtins.ttl +48 -0
  39. package/eyeling.js +312 -1
  40. package/lib/engine.js +307 -1
  41. package/lib/rules.js +5 -0
  42. package/package.json +1 -1
  43. package/test/n3gen.test.js +4 -4
  44. package/test/package.test.js +1 -1
  45. package/tools/n3gen.js +1883 -6
package/lib/engine.js CHANGED
@@ -129,6 +129,37 @@ function __makeSkolemRunSalt() {
129
129
  );
130
130
  }
131
131
 
132
+ function __randomUuidV4() {
133
+ // Best-effort UUID v4 generator (Node + browsers). Used by log:uuid / log:struuid.
134
+ try {
135
+ if (typeof globalThis !== 'undefined' && globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') {
136
+ return globalThis.crypto.randomUUID();
137
+ }
138
+ } catch {}
139
+
140
+ try {
141
+ if (nodeCrypto && typeof nodeCrypto.randomUUID === 'function') return nodeCrypto.randomUUID();
142
+ } catch {}
143
+
144
+ // Fallback: v4 using random bytes (Node) or Math.random
145
+ let bytes = null;
146
+ try {
147
+ if (nodeCrypto && typeof nodeCrypto.randomBytes === 'function') bytes = nodeCrypto.randomBytes(16);
148
+ } catch {}
149
+ if (!bytes) {
150
+ bytes = new Uint8Array(16);
151
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
152
+ }
153
+
154
+ // Set version (4) and variant (RFC4122)
155
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
156
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
157
+
158
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
159
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
160
+ }
161
+
162
+
132
163
  function __enterReasoningRun() {
133
164
  __skolemRunDepth += 1;
134
165
  if (__skolemRunDepth === 1) {
@@ -175,6 +206,8 @@ const __parseNumCache = new Map(); // lit string -> number|null
175
206
  const __parseIntCache = new Map(); // lit string -> bigint|null
176
207
  const __parseNumericInfoCache = new Map(); // lit string -> info|null
177
208
 
209
+ // Cache for string:jsonPointer: jsonText -> { parsed: any|null, ptrCache: Map<string, Term|null> }
210
+ const jsonPointerCache = new Map();
178
211
 
179
212
  // -----------------------------------------------------------------------------
180
213
  // log:conclusion cache
@@ -1305,6 +1338,112 @@ function termToJsStringDecoded(t) {
1305
1338
  return stripQuotes(lex);
1306
1339
  }
1307
1340
 
1341
+ function jsonPointerUnescape(seg) {
1342
+ // RFC6901: ~1 -> '/', ~0 -> '~'
1343
+ let out = '';
1344
+ for (let i = 0; i < seg.length; i++) {
1345
+ const c = seg[i];
1346
+ if (c !== '~') {
1347
+ out += c;
1348
+ continue;
1349
+ }
1350
+ if (i + 1 >= seg.length) return null;
1351
+ const n = seg[i + 1];
1352
+ if (n === '0') out += '~';
1353
+ else if (n === '1') out += '/';
1354
+ else return null;
1355
+ i++;
1356
+ }
1357
+ return out;
1358
+ }
1359
+
1360
+ function jsonToTerm(v) {
1361
+ if (v === null) return makeStringLiteral('null');
1362
+ if (typeof v === 'string') return makeStringLiteral(v);
1363
+ if (typeof v === 'number') return internLiteral(String(v));
1364
+ if (typeof v === 'boolean') return internLiteral(v ? 'true' : 'false');
1365
+ if (Array.isArray(v)) return new ListTerm(v.map(jsonToTerm));
1366
+
1367
+ if (v && typeof v === 'object') {
1368
+ return makeRdfJsonLiteral(JSON.stringify(v));
1369
+ }
1370
+ return null;
1371
+ }
1372
+
1373
+ function jsonPointerLookup(jsonText, pointer) {
1374
+ let ptr = pointer;
1375
+
1376
+ // Support URI fragment form "#/a/b"
1377
+ if (ptr.startsWith('#')) {
1378
+ try {
1379
+ ptr = decodeURIComponent(ptr.slice(1));
1380
+ } catch {
1381
+ return null;
1382
+ }
1383
+ }
1384
+
1385
+ let entry = jsonPointerCache.get(jsonText);
1386
+ if (!entry) {
1387
+ let parsed = null;
1388
+ try {
1389
+ parsed = JSON.parse(jsonText);
1390
+ } catch {
1391
+ parsed = null;
1392
+ }
1393
+ entry = { parsed, ptrCache: new Map() };
1394
+ jsonPointerCache.set(jsonText, entry);
1395
+ }
1396
+ if (entry.parsed === null) return null;
1397
+
1398
+ if (entry.ptrCache.has(ptr)) return entry.ptrCache.get(ptr);
1399
+
1400
+ let cur = entry.parsed;
1401
+
1402
+ if (ptr === '') {
1403
+ const t = jsonToTerm(cur);
1404
+ entry.ptrCache.set(ptr, t);
1405
+ return t;
1406
+ }
1407
+ if (!ptr.startsWith('/')) {
1408
+ entry.ptrCache.set(ptr, null);
1409
+ return null;
1410
+ }
1411
+
1412
+ const parts = ptr.split('/').slice(1);
1413
+ for (const raw of parts) {
1414
+ const seg = jsonPointerUnescape(raw);
1415
+ if (seg === null) {
1416
+ entry.ptrCache.set(ptr, null);
1417
+ return null;
1418
+ }
1419
+
1420
+ if (Array.isArray(cur)) {
1421
+ if (!/^(0|[1-9]\d*)$/.test(seg)) {
1422
+ entry.ptrCache.set(ptr, null);
1423
+ return null;
1424
+ }
1425
+ const idx = Number(seg);
1426
+ if (idx < 0 || idx >= cur.length) {
1427
+ entry.ptrCache.set(ptr, null);
1428
+ return null;
1429
+ }
1430
+ cur = cur[idx];
1431
+ } else if (cur !== null && typeof cur === 'object') {
1432
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
1433
+ entry.ptrCache.set(ptr, null);
1434
+ return null;
1435
+ }
1436
+ cur = cur[seg];
1437
+ } else {
1438
+ entry.ptrCache.set(ptr, null);
1439
+ return null;
1440
+ }
1441
+ }
1442
+
1443
+ const out = jsonToTerm(cur);
1444
+ entry.ptrCache.set(ptr, out);
1445
+ return out;
1446
+ }
1308
1447
 
1309
1448
  // Tiny subset of sprintf: supports only %s and %%.
1310
1449
  // Good enough for most N3 string:format use cases that just splice strings.
@@ -2423,7 +2562,9 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2423
2562
  ? 'sha256'
2424
2563
  : pv === CRYPTO_NS + 'sha512'
2425
2564
  ? 'sha512'
2426
- : null;
2565
+ : pv === CRYPTO_NS + 'sha384'
2566
+ ? 'sha384'
2567
+ : null;
2427
2568
  if (cryptoAlgo) return evalCryptoHashBuiltin(g, subst, cryptoAlgo);
2428
2569
 
2429
2570
  // -----------------------------------------------------------------
@@ -2916,6 +3057,38 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2916
3057
  s2[g.o.name] = lit;
2917
3058
  return [s2];
2918
3059
  }
3060
+
3061
+ // math:ceiling (Eyeling extension)
3062
+ // Schema: $s+ math:ceiling $o-
3063
+ // Smallest integer >= s (fails on NaN / non-numeric).
3064
+ if (pv === MATH_NS + 'ceiling') {
3065
+ const info = parseNumericLiteralInfo(g.s);
3066
+ if (!info) return [];
3067
+ if (typeof info.value !== 'number') {
3068
+ // BigInt (xsd:integer) – already integral
3069
+ const lit = internLiteral(String(info.value));
3070
+ return unifyTermMaybe(g.o, lit, subst);
3071
+ }
3072
+ if (Number.isNaN(info.value) || !Number.isFinite(info.value)) return [];
3073
+ const lit = internLiteral(String(Math.ceil(info.value)));
3074
+ return unifyTermMaybe(g.o, lit, subst);
3075
+ }
3076
+
3077
+ // math:floor (Eyeling extension)
3078
+ // Schema: $s+ math:floor $o-
3079
+ // Largest integer <= s (fails on NaN / non-numeric).
3080
+ if (pv === MATH_NS + 'floor') {
3081
+ const info = parseNumericLiteralInfo(g.s);
3082
+ if (!info) return [];
3083
+ if (typeof info.value !== 'number') {
3084
+ const lit = internLiteral(String(info.value));
3085
+ return unifyTermMaybe(g.o, lit, subst);
3086
+ }
3087
+ if (Number.isNaN(info.value) || !Number.isFinite(info.value)) return [];
3088
+ const lit = internLiteral(String(Math.floor(info.value)));
3089
+ return unifyTermMaybe(g.o, lit, subst);
3090
+ }
3091
+
2919
3092
  if (g.o instanceof Blank) return [{ ...subst }];
2920
3093
 
2921
3094
  // Accept typed numeric literals too (e.g., "3"^^xsd:float) if numerically equal.
@@ -3712,6 +3885,44 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3712
3885
  s2[g.o.name] = ty;
3713
3886
  return [s2];
3714
3887
  }
3888
+
3889
+ // log:isIRI / log:isLiteral / log:isBlank / log:isNumeric / log:isTriple (Eyeling extensions)
3890
+ // Schema: $s+ log:isIRI $o? (succeeds when s matches; o may be 'true' or a variable)
3891
+ function unifyBoolTrue(obj, subst0) {
3892
+ if (obj instanceof Blank) return [subst0];
3893
+ const tTrue = internLiteral('true');
3894
+ const s2 = unifyTermMaybe(obj, tTrue, subst0);
3895
+ return s2 ? [s2] : [];
3896
+ }
3897
+
3898
+ if (pv === LOG_NS + 'isIRI') {
3899
+ if (!(g.s instanceof Iri)) return [];
3900
+ return unifyBoolTrue(g.o, subst);
3901
+ }
3902
+
3903
+ if (pv === LOG_NS + 'isLiteral') {
3904
+ if (!(g.s instanceof Literal)) return [];
3905
+ return unifyBoolTrue(g.o, subst);
3906
+ }
3907
+
3908
+ if (pv === LOG_NS + 'isBlank') {
3909
+ if (!(g.s instanceof Blank)) return [];
3910
+ return unifyBoolTrue(g.o, subst);
3911
+ }
3912
+
3913
+ if (pv === LOG_NS + 'isNumeric') {
3914
+ if (!(g.s instanceof Literal)) return [];
3915
+ const dt = numericDatatypeOfTerm(g.s);
3916
+ if (!dt) return [];
3917
+ return unifyBoolTrue(g.o, subst);
3918
+ }
3919
+
3920
+ if (pv === LOG_NS + 'isTriple') {
3921
+ if (!(g.s instanceof GraphTerm)) return [];
3922
+ if (g.s.triples.length !== 1) return [];
3923
+ return unifyBoolTrue(g.o, subst);
3924
+ }
3925
+
3715
3926
  if (g.o instanceof Blank) return [{ ...subst }];
3716
3927
 
3717
3928
  const s2 = unifyTerm(g.o, ty, subst);
@@ -4190,6 +4401,24 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4190
4401
  skolemCache.set(key, iri);
4191
4402
  }
4192
4403
 
4404
+ // log:uuid / log:struuid (Eyeling extensions)
4405
+ // NOTE: these generate fresh values and can affect termination; prefer log:skolem for deterministic IDs.
4406
+ //
4407
+ // log:uuid: $s? log:uuid $o- -> binds $o to a fresh <urn:uuid:...> IRI
4408
+ // log:struuid: $s? log:struuid $o- -> binds $o to a fresh UUID string literal
4409
+ if (pv === LOG_NS + 'uuid') {
4410
+ const uuid = __randomUuidV4();
4411
+ const iri = internIri('urn:uuid:' + uuid);
4412
+ return unifyTermMaybe(g.o, iri, subst);
4413
+ }
4414
+
4415
+ if (pv === LOG_NS + 'struuid') {
4416
+ const uuid = __randomUuidV4();
4417
+ const lit = makeStringLiteral(uuid);
4418
+ return unifyTermMaybe(g.o, lit, subst);
4419
+ }
4420
+
4421
+
4193
4422
  const s2 = unifyTerm(goal.o, iri, subst);
4194
4423
  return s2 !== null ? [s2] : [];
4195
4424
  }
@@ -4297,6 +4526,68 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4297
4526
  args.push(aStr);
4298
4527
  }
4299
4528
 
4529
+ // string:length (Eyeling extension)
4530
+ // Schema: $s+ string:length $o-
4531
+ if (pv === STRING_NS + 'length') {
4532
+ const s0 = termToJsStringDecoded(g.s);
4533
+ if (s0 === null) return [];
4534
+ const lit = internLiteral(String(s0.length));
4535
+ return unifyTermMaybe(g.o, lit, subst);
4536
+ }
4537
+
4538
+ // string:upperCase / string:lowerCase (Eyeling extensions)
4539
+ // Schema: $s+ string:upperCase $o- ; $s+ string:lowerCase $o-
4540
+ if (pv === STRING_NS + 'upperCase' || pv === STRING_NS + 'lowerCase') {
4541
+ const s0 = termToJsStringDecoded(g.s);
4542
+ if (s0 === null) return [];
4543
+ const out = pv.endsWith('upperCase') ? s0.toUpperCase() : s0.toLowerCase();
4544
+ const lit = makeStringLiteral(out);
4545
+ return unifyTermMaybe(g.o, lit, subst);
4546
+ }
4547
+
4548
+ // string:encodeForURI (Eyeling extension)
4549
+ // Schema: $s+ string:encodeForURI $o-
4550
+ if (pv === STRING_NS + 'encodeForURI') {
4551
+ const s0 = termToJsStringDecoded(g.s);
4552
+ if (s0 === null) return [];
4553
+ // SPARQL-like: start with encodeURIComponent and also escape [!'()*]
4554
+ const enc = encodeURIComponent(s0).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
4555
+ const lit = makeStringLiteral(enc);
4556
+ return unifyTermMaybe(g.o, lit, subst);
4557
+ }
4558
+
4559
+ // string:substring (Eyeling extension)
4560
+ // Schema: ( $s+ $start+ [$len+] ) string:substring $o-
4561
+ // NOTE: start is 1-based (SPARQL SUBSTR), len is optional.
4562
+ if (pv === STRING_NS + 'substring') {
4563
+ if (!(g.s instanceof ListTerm)) return [];
4564
+ const items = g.s.items;
4565
+ if (items.length !== 2 && items.length !== 3) return [];
4566
+ const s0 = termToJsStringDecoded(items[0]);
4567
+ if (s0 === null) return [];
4568
+ const startInfo = parseNumericLiteralInfo(items[1]);
4569
+ if (!startInfo || typeof startInfo.value !== 'number' || Number.isNaN(startInfo.value)) return [];
4570
+ let start = Math.floor(startInfo.value);
4571
+ if (!Number.isFinite(start)) return [];
4572
+ start = Math.max(1, start);
4573
+
4574
+ let outStr = '';
4575
+ if (items.length === 2) {
4576
+ outStr = s0.slice(start - 1);
4577
+ } else {
4578
+ const lenInfo = parseNumericLiteralInfo(items[2]);
4579
+ if (!lenInfo || typeof lenInfo.value !== 'number' || Number.isNaN(lenInfo.value)) return [];
4580
+ let len = Math.floor(lenInfo.value);
4581
+ if (!Number.isFinite(len)) return [];
4582
+ if (len <= 0) outStr = '';
4583
+ else outStr = s0.slice(start - 1, start - 1 + len);
4584
+ }
4585
+
4586
+ const lit = makeStringLiteral(outStr);
4587
+ return unifyTermMaybe(g.o, lit, subst);
4588
+ }
4589
+
4590
+
4300
4591
  const formatted = simpleStringFormat(fmtStr, args);
4301
4592
  if (formatted === null) return []; // unsupported format specifier(s)
4302
4593
 
@@ -4310,6 +4601,21 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4310
4601
  return s2 !== null ? [s2] : [];
4311
4602
  }
4312
4603
 
4604
+ // string:jsonPointer
4605
+ // Schema: ( $jsonText $pointer ) string:jsonPointer $value
4606
+ if (pv === STRING_NS + 'jsonPointer') {
4607
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
4608
+
4609
+ const jsonText = termToJsonText(g.s.elems[0]);
4610
+ const ptr = termToJsStringDecoded(g.s.elems[1]);
4611
+ if (jsonText === null || ptr === null) return [];
4612
+
4613
+ const valTerm = jsonPointerLookup(jsonText, ptr);
4614
+ if (valTerm === null) return [];
4615
+
4616
+ const s2 = unifyTerm(g.o, valTerm, subst);
4617
+ return s2 !== null ? [s2] : [];
4618
+ }
4313
4619
 
4314
4620
  // string:greaterThan
4315
4621
  if (pv === STRING_NS + 'greaterThan') {
package/lib/rules.js CHANGED
@@ -94,6 +94,11 @@ function isConstraintBuiltin(tr) {
94
94
  v === LOG_NS + 'forAllIn' ||
95
95
  v === LOG_NS + 'notEqualTo' ||
96
96
  v === LOG_NS + 'notIncludes' ||
97
+ v === LOG_NS + 'isIRI' ||
98
+ v === LOG_NS + 'isLiteral' ||
99
+ v === LOG_NS + 'isBlank' ||
100
+ v === LOG_NS + 'isNumeric' ||
101
+ v === LOG_NS + 'isTriple' ||
97
102
  v === LOG_NS + 'outputString'
98
103
  ) {
99
104
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.10.20",
3
+ "version": "1.10.21",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // Convert examples/input/*.{ttl,trig} -> examples/*.n3 using n3gen.js
4
+ // Convert examples/input/*.{ttl,trig,srl} -> examples/*.n3 using n3gen.js
5
5
  // Designed to work both in a git checkout (maintainer mode) and in an npm-installed package.
6
6
  //
7
7
  // In git mode:
@@ -109,14 +109,14 @@ function main() {
109
109
  const IN_GIT = inGitWorktree(root);
110
110
 
111
111
  const inputs = fs.readdirSync(inputDir)
112
- .filter(f => /\.(ttl|trig)$/i.test(f))
112
+ .filter(f => /\.(ttl|trig|srl)$/i.test(f))
113
113
  .sort((a, b) => a.localeCompare(b));
114
114
 
115
115
  info(`Running n3 conversions for ${inputs.length} inputs (${IN_GIT ? 'git worktree mode' : 'npm-installed mode'})`);
116
116
  console.log(`${C.dim}node ${process.version}${C.n}`);
117
117
 
118
118
  if (inputs.length === 0) {
119
- ok('No .ttl/.trig files found in examples/input/');
119
+ ok('No .ttl/.trig/.srl files found in examples/input/');
120
120
  process.exit(0);
121
121
  }
122
122
 
@@ -129,7 +129,7 @@ function main() {
129
129
  const start = Date.now();
130
130
 
131
131
  const inPath = path.join(inputDir, inFile);
132
- const base = inFile.replace(/\.(ttl|trig)$/i, '');
132
+ const base = inFile.replace(/\.(ttl|trig|srl)$/i, '');
133
133
  const outFile = `${base}.n3`;
134
134
 
135
135
  const expectedPath = path.join(examplesDir, outFile);
@@ -178,7 +178,7 @@ const SMOKE_EXAMPLES = [
178
178
  'age.n3',
179
179
  'basic-monadic.n3',
180
180
  'collection.n3',
181
- 'family-cousins.n3',
181
+ 'family.n3',
182
182
  'backward.n3',
183
183
  ];
184
184