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
@@ -0,0 +1,3 @@
1
+ @prefix : <http://example.org/#> .
2
+
3
+ :Alice :hates :Nobody .
@@ -0,0 +1,6 @@
1
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
2
+ @prefix : <http://example.org/#> .
3
+
4
+ :Alice :loves :Bob. :Bob a :Person.
5
+
6
+ { ?SCOPE log:notIncludes { :Alice :hates ?X . } . ?X a :Person . } => { :Alice :hates :Nobody. } .
@@ -55,6 +55,9 @@ crypto:md5 a ex:Builtin ; ex:kind ex:Function ;
55
55
  crypto:sha256 a ex:Builtin ; ex:kind ex:Function ;
56
56
  rdfs:comment "Hash builtin (SHA-256). Computes digest over the subject (string-ish) and unifies/binds the object with the digest." .
57
57
 
58
+ crypto:sha384 a ex:Builtin ; ex:kind ex:Function ;
59
+ rdfs:comment "Hash builtin (SHA-384). Computes digest over the subject (string-ish) and unifies/binds the object with the digest. (Eyeling extension; useful for SHACL/Sparql-style mappings.)" .
60
+
58
61
  crypto:sha512 a ex:Builtin ; ex:kind ex:Function ;
59
62
  rdfs:comment "Hash builtin (SHA-512). Computes digest over the subject (string-ish) and unifies/binds the object with the digest." .
60
63
 
@@ -101,6 +104,12 @@ math:remainder a ex:Builtin ; ex:kind ex:Function ;
101
104
  math:rounded a ex:Builtin ; ex:kind ex:Function ;
102
105
  rdfs:comment "Rounds subject to nearest integer (JS tie-breaking: toward +∞). Binds/unifies object with the rounded integer value." .
103
106
 
107
+ math:ceiling a ex:Builtin ; ex:kind ex:Function ;
108
+ rdfs:comment "Ceiling of a numeric value (like SPARQL CEIL). Returns an integer token. (Eyeling extension.)" .
109
+
110
+ math:floor a ex:Builtin ; ex:kind ex:Function ;
111
+ rdfs:comment "Floor of a numeric value (like SPARQL FLOOR). Returns an integer token. (Eyeling extension.)" .
112
+
104
113
  math:exponentiation a ex:Builtin ; ex:kind ex:Function ;
105
114
  rdfs:comment "Exponentiation. Forward: (base exponent) -> result. Limited inverse: if base is numeric and exponent is a variable, may solve exponent via logs for positive base != 1 and positive result." .
106
115
 
@@ -248,6 +257,27 @@ log:parsedAsN3 a ex:Builtin ; ex:kind ex:Meta ;
248
257
  log:rawType a ex:Builtin ; ex:kind ex:Meta ;
249
258
  rdfs:comment "Returns one of log:Formula, log:Literal, rdf:List, or log:Other for the subject term." .
250
259
 
260
+ log:isIRI a ex:Builtin ; ex:kind ex:Test ; ex:isConstraint true ;
261
+ rdfs:comment "Succeeds iff the subject is an IRI term (like SPARQL isIRI). Object is typically 'true'. (Eyeling extension.)" .
262
+
263
+ log:isLiteral a ex:Builtin ; ex:kind ex:Test ; ex:isConstraint true ;
264
+ rdfs:comment "Succeeds iff the subject is a literal term (like SPARQL isLITERAL). (Eyeling extension.)" .
265
+
266
+ log:isBlank a ex:Builtin ; ex:kind ex:Test ; ex:isConstraint true ;
267
+ rdfs:comment "Succeeds iff the subject is a blank node term (like SPARQL isBLANK). (Eyeling extension.)" .
268
+
269
+ log:isNumeric a ex:Builtin ; ex:kind ex:Test ; ex:isConstraint true ;
270
+ rdfs:comment "Succeeds iff the subject is a numeric literal (like SPARQL isNUMERIC). (Eyeling extension.)" .
271
+
272
+ log:isTriple a ex:Builtin ; ex:kind ex:Test ; ex:isConstraint true ;
273
+ rdfs:comment "Succeeds iff the subject is a formula with exactly one triple (like SPARQL isTRIPLE over RDF-star triple terms). (Eyeling extension.)" .
274
+
275
+ log:uuid a ex:Builtin ; ex:kind ex:Function ;
276
+ rdfs:comment "Generates a fresh UUID IRI (<urn:uuid:...>). Non-deterministic; can affect termination. (Eyeling extension.)" .
277
+
278
+ log:struuid a ex:Builtin ; ex:kind ex:Function ;
279
+ rdfs:comment "Generates a fresh UUID string literal. Non-deterministic; can affect termination. (Eyeling extension.)" .
280
+
251
281
  log:dtlit a ex:Builtin ; ex:kind ex:Function ;
252
282
  rdfs:comment "Builds a datatype literal from (lex datatypeIri). Binds/unifies object with the resulting Literal." .
253
283
 
@@ -334,3 +364,21 @@ string:scrape a ex:Builtin ; ex:kind ex:Function ;
334
364
  string:format a ex:Builtin ; ex:kind ex:Function ;
335
365
  rdfs:comment "Simple formatter: (fmt arg1 ... argN) -> output string. Only %s and %% are supported; other specifiers fail." .
336
366
 
367
+ string:length a ex:Builtin ; ex:kind ex:Function ;
368
+ rdfs:comment "Length of a string (like SPARQL STRLEN). (Eyeling extension.)" .
369
+
370
+ string:substring a ex:Builtin ; ex:kind ex:Function ;
371
+ rdfs:comment "Substring: subject must be a list (string start [len]); start is 1-based (like SPARQL SUBSTR). (Eyeling extension.)" .
372
+
373
+ string:upperCase a ex:Builtin ; ex:kind ex:Function ;
374
+ rdfs:comment "Uppercase conversion (like SPARQL UCASE). (Eyeling extension.)" .
375
+
376
+ string:lowerCase a ex:Builtin ; ex:kind ex:Function ;
377
+ rdfs:comment "Lowercase conversion (like SPARQL LCASE). (Eyeling extension.)" .
378
+
379
+ string:encodeForURI a ex:Builtin ; ex:kind ex:Function ;
380
+ rdfs:comment "Percent-encode a string (like SPARQL ENCODE_FOR_URI). (Eyeling extension.)" .
381
+
382
+ string:jsonPointer a ex:Builtin ; ex:kind ex:Function ;
383
+ rdfs:comment "JSON Pointer lookup: (jsonText pointer) -> value term. Expects rdf:JSON-typed literal (or treated as rdf:JSON); caches parsed JSON and pointer lookups." .
384
+
package/eyeling.js CHANGED
@@ -814,6 +814,37 @@ function __makeSkolemRunSalt() {
814
814
  );
815
815
  }
816
816
 
817
+ function __randomUuidV4() {
818
+ // Best-effort UUID v4 generator (Node + browsers). Used by log:uuid / log:struuid.
819
+ try {
820
+ if (typeof globalThis !== 'undefined' && globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') {
821
+ return globalThis.crypto.randomUUID();
822
+ }
823
+ } catch {}
824
+
825
+ try {
826
+ if (nodeCrypto && typeof nodeCrypto.randomUUID === 'function') return nodeCrypto.randomUUID();
827
+ } catch {}
828
+
829
+ // Fallback: v4 using random bytes (Node) or Math.random
830
+ let bytes = null;
831
+ try {
832
+ if (nodeCrypto && typeof nodeCrypto.randomBytes === 'function') bytes = nodeCrypto.randomBytes(16);
833
+ } catch {}
834
+ if (!bytes) {
835
+ bytes = new Uint8Array(16);
836
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
837
+ }
838
+
839
+ // Set version (4) and variant (RFC4122)
840
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
841
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
842
+
843
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
844
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
845
+ }
846
+
847
+
817
848
  function __enterReasoningRun() {
818
849
  __skolemRunDepth += 1;
819
850
  if (__skolemRunDepth === 1) {
@@ -860,6 +891,8 @@ const __parseNumCache = new Map(); // lit string -> number|null
860
891
  const __parseIntCache = new Map(); // lit string -> bigint|null
861
892
  const __parseNumericInfoCache = new Map(); // lit string -> info|null
862
893
 
894
+ // Cache for string:jsonPointer: jsonText -> { parsed: any|null, ptrCache: Map<string, Term|null> }
895
+ const jsonPointerCache = new Map();
863
896
 
864
897
  // -----------------------------------------------------------------------------
865
898
  // log:conclusion cache
@@ -1990,6 +2023,112 @@ function termToJsStringDecoded(t) {
1990
2023
  return stripQuotes(lex);
1991
2024
  }
1992
2025
 
2026
+ function jsonPointerUnescape(seg) {
2027
+ // RFC6901: ~1 -> '/', ~0 -> '~'
2028
+ let out = '';
2029
+ for (let i = 0; i < seg.length; i++) {
2030
+ const c = seg[i];
2031
+ if (c !== '~') {
2032
+ out += c;
2033
+ continue;
2034
+ }
2035
+ if (i + 1 >= seg.length) return null;
2036
+ const n = seg[i + 1];
2037
+ if (n === '0') out += '~';
2038
+ else if (n === '1') out += '/';
2039
+ else return null;
2040
+ i++;
2041
+ }
2042
+ return out;
2043
+ }
2044
+
2045
+ function jsonToTerm(v) {
2046
+ if (v === null) return makeStringLiteral('null');
2047
+ if (typeof v === 'string') return makeStringLiteral(v);
2048
+ if (typeof v === 'number') return internLiteral(String(v));
2049
+ if (typeof v === 'boolean') return internLiteral(v ? 'true' : 'false');
2050
+ if (Array.isArray(v)) return new ListTerm(v.map(jsonToTerm));
2051
+
2052
+ if (v && typeof v === 'object') {
2053
+ return makeRdfJsonLiteral(JSON.stringify(v));
2054
+ }
2055
+ return null;
2056
+ }
2057
+
2058
+ function jsonPointerLookup(jsonText, pointer) {
2059
+ let ptr = pointer;
2060
+
2061
+ // Support URI fragment form "#/a/b"
2062
+ if (ptr.startsWith('#')) {
2063
+ try {
2064
+ ptr = decodeURIComponent(ptr.slice(1));
2065
+ } catch {
2066
+ return null;
2067
+ }
2068
+ }
2069
+
2070
+ let entry = jsonPointerCache.get(jsonText);
2071
+ if (!entry) {
2072
+ let parsed = null;
2073
+ try {
2074
+ parsed = JSON.parse(jsonText);
2075
+ } catch {
2076
+ parsed = null;
2077
+ }
2078
+ entry = { parsed, ptrCache: new Map() };
2079
+ jsonPointerCache.set(jsonText, entry);
2080
+ }
2081
+ if (entry.parsed === null) return null;
2082
+
2083
+ if (entry.ptrCache.has(ptr)) return entry.ptrCache.get(ptr);
2084
+
2085
+ let cur = entry.parsed;
2086
+
2087
+ if (ptr === '') {
2088
+ const t = jsonToTerm(cur);
2089
+ entry.ptrCache.set(ptr, t);
2090
+ return t;
2091
+ }
2092
+ if (!ptr.startsWith('/')) {
2093
+ entry.ptrCache.set(ptr, null);
2094
+ return null;
2095
+ }
2096
+
2097
+ const parts = ptr.split('/').slice(1);
2098
+ for (const raw of parts) {
2099
+ const seg = jsonPointerUnescape(raw);
2100
+ if (seg === null) {
2101
+ entry.ptrCache.set(ptr, null);
2102
+ return null;
2103
+ }
2104
+
2105
+ if (Array.isArray(cur)) {
2106
+ if (!/^(0|[1-9]\d*)$/.test(seg)) {
2107
+ entry.ptrCache.set(ptr, null);
2108
+ return null;
2109
+ }
2110
+ const idx = Number(seg);
2111
+ if (idx < 0 || idx >= cur.length) {
2112
+ entry.ptrCache.set(ptr, null);
2113
+ return null;
2114
+ }
2115
+ cur = cur[idx];
2116
+ } else if (cur !== null && typeof cur === 'object') {
2117
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
2118
+ entry.ptrCache.set(ptr, null);
2119
+ return null;
2120
+ }
2121
+ cur = cur[seg];
2122
+ } else {
2123
+ entry.ptrCache.set(ptr, null);
2124
+ return null;
2125
+ }
2126
+ }
2127
+
2128
+ const out = jsonToTerm(cur);
2129
+ entry.ptrCache.set(ptr, out);
2130
+ return out;
2131
+ }
1993
2132
 
1994
2133
  // Tiny subset of sprintf: supports only %s and %%.
1995
2134
  // Good enough for most N3 string:format use cases that just splice strings.
@@ -3108,7 +3247,9 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3108
3247
  ? 'sha256'
3109
3248
  : pv === CRYPTO_NS + 'sha512'
3110
3249
  ? 'sha512'
3111
- : null;
3250
+ : pv === CRYPTO_NS + 'sha384'
3251
+ ? 'sha384'
3252
+ : null;
3112
3253
  if (cryptoAlgo) return evalCryptoHashBuiltin(g, subst, cryptoAlgo);
3113
3254
 
3114
3255
  // -----------------------------------------------------------------
@@ -3601,6 +3742,38 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3601
3742
  s2[g.o.name] = lit;
3602
3743
  return [s2];
3603
3744
  }
3745
+
3746
+ // math:ceiling (Eyeling extension)
3747
+ // Schema: $s+ math:ceiling $o-
3748
+ // Smallest integer >= s (fails on NaN / non-numeric).
3749
+ if (pv === MATH_NS + 'ceiling') {
3750
+ const info = parseNumericLiteralInfo(g.s);
3751
+ if (!info) return [];
3752
+ if (typeof info.value !== 'number') {
3753
+ // BigInt (xsd:integer) – already integral
3754
+ const lit = internLiteral(String(info.value));
3755
+ return unifyTermMaybe(g.o, lit, subst);
3756
+ }
3757
+ if (Number.isNaN(info.value) || !Number.isFinite(info.value)) return [];
3758
+ const lit = internLiteral(String(Math.ceil(info.value)));
3759
+ return unifyTermMaybe(g.o, lit, subst);
3760
+ }
3761
+
3762
+ // math:floor (Eyeling extension)
3763
+ // Schema: $s+ math:floor $o-
3764
+ // Largest integer <= s (fails on NaN / non-numeric).
3765
+ if (pv === MATH_NS + 'floor') {
3766
+ const info = parseNumericLiteralInfo(g.s);
3767
+ if (!info) return [];
3768
+ if (typeof info.value !== 'number') {
3769
+ const lit = internLiteral(String(info.value));
3770
+ return unifyTermMaybe(g.o, lit, subst);
3771
+ }
3772
+ if (Number.isNaN(info.value) || !Number.isFinite(info.value)) return [];
3773
+ const lit = internLiteral(String(Math.floor(info.value)));
3774
+ return unifyTermMaybe(g.o, lit, subst);
3775
+ }
3776
+
3604
3777
  if (g.o instanceof Blank) return [{ ...subst }];
3605
3778
 
3606
3779
  // Accept typed numeric literals too (e.g., "3"^^xsd:float) if numerically equal.
@@ -4397,6 +4570,44 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4397
4570
  s2[g.o.name] = ty;
4398
4571
  return [s2];
4399
4572
  }
4573
+
4574
+ // log:isIRI / log:isLiteral / log:isBlank / log:isNumeric / log:isTriple (Eyeling extensions)
4575
+ // Schema: $s+ log:isIRI $o? (succeeds when s matches; o may be 'true' or a variable)
4576
+ function unifyBoolTrue(obj, subst0) {
4577
+ if (obj instanceof Blank) return [subst0];
4578
+ const tTrue = internLiteral('true');
4579
+ const s2 = unifyTermMaybe(obj, tTrue, subst0);
4580
+ return s2 ? [s2] : [];
4581
+ }
4582
+
4583
+ if (pv === LOG_NS + 'isIRI') {
4584
+ if (!(g.s instanceof Iri)) return [];
4585
+ return unifyBoolTrue(g.o, subst);
4586
+ }
4587
+
4588
+ if (pv === LOG_NS + 'isLiteral') {
4589
+ if (!(g.s instanceof Literal)) return [];
4590
+ return unifyBoolTrue(g.o, subst);
4591
+ }
4592
+
4593
+ if (pv === LOG_NS + 'isBlank') {
4594
+ if (!(g.s instanceof Blank)) return [];
4595
+ return unifyBoolTrue(g.o, subst);
4596
+ }
4597
+
4598
+ if (pv === LOG_NS + 'isNumeric') {
4599
+ if (!(g.s instanceof Literal)) return [];
4600
+ const dt = numericDatatypeOfTerm(g.s);
4601
+ if (!dt) return [];
4602
+ return unifyBoolTrue(g.o, subst);
4603
+ }
4604
+
4605
+ if (pv === LOG_NS + 'isTriple') {
4606
+ if (!(g.s instanceof GraphTerm)) return [];
4607
+ if (g.s.triples.length !== 1) return [];
4608
+ return unifyBoolTrue(g.o, subst);
4609
+ }
4610
+
4400
4611
  if (g.o instanceof Blank) return [{ ...subst }];
4401
4612
 
4402
4613
  const s2 = unifyTerm(g.o, ty, subst);
@@ -4875,6 +5086,24 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4875
5086
  skolemCache.set(key, iri);
4876
5087
  }
4877
5088
 
5089
+ // log:uuid / log:struuid (Eyeling extensions)
5090
+ // NOTE: these generate fresh values and can affect termination; prefer log:skolem for deterministic IDs.
5091
+ //
5092
+ // log:uuid: $s? log:uuid $o- -> binds $o to a fresh <urn:uuid:...> IRI
5093
+ // log:struuid: $s? log:struuid $o- -> binds $o to a fresh UUID string literal
5094
+ if (pv === LOG_NS + 'uuid') {
5095
+ const uuid = __randomUuidV4();
5096
+ const iri = internIri('urn:uuid:' + uuid);
5097
+ return unifyTermMaybe(g.o, iri, subst);
5098
+ }
5099
+
5100
+ if (pv === LOG_NS + 'struuid') {
5101
+ const uuid = __randomUuidV4();
5102
+ const lit = makeStringLiteral(uuid);
5103
+ return unifyTermMaybe(g.o, lit, subst);
5104
+ }
5105
+
5106
+
4878
5107
  const s2 = unifyTerm(goal.o, iri, subst);
4879
5108
  return s2 !== null ? [s2] : [];
4880
5109
  }
@@ -4982,6 +5211,68 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4982
5211
  args.push(aStr);
4983
5212
  }
4984
5213
 
5214
+ // string:length (Eyeling extension)
5215
+ // Schema: $s+ string:length $o-
5216
+ if (pv === STRING_NS + 'length') {
5217
+ const s0 = termToJsStringDecoded(g.s);
5218
+ if (s0 === null) return [];
5219
+ const lit = internLiteral(String(s0.length));
5220
+ return unifyTermMaybe(g.o, lit, subst);
5221
+ }
5222
+
5223
+ // string:upperCase / string:lowerCase (Eyeling extensions)
5224
+ // Schema: $s+ string:upperCase $o- ; $s+ string:lowerCase $o-
5225
+ if (pv === STRING_NS + 'upperCase' || pv === STRING_NS + 'lowerCase') {
5226
+ const s0 = termToJsStringDecoded(g.s);
5227
+ if (s0 === null) return [];
5228
+ const out = pv.endsWith('upperCase') ? s0.toUpperCase() : s0.toLowerCase();
5229
+ const lit = makeStringLiteral(out);
5230
+ return unifyTermMaybe(g.o, lit, subst);
5231
+ }
5232
+
5233
+ // string:encodeForURI (Eyeling extension)
5234
+ // Schema: $s+ string:encodeForURI $o-
5235
+ if (pv === STRING_NS + 'encodeForURI') {
5236
+ const s0 = termToJsStringDecoded(g.s);
5237
+ if (s0 === null) return [];
5238
+ // SPARQL-like: start with encodeURIComponent and also escape [!'()*]
5239
+ const enc = encodeURIComponent(s0).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
5240
+ const lit = makeStringLiteral(enc);
5241
+ return unifyTermMaybe(g.o, lit, subst);
5242
+ }
5243
+
5244
+ // string:substring (Eyeling extension)
5245
+ // Schema: ( $s+ $start+ [$len+] ) string:substring $o-
5246
+ // NOTE: start is 1-based (SPARQL SUBSTR), len is optional.
5247
+ if (pv === STRING_NS + 'substring') {
5248
+ if (!(g.s instanceof ListTerm)) return [];
5249
+ const items = g.s.items;
5250
+ if (items.length !== 2 && items.length !== 3) return [];
5251
+ const s0 = termToJsStringDecoded(items[0]);
5252
+ if (s0 === null) return [];
5253
+ const startInfo = parseNumericLiteralInfo(items[1]);
5254
+ if (!startInfo || typeof startInfo.value !== 'number' || Number.isNaN(startInfo.value)) return [];
5255
+ let start = Math.floor(startInfo.value);
5256
+ if (!Number.isFinite(start)) return [];
5257
+ start = Math.max(1, start);
5258
+
5259
+ let outStr = '';
5260
+ if (items.length === 2) {
5261
+ outStr = s0.slice(start - 1);
5262
+ } else {
5263
+ const lenInfo = parseNumericLiteralInfo(items[2]);
5264
+ if (!lenInfo || typeof lenInfo.value !== 'number' || Number.isNaN(lenInfo.value)) return [];
5265
+ let len = Math.floor(lenInfo.value);
5266
+ if (!Number.isFinite(len)) return [];
5267
+ if (len <= 0) outStr = '';
5268
+ else outStr = s0.slice(start - 1, start - 1 + len);
5269
+ }
5270
+
5271
+ const lit = makeStringLiteral(outStr);
5272
+ return unifyTermMaybe(g.o, lit, subst);
5273
+ }
5274
+
5275
+
4985
5276
  const formatted = simpleStringFormat(fmtStr, args);
4986
5277
  if (formatted === null) return []; // unsupported format specifier(s)
4987
5278
 
@@ -4995,6 +5286,21 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4995
5286
  return s2 !== null ? [s2] : [];
4996
5287
  }
4997
5288
 
5289
+ // string:jsonPointer
5290
+ // Schema: ( $jsonText $pointer ) string:jsonPointer $value
5291
+ if (pv === STRING_NS + 'jsonPointer') {
5292
+ if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
5293
+
5294
+ const jsonText = termToJsonText(g.s.elems[0]);
5295
+ const ptr = termToJsStringDecoded(g.s.elems[1]);
5296
+ if (jsonText === null || ptr === null) return [];
5297
+
5298
+ const valTerm = jsonPointerLookup(jsonText, ptr);
5299
+ if (valTerm === null) return [];
5300
+
5301
+ const s2 = unifyTerm(g.o, valTerm, subst);
5302
+ return s2 !== null ? [s2] : [];
5303
+ }
4998
5304
 
4999
5305
  // string:greaterThan
5000
5306
  if (pv === STRING_NS + 'greaterThan') {
@@ -8084,6 +8390,11 @@ function isConstraintBuiltin(tr) {
8084
8390
  v === LOG_NS + 'forAllIn' ||
8085
8391
  v === LOG_NS + 'notEqualTo' ||
8086
8392
  v === LOG_NS + 'notIncludes' ||
8393
+ v === LOG_NS + 'isIRI' ||
8394
+ v === LOG_NS + 'isLiteral' ||
8395
+ v === LOG_NS + 'isBlank' ||
8396
+ v === LOG_NS + 'isNumeric' ||
8397
+ v === LOG_NS + 'isTriple' ||
8087
8398
  v === LOG_NS + 'outputString'
8088
8399
  ) {
8089
8400
  return true;