eyeling 1.15.9 → 1.15.10

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/HANDBOOK.md CHANGED
@@ -376,6 +376,10 @@ That’s alpha-equivalence:
376
376
 
377
377
  Eyeling implements alpha-equivalence by checking whether there exists a consistent renaming mapping between the two formulas’ variables/blanks that makes the triples match.
378
378
 
379
+ Important scope nuance: only blanks/variables that are local to the quoted formula participate in alpha-renaming. If a formula is being matched after an outer substitution has already instantiated part of it, those substituted terms are treated as fixed. In other words, alpha-equivalence may rename formula-local placeholders, but it must not rename names that came from the enclosing match. This prevents a substituted outer blank node from being confused with a local blank node inside the quoted formula.
380
+
381
+ So `{ _:x :p :o }` obtained by substituting `?A = _:x` into `{ ?A :p :o }` must not alpha-match `{ _:b :p :o }` by renaming `_:x` to `_:b`.
382
+
379
383
  ### 6.2 Groundness: “variables inside formulas don’t leak”
380
384
 
381
385
  Eyeling makes a deliberate choice about _groundness_:
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # eyeling
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/eyereasoner.svg)](https://www.npmjs.com/package/eyereasoner) [![DOI](https://zenodo.org/badge/581706557.svg)](https://doi.org/10.5281/zenodo.19068086)
4
+
3
5
  A compact [Notation3 (N3)](https://notation3.org/) reasoner in **JavaScript**.
4
6
 
5
7
  - Single self-contained bundle (`eyeling.js`), no external runtime dependencies
@@ -29,3 +29,5 @@
29
29
  # OWL rules
30
30
  {?P a owl:TransitiveProperty. ?S ?P ?X. ?X ?P ?O.} => {?S ?P ?O}.
31
31
 
32
+ # query
33
+ {?C :path :nantes} log:query {?C :path :nantes}.
@@ -0,0 +1,92 @@
1
+ # =================================================================
2
+ # More advanced jsonterm example.
3
+ #
4
+ # Mapping used:
5
+ # - JSON objects become anonymous node structures.
6
+ # - JSON arrays become N3 lists.
7
+ # - JSON null becomes a normal symbol here, j:null.
8
+ # - JSON booleans become ordinary boolean literals: true and false.
9
+ # =================================================================
10
+
11
+ @prefix ex: <https://example.org/#> .
12
+ @prefix j: <https://example.org/json#> .
13
+
14
+ [
15
+ j:id "user-101";
16
+ j:profile [
17
+ j:name "Alice";
18
+ j:active true;
19
+ j:address [
20
+ j:city "Ghent";
21
+ j:country "BE"
22
+ ]
23
+ ];
24
+ j:roles ("admin" "editor");
25
+ j:preferences [
26
+ j:languages ("nl" "fr" "en");
27
+ j:theme "dark";
28
+ j:beta false
29
+ ];
30
+ j:orders (
31
+ [
32
+ j:id "ord-1";
33
+ j:total 125;
34
+ j:items ("book" "pen")
35
+ ]
36
+ [
37
+ j:id "ord-2";
38
+ j:total 45;
39
+ j:items ("notebook");
40
+ j:coupon j:null
41
+ ]
42
+ )
43
+ ] ex:kind j:UserDocument .
44
+
45
+ {
46
+ ?Doc ex:kind j:UserDocument .
47
+ ?Doc j:profile [
48
+ j:name ?Name;
49
+ j:active true;
50
+ j:address [ j:country "BE" ]
51
+ ] .
52
+ }
53
+ =>
54
+ {
55
+ ?Doc ex:userName ?Name .
56
+ ?Doc ex:eligibleForReview true .
57
+ } .
58
+
59
+ {
60
+ ?Doc j:preferences [
61
+ j:languages ("nl" "fr" "en");
62
+ j:theme "dark";
63
+ j:beta false
64
+ ] .
65
+ }
66
+ =>
67
+ {
68
+ ?Doc ex:profileTag "multilingual-dark-profile" .
69
+ } .
70
+
71
+ {
72
+ ?Doc j:orders (
73
+ [ j:id ?FirstId; j:total 125; j:items ("book" "pen") ]
74
+ [ j:id ?SecondId; j:total 45; j:items ("notebook"); j:coupon j:null ]
75
+ ) .
76
+ }
77
+ =>
78
+ {
79
+ ?Doc ex:hasHighValueStarterOrder true .
80
+ ?Doc ex:hasCouponlessFollowup true .
81
+ } .
82
+
83
+ {
84
+ ?Doc ex:eligibleForReview true .
85
+ ?Doc ex:profileTag "multilingual-dark-profile" .
86
+ ?Doc ex:hasHighValueStarterOrder true .
87
+ ?Doc ex:hasCouponlessFollowup true .
88
+ }
89
+ =>
90
+ {
91
+ ex:test ex:is true .
92
+ } .
@@ -0,0 +1,37 @@
1
+ # =================================================
2
+ # jsonterm example
3
+ #
4
+ # Mapping used:
5
+ # - JSON objects become anonymous node structures.
6
+ # - JSON arrays become N3 lists.
7
+ # - JSON null becomes a normal symbol here, j:null.
8
+ # =================================================
9
+
10
+ @prefix ex: <https://example.org/#> .
11
+ @prefix j: <https://example.org/json#> .
12
+
13
+ [
14
+ j:user "alice";
15
+ j:roles ("admin" j:null)
16
+ ]
17
+ ("likes" 1)
18
+ [
19
+ j:city "Paris";
20
+ j:coords (2.35 48.85)
21
+ ] .
22
+
23
+ {
24
+ [
25
+ j:user ?U;
26
+ j:roles ("admin" j:null)
27
+ ]
28
+ ("likes" 1)
29
+ [
30
+ j:city "Paris";
31
+ j:coords (2.35 48.85)
32
+ ] .
33
+ }
34
+ =>
35
+ {
36
+ ex:test ex:is true .
37
+ } .
@@ -1,23 +1,6 @@
1
1
  @prefix : <http://www.agfa.com/w3c/euler/graph.axiom#> .
2
2
 
3
- :paris :path :orleans .
4
- :paris :path :chartres .
5
- :paris :path :amiens .
6
- :orleans :path :blois .
7
- :orleans :path :bourges .
8
- :blois :path :tours .
9
- :chartres :path :lemans .
10
- :lemans :path :angers .
11
- :lemans :path :tours .
12
3
  :angers :path :nantes .
13
- :paris :path :blois .
14
- :paris :path :bourges .
15
- :paris :path :lemans .
16
- :orleans :path :tours .
17
- :chartres :path :angers .
18
- :chartres :path :tours .
19
- :lemans :path :nantes .
20
- :paris :path :tours .
21
- :paris :path :angers .
22
4
  :chartres :path :nantes .
5
+ :lemans :path :nantes .
23
6
  :paris :path :nantes .
@@ -0,0 +1,8 @@
1
+ @prefix ex: <https://example.org/#> .
2
+
3
+ _:b1 ex:userName "Alice" .
4
+ _:b1 ex:eligibleForReview true .
5
+ _:b1 ex:profileTag "multilingual-dark-profile" .
6
+ _:b1 ex:hasHighValueStarterOrder true .
7
+ _:b1 ex:hasCouponlessFollowup true .
8
+ ex:test ex:is true .
@@ -0,0 +1,3 @@
1
+ @prefix ex: <https://example.org/#> .
2
+
3
+ ex:test ex:is true .
package/eyeling.js CHANGED
@@ -5352,27 +5352,80 @@ function triplesListEqual(xs, ys) {
5352
5352
  return true;
5353
5353
  }
5354
5354
 
5355
- // Alpha-equivalence for quoted formulas, up to *variable* and blank-node renaming.
5355
+ function collectProtectedNamesInTerm(t, protectedVars, protectedBlanks) {
5356
+ if (t instanceof Var) {
5357
+ protectedVars.add(t.name);
5358
+ return;
5359
+ }
5360
+ if (t instanceof Blank) {
5361
+ protectedBlanks.add(t.label);
5362
+ return;
5363
+ }
5364
+ if (t instanceof ListTerm) {
5365
+ for (const e of t.elems) collectProtectedNamesInTerm(e, protectedVars, protectedBlanks);
5366
+ return;
5367
+ }
5368
+ if (t instanceof OpenListTerm) {
5369
+ for (const e of t.prefix) collectProtectedNamesInTerm(e, protectedVars, protectedBlanks);
5370
+ protectedVars.add(t.tailVar);
5371
+ return;
5372
+ }
5373
+ if (t instanceof GraphTerm) {
5374
+ for (const tr of t.triples) {
5375
+ collectProtectedNamesInTerm(tr.s, protectedVars, protectedBlanks);
5376
+ collectProtectedNamesInTerm(tr.p, protectedVars, protectedBlanks);
5377
+ collectProtectedNamesInTerm(tr.o, protectedVars, protectedBlanks);
5378
+ }
5379
+ }
5380
+ }
5381
+
5382
+ function collectProtectedNamesFromSubst(subst) {
5383
+ const protectedVars = new Set();
5384
+ const protectedBlanks = new Set();
5385
+ if (!subst) return { protectedVars, protectedBlanks };
5386
+ for (const k in subst) {
5387
+ if (!Object.prototype.hasOwnProperty.call(subst, k)) continue;
5388
+ collectProtectedNamesInTerm(subst[k], protectedVars, protectedBlanks);
5389
+ }
5390
+ return { protectedVars, protectedBlanks };
5391
+ }
5392
+
5393
+ // Alpha-equivalence for quoted formulas, up to *local* variable and blank-node renaming.
5394
+ // Terms that originate from the surrounding substitution are treated as fixed and are
5395
+ // therefore not alpha-renamable inside the quoted formula.
5356
5396
  // Treats a formula as an unordered set of triples (order-insensitive match).
5357
- function alphaEqVarName(x, y, vmap) {
5397
+ function alphaEqVarName(x, y, vmap, protectedVarsA, protectedVarsB) {
5398
+ const xProtected = protectedVarsA && protectedVarsA.has(x);
5399
+ const yProtected = protectedVarsB && protectedVarsB.has(y);
5400
+ if (xProtected || yProtected) return xProtected && yProtected && x === y;
5358
5401
  if (Object.prototype.hasOwnProperty.call(vmap, x)) return vmap[x] === y;
5359
5402
  vmap[x] = y;
5360
5403
  return true;
5361
5404
  }
5362
5405
 
5363
- function alphaEqTermInGraph(a, b, vmap, bmap) {
5364
- // Blank nodes: renamable
5406
+ function alphaEqBlankLabel(x, y, bmap, protectedBlanksA, protectedBlanksB) {
5407
+ const xProtected = protectedBlanksA && protectedBlanksA.has(x);
5408
+ const yProtected = protectedBlanksB && protectedBlanksB.has(y);
5409
+ if (xProtected || yProtected) return xProtected && yProtected && x === y;
5410
+ if (Object.prototype.hasOwnProperty.call(bmap, x)) return bmap[x] === y;
5411
+ bmap[x] = y;
5412
+ return true;
5413
+ }
5414
+
5415
+ function alphaEqTermInGraph(a, b, vmap, bmap, opts) {
5416
+ const protectedVarsA = opts && opts.protectedVarsA;
5417
+ const protectedVarsB = opts && opts.protectedVarsB;
5418
+ const protectedBlanksA = opts && opts.protectedBlanksA;
5419
+ const protectedBlanksB = opts && opts.protectedBlanksB;
5420
+
5421
+ // Blank nodes: renamable only when they are local to the formula.
5365
5422
  if (a instanceof Blank && b instanceof Blank) {
5366
- const x = a.label;
5367
- const y = b.label;
5368
- if (Object.prototype.hasOwnProperty.call(bmap, x)) return bmap[x] === y;
5369
- bmap[x] = y;
5370
- return true;
5423
+ return alphaEqBlankLabel(a.label, b.label, bmap, protectedBlanksA, protectedBlanksB);
5371
5424
  }
5372
5425
 
5373
- // Variables: renamable (ONLY inside quoted formulas)
5426
+ // Variables: renamable only when they are local to the formula.
5374
5427
  if (a instanceof Var && b instanceof Var) {
5375
- return alphaEqVarName(a.name, b.name, vmap);
5428
+ return alphaEqVarName(a.name, b.name, vmap, protectedVarsA, protectedVarsB);
5376
5429
  }
5377
5430
 
5378
5431
  if (a instanceof Iri && b instanceof Iri) return a.value === b.value;
@@ -5381,7 +5434,7 @@ function alphaEqTermInGraph(a, b, vmap, bmap) {
5381
5434
  if (a instanceof ListTerm && b instanceof ListTerm) {
5382
5435
  if (a.elems.length !== b.elems.length) return false;
5383
5436
  for (let i = 0; i < a.elems.length; i++) {
5384
- if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap)) return false;
5437
+ if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap, opts)) return false;
5385
5438
  }
5386
5439
  return true;
5387
5440
  }
@@ -5389,29 +5442,30 @@ function alphaEqTermInGraph(a, b, vmap, bmap) {
5389
5442
  if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
5390
5443
  if (a.prefix.length !== b.prefix.length) return false;
5391
5444
  for (let i = 0; i < a.prefix.length; i++) {
5392
- if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap)) return false;
5445
+ if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap, opts)) return false;
5393
5446
  }
5394
- // tailVar is a var-name string, so treat it as renamable too
5395
- return alphaEqVarName(a.tailVar, b.tailVar, vmap);
5447
+ // tailVar is a var-name string, so treat it as renamable too when local.
5448
+ return alphaEqVarName(a.tailVar, b.tailVar, vmap, protectedVarsA, protectedVarsB);
5396
5449
  }
5397
5450
 
5398
- // Nested formulas: compare with fresh maps (separate scope)
5451
+ // Nested formulas: compare with fresh maps (separate scope), but keep the same
5452
+ // protected outer names so already-substituted terms stay fixed everywhere.
5399
5453
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
5400
- return alphaEqGraphTriples(a.triples, b.triples);
5454
+ return alphaEqGraphTriples(a.triples, b.triples, opts);
5401
5455
  }
5402
5456
 
5403
5457
  return false;
5404
5458
  }
5405
5459
 
5406
- function alphaEqTripleInGraph(a, b, vmap, bmap) {
5460
+ function alphaEqTripleInGraph(a, b, vmap, bmap, opts) {
5407
5461
  return (
5408
- alphaEqTermInGraph(a.s, b.s, vmap, bmap) &&
5409
- alphaEqTermInGraph(a.p, b.p, vmap, bmap) &&
5410
- alphaEqTermInGraph(a.o, b.o, vmap, bmap)
5462
+ alphaEqTermInGraph(a.s, b.s, vmap, bmap, opts) &&
5463
+ alphaEqTermInGraph(a.p, b.p, vmap, bmap, opts) &&
5464
+ alphaEqTermInGraph(a.o, b.o, vmap, bmap, opts)
5411
5465
  );
5412
5466
  }
5413
5467
 
5414
- function alphaEqGraphTriples(xs, ys) {
5468
+ function alphaEqGraphTriples(xs, ys, opts) {
5415
5469
  if (xs.length !== ys.length) return false;
5416
5470
  // Fast path: exact same sequence.
5417
5471
  if (triplesListEqual(xs, ys)) return true;
@@ -5430,7 +5484,7 @@ function alphaEqGraphTriples(xs, ys) {
5430
5484
 
5431
5485
  const v2 = { ...vmap };
5432
5486
  const b2 = { ...bmap };
5433
- if (!alphaEqTripleInGraph(x, y, v2, b2)) continue;
5487
+ if (!alphaEqTripleInGraph(x, y, v2, b2, opts)) continue;
5434
5488
 
5435
5489
  used[j] = true;
5436
5490
  if (step(i + 1, v2, b2)) return true;
@@ -6149,7 +6203,17 @@ function unifyTermWithOptions(a, b, subst, opts) {
6149
6203
 
6150
6204
  // Graphs
6151
6205
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
6152
- if (alphaEqGraphTriples(a.triples, b.triples)) return subst;
6206
+ const protectedNames = collectProtectedNamesFromSubst(subst);
6207
+ if (
6208
+ alphaEqGraphTriples(a.triples, b.triples, {
6209
+ protectedVarsA: protectedNames.protectedVars,
6210
+ protectedVarsB: protectedNames.protectedVars,
6211
+ protectedBlanksA: protectedNames.protectedBlanks,
6212
+ protectedBlanksB: protectedNames.protectedBlanks,
6213
+ })
6214
+ ) {
6215
+ return subst;
6216
+ }
6153
6217
  return unifyGraphTriples(a.triples, b.triples, subst);
6154
6218
  }
6155
6219
 
@@ -6521,7 +6585,17 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6521
6585
 
6522
6586
  // Graphs
6523
6587
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
6524
- if (alphaEqGraphTriples(a.triples, b.triples)) return true;
6588
+ const protectedNames = collectProtectedNamesFromSubst(substMut);
6589
+ if (
6590
+ alphaEqGraphTriples(a.triples, b.triples, {
6591
+ protectedVarsA: protectedNames.protectedVars,
6592
+ protectedVarsB: protectedNames.protectedVars,
6593
+ protectedBlanksA: protectedNames.protectedBlanks,
6594
+ protectedBlanksB: protectedNames.protectedBlanks,
6595
+ })
6596
+ ) {
6597
+ return true;
6598
+ }
6525
6599
  // Fallback: reuse allocation-heavy graph unifier rarely hit in typical workloads.
6526
6600
  const delta = unifyGraphTriples(a.triples, b.triples, {});
6527
6601
  if (delta === null) return false;
package/lib/engine.js CHANGED
@@ -736,27 +736,80 @@ function triplesListEqual(xs, ys) {
736
736
  return true;
737
737
  }
738
738
 
739
- // Alpha-equivalence for quoted formulas, up to *variable* and blank-node renaming.
739
+ function collectProtectedNamesInTerm(t, protectedVars, protectedBlanks) {
740
+ if (t instanceof Var) {
741
+ protectedVars.add(t.name);
742
+ return;
743
+ }
744
+ if (t instanceof Blank) {
745
+ protectedBlanks.add(t.label);
746
+ return;
747
+ }
748
+ if (t instanceof ListTerm) {
749
+ for (const e of t.elems) collectProtectedNamesInTerm(e, protectedVars, protectedBlanks);
750
+ return;
751
+ }
752
+ if (t instanceof OpenListTerm) {
753
+ for (const e of t.prefix) collectProtectedNamesInTerm(e, protectedVars, protectedBlanks);
754
+ protectedVars.add(t.tailVar);
755
+ return;
756
+ }
757
+ if (t instanceof GraphTerm) {
758
+ for (const tr of t.triples) {
759
+ collectProtectedNamesInTerm(tr.s, protectedVars, protectedBlanks);
760
+ collectProtectedNamesInTerm(tr.p, protectedVars, protectedBlanks);
761
+ collectProtectedNamesInTerm(tr.o, protectedVars, protectedBlanks);
762
+ }
763
+ }
764
+ }
765
+
766
+ function collectProtectedNamesFromSubst(subst) {
767
+ const protectedVars = new Set();
768
+ const protectedBlanks = new Set();
769
+ if (!subst) return { protectedVars, protectedBlanks };
770
+ for (const k in subst) {
771
+ if (!Object.prototype.hasOwnProperty.call(subst, k)) continue;
772
+ collectProtectedNamesInTerm(subst[k], protectedVars, protectedBlanks);
773
+ }
774
+ return { protectedVars, protectedBlanks };
775
+ }
776
+
777
+ // Alpha-equivalence for quoted formulas, up to *local* variable and blank-node renaming.
778
+ // Terms that originate from the surrounding substitution are treated as fixed and are
779
+ // therefore not alpha-renamable inside the quoted formula.
740
780
  // Treats a formula as an unordered set of triples (order-insensitive match).
741
- function alphaEqVarName(x, y, vmap) {
781
+ function alphaEqVarName(x, y, vmap, protectedVarsA, protectedVarsB) {
782
+ const xProtected = protectedVarsA && protectedVarsA.has(x);
783
+ const yProtected = protectedVarsB && protectedVarsB.has(y);
784
+ if (xProtected || yProtected) return xProtected && yProtected && x === y;
742
785
  if (Object.prototype.hasOwnProperty.call(vmap, x)) return vmap[x] === y;
743
786
  vmap[x] = y;
744
787
  return true;
745
788
  }
746
789
 
747
- function alphaEqTermInGraph(a, b, vmap, bmap) {
748
- // Blank nodes: renamable
790
+ function alphaEqBlankLabel(x, y, bmap, protectedBlanksA, protectedBlanksB) {
791
+ const xProtected = protectedBlanksA && protectedBlanksA.has(x);
792
+ const yProtected = protectedBlanksB && protectedBlanksB.has(y);
793
+ if (xProtected || yProtected) return xProtected && yProtected && x === y;
794
+ if (Object.prototype.hasOwnProperty.call(bmap, x)) return bmap[x] === y;
795
+ bmap[x] = y;
796
+ return true;
797
+ }
798
+
799
+ function alphaEqTermInGraph(a, b, vmap, bmap, opts) {
800
+ const protectedVarsA = opts && opts.protectedVarsA;
801
+ const protectedVarsB = opts && opts.protectedVarsB;
802
+ const protectedBlanksA = opts && opts.protectedBlanksA;
803
+ const protectedBlanksB = opts && opts.protectedBlanksB;
804
+
805
+ // Blank nodes: renamable only when they are local to the formula.
749
806
  if (a instanceof Blank && b instanceof Blank) {
750
- const x = a.label;
751
- const y = b.label;
752
- if (Object.prototype.hasOwnProperty.call(bmap, x)) return bmap[x] === y;
753
- bmap[x] = y;
754
- return true;
807
+ return alphaEqBlankLabel(a.label, b.label, bmap, protectedBlanksA, protectedBlanksB);
755
808
  }
756
809
 
757
- // Variables: renamable (ONLY inside quoted formulas)
810
+ // Variables: renamable only when they are local to the formula.
758
811
  if (a instanceof Var && b instanceof Var) {
759
- return alphaEqVarName(a.name, b.name, vmap);
812
+ return alphaEqVarName(a.name, b.name, vmap, protectedVarsA, protectedVarsB);
760
813
  }
761
814
 
762
815
  if (a instanceof Iri && b instanceof Iri) return a.value === b.value;
@@ -765,7 +818,7 @@ function alphaEqTermInGraph(a, b, vmap, bmap) {
765
818
  if (a instanceof ListTerm && b instanceof ListTerm) {
766
819
  if (a.elems.length !== b.elems.length) return false;
767
820
  for (let i = 0; i < a.elems.length; i++) {
768
- if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap)) return false;
821
+ if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap, opts)) return false;
769
822
  }
770
823
  return true;
771
824
  }
@@ -773,29 +826,30 @@ function alphaEqTermInGraph(a, b, vmap, bmap) {
773
826
  if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
774
827
  if (a.prefix.length !== b.prefix.length) return false;
775
828
  for (let i = 0; i < a.prefix.length; i++) {
776
- if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap)) return false;
829
+ if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap, opts)) return false;
777
830
  }
778
- // tailVar is a var-name string, so treat it as renamable too
779
- return alphaEqVarName(a.tailVar, b.tailVar, vmap);
831
+ // tailVar is a var-name string, so treat it as renamable too when local.
832
+ return alphaEqVarName(a.tailVar, b.tailVar, vmap, protectedVarsA, protectedVarsB);
780
833
  }
781
834
 
782
- // Nested formulas: compare with fresh maps (separate scope)
835
+ // Nested formulas: compare with fresh maps (separate scope), but keep the same
836
+ // protected outer names so already-substituted terms stay fixed everywhere.
783
837
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
784
- return alphaEqGraphTriples(a.triples, b.triples);
838
+ return alphaEqGraphTriples(a.triples, b.triples, opts);
785
839
  }
786
840
 
787
841
  return false;
788
842
  }
789
843
 
790
- function alphaEqTripleInGraph(a, b, vmap, bmap) {
844
+ function alphaEqTripleInGraph(a, b, vmap, bmap, opts) {
791
845
  return (
792
- alphaEqTermInGraph(a.s, b.s, vmap, bmap) &&
793
- alphaEqTermInGraph(a.p, b.p, vmap, bmap) &&
794
- alphaEqTermInGraph(a.o, b.o, vmap, bmap)
846
+ alphaEqTermInGraph(a.s, b.s, vmap, bmap, opts) &&
847
+ alphaEqTermInGraph(a.p, b.p, vmap, bmap, opts) &&
848
+ alphaEqTermInGraph(a.o, b.o, vmap, bmap, opts)
795
849
  );
796
850
  }
797
851
 
798
- function alphaEqGraphTriples(xs, ys) {
852
+ function alphaEqGraphTriples(xs, ys, opts) {
799
853
  if (xs.length !== ys.length) return false;
800
854
  // Fast path: exact same sequence.
801
855
  if (triplesListEqual(xs, ys)) return true;
@@ -814,7 +868,7 @@ function alphaEqGraphTriples(xs, ys) {
814
868
 
815
869
  const v2 = { ...vmap };
816
870
  const b2 = { ...bmap };
817
- if (!alphaEqTripleInGraph(x, y, v2, b2)) continue;
871
+ if (!alphaEqTripleInGraph(x, y, v2, b2, opts)) continue;
818
872
 
819
873
  used[j] = true;
820
874
  if (step(i + 1, v2, b2)) return true;
@@ -1533,7 +1587,17 @@ function unifyTermWithOptions(a, b, subst, opts) {
1533
1587
 
1534
1588
  // Graphs
1535
1589
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
1536
- if (alphaEqGraphTriples(a.triples, b.triples)) return subst;
1590
+ const protectedNames = collectProtectedNamesFromSubst(subst);
1591
+ if (
1592
+ alphaEqGraphTriples(a.triples, b.triples, {
1593
+ protectedVarsA: protectedNames.protectedVars,
1594
+ protectedVarsB: protectedNames.protectedVars,
1595
+ protectedBlanksA: protectedNames.protectedBlanks,
1596
+ protectedBlanksB: protectedNames.protectedBlanks,
1597
+ })
1598
+ ) {
1599
+ return subst;
1600
+ }
1537
1601
  return unifyGraphTriples(a.triples, b.triples, subst);
1538
1602
  }
1539
1603
 
@@ -1905,7 +1969,17 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
1905
1969
 
1906
1970
  // Graphs
1907
1971
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
1908
- if (alphaEqGraphTriples(a.triples, b.triples)) return true;
1972
+ const protectedNames = collectProtectedNamesFromSubst(substMut);
1973
+ if (
1974
+ alphaEqGraphTriples(a.triples, b.triples, {
1975
+ protectedVarsA: protectedNames.protectedVars,
1976
+ protectedVarsB: protectedNames.protectedVars,
1977
+ protectedBlanksA: protectedNames.protectedBlanks,
1978
+ protectedBlanksB: protectedNames.protectedBlanks,
1979
+ })
1980
+ ) {
1981
+ return true;
1982
+ }
1909
1983
  // Fallback: reuse allocation-heavy graph unifier rarely hit in typical workloads.
1910
1984
  const delta = unifyGraphTriples(a.triples, b.triples, {});
1911
1985
  if (delta === null) return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.15.9",
3
+ "version": "1.15.10",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -1432,6 +1432,34 @@ ex:w a ex:Woman .
1432
1432
  `,
1433
1433
  expect: [/:(?:test)\s+:(?:is)\s+true\s*\./],
1434
1434
  },
1435
+
1436
+ {
1437
+ name: '59 regression: quoted-formula alpha-equivalence must not rename blanks introduced by outer substitution',
1438
+ opt: { proofComments: false },
1439
+ input: `@prefix : <http://example.org/> .
1440
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
1441
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
1442
+
1443
+ _:x :hates { _:foo :making :mess }.
1444
+
1445
+ {
1446
+ ?A :hates { ?A :making :mess }.
1447
+ }
1448
+ =>
1449
+ {
1450
+ ?A :hates :Himself.
1451
+ }.
1452
+
1453
+ {
1454
+ ?A :hates :Himself.
1455
+ }
1456
+ =>
1457
+ {
1458
+ :test :is false.
1459
+ }.
1460
+ `,
1461
+ notExpect: [/:(?:test)\s+:(?:is)\s+false\s*\./],
1462
+ },
1435
1463
  ];
1436
1464
 
1437
1465
  let passed = 0;