eyeling 1.25.3 → 1.25.4

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
@@ -559,21 +559,25 @@ Facts live in an array `facts: Triple[]`.
559
559
  Eyeling attaches hidden (non-enumerable) index fields:
560
560
 
561
561
  - `facts.__byPred: Map<predicateId, number[]>` where each entry is an index into `facts` (and `predicateId` is `predicate.__tid`)
562
- - `facts.__byPS: Map<predicateId, Map<termId, number[]>>` where each entry is an index into `facts` (and `termId` is `term.__tid`)
563
- - `facts.__byPO: Map<predicateId, Map<termId, number[]>>` where each entry is an index into `facts` (and `termId` is `term.__tid`)
562
+ - `facts.__byPS: Map<predicateId, Map<lookupKey, number[]>>` where each entry is an index into `facts`
563
+ - `facts.__byPO: Map<predicateId, Map<lookupKey, number[]>>` where each entry is an index into `facts`
564
+ - `facts.__byPNonFastS` / `facts.__byPNonFastO` for the small fallback set of IRI-predicate facts whose subject/object cannot be fast-keyed (for example variables or quoted formulas)
565
+ - `facts.__varPred*` for top-level facts whose predicate is a variable; these are the only non-IRI-predicate facts that can match a ground IRI predicate goal
564
566
  - `facts.__keySet: Set<string>` for a fast-path `"sid pid oid"` key (all three are `__tid` values)
565
567
 
566
- `termFastKey(term)` returns a `termId` (`term.__tid`) for **Iri**, **Literal**, and **Blank** terms, and `null` for structured terms (lists, quoted graphs) and variables.
568
+ `termFastKey(term)` returns a `termId` (`term.__tid`) for **Iri**, **Literal**, **Blank**, and strict-ground list terms, and `null` for quoted graphs, open lists, and variables. Proving-time lookup uses `termLookupKey(term)`, which is aligned with `unifyTerm`: it keeps exact ids for IRIs/blanks/strings, but canonicalizes value-equivalent booleans and numerics such as `true` / `"1"^^xsd:boolean` and `1.0` / `1.00`.
567
569
 
568
- The “fast key” only exists when `termFastKey` succeeds for all three terms.
570
+ The duplicate-check “fast key” only exists when `termFastKey` succeeds for all three terms; the proof indexes use the broader `termLookupKey`.
569
571
 
570
572
  ### 7.2 Candidate selection: pick the smallest bucket
571
573
 
572
574
  When proving a goal with IRI predicate, Eyeling computes candidate facts by:
573
575
 
574
- 1. restricting to predicate bucket
575
- 2. optionally narrowing further by subject or object fast key
576
- 3. choosing the smaller of (p,s) vs (p,o) when both exist
576
+ 1. restricting to the IRI predicate bucket
577
+ 2. if subject or object has a lookup key, using the matching `(p,s)` or `(p,o)` bucket plus only the non-fast fallback facts for that same position
578
+ 3. choosing the smaller of the subject-constrained and object-constrained candidate sets when both exist
579
+
580
+ Variable-predicate facts are kept in a separate tiny fallback index. Blank/list/formula predicates are not scanned for an IRI predicate goal because they cannot unify with an IRI predicate.
577
581
 
578
582
  This is a cheap selectivity heuristic. In type-heavy RDF, `(p,o)` is often extremely selective (e.g., `rdf:type` + a class IRI), so the PO index can be a major speed win.
579
583
 
@@ -583,7 +587,7 @@ The same selectivity idea is also reused by the single-premise forward-rule agen
583
587
 
584
588
  When adding derived facts, Eyeling uses a fast-path duplicate check when possible:
585
589
 
586
- - If all three terms have a fast key (Iri/Literal/Blank → `__tid`), it checks membership in `facts.__keySet` using the `"sid pid oid"` key.
590
+ - If all three terms have a duplicate-check fast key, it checks membership in `facts.__keySet` using the `"sid pid oid"` key.
587
591
  - Otherwise (lists, quoted graphs, variables), it falls back to structural triple equality.
588
592
 
589
593
  This still treats blanks correctly: blanks are _not_ interchangeable; the blank **label** (and thus its `__tid`) is part of the key.
@@ -6523,6 +6523,12 @@ function alphaEqGraphTriples(xs, ys, opts) {
6523
6523
  // - __byPred: Map<predicateId, number[]> (indices into facts array)
6524
6524
  // - __byPS: Map<predicateId, Map<subjectId, number[]>>
6525
6525
  // - __byPO: Map<predicateId, Map<objectId, number[]>>
6526
+ // - __byPNonFastS / __byPNonFastO: Map<predicateId, number[]>
6527
+ // IRI-predicate facts whose subject/object cannot be indexed by fast key.
6528
+ // These are the fallback clauses for a constrained subject/object, just
6529
+ // like a Prolog clause index keeps variable-headed clauses as fallback.
6530
+ // - __varPred* indexes: facts whose predicate is a Var. Only these non-IRI
6531
+ // predicate facts can unify with a ground IRI predicate goal.
6526
6532
  // - __keySet: Set<"S\tP\tO"> for Iri/Literal/Blank-only triples (fast dup check)
6527
6533
  //
6528
6534
  // Backward rules:
@@ -6538,6 +6544,8 @@ const __compoundKeyToTid = new Map();
6538
6544
  // Use a negative id space so we never collide with __tid (which is positive).
6539
6545
  let __nextCompoundTid = -1;
6540
6546
 
6547
+ const EMPTY_FACT_INDEX_BUCKET = Object.freeze([]);
6548
+
6541
6549
  function __internCompoundTid(key) {
6542
6550
  const hit = __compoundKeyToTid.get(key);
6543
6551
  if (hit !== undefined) return hit;
@@ -6589,6 +6597,71 @@ function termFastKey(t) {
6589
6597
  return null;
6590
6598
  }
6591
6599
 
6600
+ function encodeLookupKeyPart(k) {
6601
+ if (typeof k === 'number') return 'T' + k;
6602
+ const s = String(k);
6603
+ return 'K' + s.length + ':' + s;
6604
+ }
6605
+
6606
+ function literalLookupKey(t) {
6607
+ const boolInfo = parseBooleanLiteralInfo(t);
6608
+ if (boolInfo) return '\u0000B' + (boolInfo.value ? '1' : '0');
6609
+
6610
+ const numInfo = parseNumericLiteralInfo(t);
6611
+ if (numInfo) {
6612
+ if (numInfo.kind === 'bigint') return '\u0000N' + numInfo.dt + '\u0000' + numInfo.value.toString();
6613
+
6614
+ const n = numInfo.value;
6615
+ // Normal unification intentionally does not make NaN value-equal to NaN;
6616
+ // only identical lexical literals match through the ordinary __tid path.
6617
+ if (!Number.isNaN(n)) return '\u0000N' + numInfo.dt + '\u0000' + String(n);
6618
+ }
6619
+
6620
+ // Covers exact literals plus plain string / xsd:string canonicalization, which
6621
+ // Literal construction already normalizes into a shared __tid.
6622
+ return termFastKey(t);
6623
+ }
6624
+
6625
+ function termLookupKey(t) {
6626
+ // Lookup keys summarize the equality accepted by ordinary unifyTerm(), not
6627
+ // merely object identity. This keeps literal-index pruning complete for
6628
+ // value-equivalent booleans/numerics such as true/"1"^^xsd:boolean and
6629
+ // 1.0/1.00, while preserving exact fast ids for IRIs, blanks, and strings.
6630
+ if (t instanceof Iri) {
6631
+ if (t.value === RDF_NIL_IRI) return '\u0000L0';
6632
+ return t.__tid;
6633
+ }
6634
+ if (t instanceof Blank) return t.__tid;
6635
+ if (t instanceof Literal) return literalLookupKey(t);
6636
+
6637
+ if (t instanceof ListTerm) {
6638
+ const cached = t.__lookupKey;
6639
+ if (cached !== undefined) return cached === false ? null : cached;
6640
+
6641
+ const xs = t.elems;
6642
+ if (xs.length === 0) {
6643
+ Object.defineProperty(t, '__lookupKey', { value: '\u0000L0', enumerable: false });
6644
+ return '\u0000L0';
6645
+ }
6646
+
6647
+ const parts = new Array(xs.length);
6648
+ for (let i = 0; i < xs.length; i++) {
6649
+ const k = termLookupKey(xs[i]);
6650
+ if (k === null) {
6651
+ Object.defineProperty(t, '__lookupKey', { value: false, enumerable: false });
6652
+ return null;
6653
+ }
6654
+ parts[i] = encodeLookupKeyPart(k);
6655
+ }
6656
+
6657
+ const key = '\u0000L' + xs.length + '\u0001' + parts.join('\u0001');
6658
+ Object.defineProperty(t, '__lookupKey', { value: key, enumerable: false });
6659
+ return key;
6660
+ }
6661
+
6662
+ return null;
6663
+ }
6664
+
6592
6665
  function tripleFastKey(tr) {
6593
6666
  const ks = termFastKey(tr.s);
6594
6667
  const kp = termFastKey(tr.p);
@@ -6602,9 +6675,13 @@ function ensureFactIndexes(facts) {
6602
6675
  facts.__byPred &&
6603
6676
  facts.__byPS &&
6604
6677
  facts.__byPO &&
6605
- facts.__wildPred &&
6606
- facts.__wildPS &&
6607
- facts.__wildPO &&
6678
+ facts.__byPNonFastS &&
6679
+ facts.__byPNonFastO &&
6680
+ facts.__varPred &&
6681
+ facts.__varPredPS &&
6682
+ facts.__varPredPO &&
6683
+ facts.__varPredNonFastS &&
6684
+ facts.__varPredNonFastO &&
6608
6685
  facts.__keySet
6609
6686
  )
6610
6687
  return;
@@ -6624,21 +6701,41 @@ function ensureFactIndexes(facts) {
6624
6701
  enumerable: false,
6625
6702
  writable: true,
6626
6703
  });
6627
- Object.defineProperty(facts, '__wildPred', {
6704
+ Object.defineProperty(facts, '__byPNonFastS', {
6705
+ value: new Map(),
6706
+ enumerable: false,
6707
+ writable: true,
6708
+ });
6709
+ Object.defineProperty(facts, '__byPNonFastO', {
6710
+ value: new Map(),
6711
+ enumerable: false,
6712
+ writable: true,
6713
+ });
6714
+ Object.defineProperty(facts, '__varPred', {
6628
6715
  value: [],
6629
6716
  enumerable: false,
6630
6717
  writable: true,
6631
6718
  });
6632
- Object.defineProperty(facts, '__wildPS', {
6719
+ Object.defineProperty(facts, '__varPredPS', {
6633
6720
  value: new Map(),
6634
6721
  enumerable: false,
6635
6722
  writable: true,
6636
6723
  });
6637
- Object.defineProperty(facts, '__wildPO', {
6724
+ Object.defineProperty(facts, '__varPredPO', {
6638
6725
  value: new Map(),
6639
6726
  enumerable: false,
6640
6727
  writable: true,
6641
6728
  });
6729
+ Object.defineProperty(facts, '__varPredNonFastS', {
6730
+ value: [],
6731
+ enumerable: false,
6732
+ writable: true,
6733
+ });
6734
+ Object.defineProperty(facts, '__varPredNonFastO', {
6735
+ value: [],
6736
+ enumerable: false,
6737
+ writable: true,
6738
+ });
6642
6739
  Object.defineProperty(facts, '__keySet', {
6643
6740
  value: new Set(),
6644
6741
  enumerable: false,
@@ -6679,16 +6776,53 @@ function cloneFactIndexesForSnapshot(src, dest) {
6679
6776
  Object.defineProperty(dest, '__byPred', { value: cloneArrayMap(src.__byPred), enumerable: false, writable: true });
6680
6777
  Object.defineProperty(dest, '__byPS', { value: cloneNestedArrayMap(src.__byPS), enumerable: false, writable: true });
6681
6778
  Object.defineProperty(dest, '__byPO', { value: cloneNestedArrayMap(src.__byPO), enumerable: false, writable: true });
6682
- Object.defineProperty(dest, '__wildPred', { value: src.__wildPred.slice(), enumerable: false, writable: true });
6683
- Object.defineProperty(dest, '__wildPS', { value: cloneArrayMap(src.__wildPS), enumerable: false, writable: true });
6684
- Object.defineProperty(dest, '__wildPO', { value: cloneArrayMap(src.__wildPO), enumerable: false, writable: true });
6779
+ Object.defineProperty(dest, '__byPNonFastS', {
6780
+ value: cloneArrayMap(src.__byPNonFastS),
6781
+ enumerable: false,
6782
+ writable: true,
6783
+ });
6784
+ Object.defineProperty(dest, '__byPNonFastO', {
6785
+ value: cloneArrayMap(src.__byPNonFastO),
6786
+ enumerable: false,
6787
+ writable: true,
6788
+ });
6789
+ Object.defineProperty(dest, '__varPred', { value: src.__varPred.slice(), enumerable: false, writable: true });
6790
+ Object.defineProperty(dest, '__varPredPS', {
6791
+ value: cloneArrayMap(src.__varPredPS),
6792
+ enumerable: false,
6793
+ writable: true,
6794
+ });
6795
+ Object.defineProperty(dest, '__varPredPO', {
6796
+ value: cloneArrayMap(src.__varPredPO),
6797
+ enumerable: false,
6798
+ writable: true,
6799
+ });
6800
+ Object.defineProperty(dest, '__varPredNonFastS', {
6801
+ value: src.__varPredNonFastS.slice(),
6802
+ enumerable: false,
6803
+ writable: true,
6804
+ });
6805
+ Object.defineProperty(dest, '__varPredNonFastO', {
6806
+ value: src.__varPredNonFastO.slice(),
6807
+ enumerable: false,
6808
+ writable: true,
6809
+ });
6685
6810
  Object.defineProperty(dest, '__keySet', { value: new Set(src.__keySet), enumerable: false, writable: true });
6686
6811
  Object.defineProperty(dest, '__keySetComplete', { value: !!src.__keySetComplete, enumerable: false, writable: true });
6687
6812
  }
6688
6813
 
6814
+ function addToIndexArrayMap(map, key, value) {
6815
+ let bucket = map.get(key);
6816
+ if (!bucket) {
6817
+ bucket = [];
6818
+ map.set(key, bucket);
6819
+ }
6820
+ bucket.push(value);
6821
+ }
6822
+
6689
6823
  function indexFact(facts, tr, idx, addKeySet = true) {
6690
- const sk = termFastKey(tr.s);
6691
- const ok = termFastKey(tr.o);
6824
+ const sk = termLookupKey(tr.s);
6825
+ const ok = termLookupKey(tr.o);
6692
6826
  let pkForKey = null;
6693
6827
 
6694
6828
  if (tr.p instanceof Iri) {
@@ -6709,12 +6843,9 @@ function indexFact(facts, tr, idx, addKeySet = true) {
6709
6843
  ps = new Map();
6710
6844
  facts.__byPS.set(pk, ps);
6711
6845
  }
6712
- let psb = ps.get(sk);
6713
- if (!psb) {
6714
- psb = [];
6715
- ps.set(sk, psb);
6716
- }
6717
- psb.push(idx);
6846
+ addToIndexArrayMap(ps, sk, idx);
6847
+ } else {
6848
+ addToIndexArrayMap(facts.__byPNonFastS, pk, idx);
6718
6849
  }
6719
6850
 
6720
6851
  if (ok !== null) {
@@ -6723,32 +6854,23 @@ function indexFact(facts, tr, idx, addKeySet = true) {
6723
6854
  po = new Map();
6724
6855
  facts.__byPO.set(pk, po);
6725
6856
  }
6726
- let pob = po.get(ok);
6727
- if (!pob) {
6728
- pob = [];
6729
- po.set(ok, pob);
6730
- }
6731
- pob.push(idx);
6857
+ addToIndexArrayMap(po, ok, idx);
6858
+ } else {
6859
+ addToIndexArrayMap(facts.__byPNonFastO, pk, idx);
6732
6860
  }
6733
- } else {
6734
- facts.__wildPred.push(idx);
6861
+ } else if (tr.p instanceof Var) {
6862
+ facts.__varPred.push(idx);
6735
6863
 
6736
6864
  if (sk !== null) {
6737
- let psb = facts.__wildPS.get(sk);
6738
- if (!psb) {
6739
- psb = [];
6740
- facts.__wildPS.set(sk, psb);
6741
- }
6742
- psb.push(idx);
6865
+ addToIndexArrayMap(facts.__varPredPS, sk, idx);
6866
+ } else {
6867
+ facts.__varPredNonFastS.push(idx);
6743
6868
  }
6744
6869
 
6745
6870
  if (ok !== null) {
6746
- let pob = facts.__wildPO.get(ok);
6747
- if (!pob) {
6748
- pob = [];
6749
- facts.__wildPO.set(ok, pob);
6750
- }
6751
- pob.push(idx);
6871
+ addToIndexArrayMap(facts.__varPredPO, ok, idx);
6872
+ } else {
6873
+ facts.__varPredNonFastO.push(idx);
6752
6874
  }
6753
6875
  }
6754
6876
 
@@ -6758,55 +6880,86 @@ function indexFact(facts, tr, idx, addKeySet = true) {
6758
6880
  }
6759
6881
  }
6760
6882
 
6883
+ function mergeIndexBuckets(primary, fallback) {
6884
+ const a = primary && primary.length ? primary : null;
6885
+ const b = fallback && fallback.length ? fallback : null;
6886
+ if (!a && !b) return EMPTY_FACT_INDEX_BUCKET;
6887
+ if (!a) return b;
6888
+ if (!b) return a;
6889
+ const out = new Array(a.length + b.length);
6890
+ for (let i = 0; i < a.length; i++) out[i] = a[i];
6891
+ for (let i = 0; i < b.length; i++) out[a.length + i] = b[i];
6892
+ return out;
6893
+ }
6894
+
6895
+ function selectPositionIndexedCandidates(all, exactByS, fallbackS, sk, exactByO, fallbackO, ok) {
6896
+ if (sk === null && ok === null) return all && all.length ? all : EMPTY_FACT_INDEX_BUCKET;
6897
+
6898
+ let sBucket = null;
6899
+ if (sk !== null) sBucket = mergeIndexBuckets(exactByS || null, fallbackS || null);
6900
+
6901
+ let oBucket = null;
6902
+ if (ok !== null) oBucket = mergeIndexBuckets(exactByO || null, fallbackO || null);
6903
+
6904
+ if (sk !== null && ok !== null) return sBucket.length <= oBucket.length ? sBucket : oBucket;
6905
+ return sk !== null ? sBucket : oBucket;
6906
+ }
6907
+
6761
6908
  function candidateFacts(facts, goal) {
6762
6909
  ensureFactIndexes(facts);
6763
6910
 
6764
6911
  if (goal.p instanceof Iri) {
6765
6912
  const pk = goal.p.__tid;
6766
6913
 
6767
- const sk = termFastKey(goal.s);
6768
- const ok = termFastKey(goal.o);
6914
+ const sk = termLookupKey(goal.s);
6915
+ const ok = termLookupKey(goal.o);
6769
6916
 
6770
- /** @type {number[] | null} */
6771
6917
  let byPS = null;
6772
6918
  if (sk !== null) {
6773
6919
  const ps = facts.__byPS.get(pk);
6774
6920
  if (ps) byPS = ps.get(sk) || null;
6775
6921
  }
6922
+ const byPNonFastS = sk !== null ? facts.__byPNonFastS.get(pk) || null : null;
6776
6923
 
6777
- /** @type {number[] | null} */
6778
6924
  let byPO = null;
6779
6925
  if (ok !== null) {
6780
6926
  const po = facts.__byPO.get(pk);
6781
6927
  if (po) byPO = po.get(ok) || null;
6782
6928
  }
6929
+ const byPNonFastO = ok !== null ? facts.__byPNonFastO.get(pk) || null : null;
6783
6930
 
6784
- let exact = null;
6785
- if (byPS && byPO) exact = byPS.length <= byPO.length ? byPS : byPO;
6786
- else if (byPS) exact = byPS;
6787
- else if (byPO) exact = byPO;
6788
- else exact = facts.__byPred.get(pk) || null;
6931
+ const exact = selectPositionIndexedCandidates(
6932
+ facts.__byPred.get(pk) || null,
6933
+ byPS,
6934
+ byPNonFastS,
6935
+ sk,
6936
+ byPO,
6937
+ byPNonFastO,
6938
+ ok,
6939
+ );
6789
6940
 
6790
- /** @type {number[] | null} */
6791
- let wildPS = null;
6792
- if (sk !== null) wildPS = facts.__wildPS.get(sk) || null;
6941
+ let varPredPS = null;
6942
+ if (sk !== null) varPredPS = facts.__varPredPS.get(sk) || null;
6793
6943
 
6794
- /** @type {number[] | null} */
6795
- let wildPO = null;
6796
- if (ok !== null) wildPO = facts.__wildPO.get(ok) || null;
6944
+ let varPredPO = null;
6945
+ if (ok !== null) varPredPO = facts.__varPredPO.get(ok) || null;
6797
6946
 
6798
- let wild = null;
6799
- if (wildPS && wildPO) wild = wildPS.length <= wildPO.length ? wildPS : wildPO;
6800
- else if (wildPS) wild = wildPS;
6801
- else if (wildPO) wild = wildPO;
6802
- else wild = facts.__wildPred.length ? facts.__wildPred : null;
6947
+ const wild = selectPositionIndexedCandidates(
6948
+ facts.__varPred,
6949
+ varPredPS,
6950
+ sk !== null ? facts.__varPredNonFastS : null,
6951
+ sk,
6952
+ varPredPO,
6953
+ ok !== null ? facts.__varPredNonFastO : null,
6954
+ ok,
6955
+ );
6803
6956
 
6804
6957
  return {
6805
- exact: exact || null,
6806
- wild: wild || null,
6807
- exactLen: exact ? exact.length : 0,
6808
- wildLen: wild ? wild.length : 0,
6809
- totalLen: (exact ? exact.length : 0) + (wild ? wild.length : 0),
6958
+ exact,
6959
+ wild,
6960
+ exactLen: exact.length,
6961
+ wildLen: wild.length,
6962
+ totalLen: exact.length + wild.length,
6810
6963
  };
6811
6964
  }
6812
6965
 
@@ -6824,20 +6977,28 @@ function hasFactIndexed(facts, tr) {
6824
6977
 
6825
6978
  if (tr.p instanceof Iri) {
6826
6979
  const pk = tr.p.__tid;
6980
+ const sk = termLookupKey(tr.s);
6981
+ let best = null;
6827
6982
 
6828
- const ok = termFastKey(tr.o);
6983
+ if (sk !== null) {
6984
+ const ps = facts.__byPS.get(pk);
6985
+ if (!ps) return false;
6986
+ const psb = ps.get(sk);
6987
+ if (!psb || psb.length === 0) return false;
6988
+ best = psb;
6989
+ }
6990
+
6991
+ const ok = termLookupKey(tr.o);
6829
6992
  if (ok !== null) {
6830
6993
  const po = facts.__byPO.get(pk);
6831
- if (po) {
6832
- const pob = po.get(ok) || [];
6833
- // Facts are all in the same graph. Different blank node labels represent
6834
- // different existentials unless explicitly connected. Do NOT treat
6835
- // triples as duplicates modulo blank renaming, or you'll incorrectly
6836
- // drop facts like: _:sk_0 :x 8.0 (because _:b8 :x 8.0 exists).
6837
- return pob.some((i) => triplesEqual(facts[i], tr));
6838
- }
6994
+ if (!po) return false;
6995
+ const pob = po.get(ok);
6996
+ if (!pob || pob.length === 0) return false;
6997
+ if (!best || pob.length < best.length) best = pob;
6839
6998
  }
6840
6999
 
7000
+ if (best) return best.some((i) => triplesEqual(facts[i], tr));
7001
+
6841
7002
  const pb = facts.__byPred.get(pk) || [];
6842
7003
  return pb.some((i) => triplesEqual(facts[i], tr));
6843
7004
  }
@@ -6946,11 +7107,25 @@ function mergeSinglePremiseAgendaBuckets() {
6946
7107
  return out;
6947
7108
  }
6948
7109
 
7110
+ function termContainsVarForAgenda(t) {
7111
+ if (t instanceof Var) return true;
7112
+ if (t instanceof ListTerm) return t.elems.some(termContainsVarForAgenda);
7113
+ if (t instanceof OpenListTerm) return true;
7114
+ if (t instanceof GraphTerm)
7115
+ return t.triples.some(
7116
+ (tr) =>
7117
+ termContainsVarForAgenda(tr.s) || termContainsVarForAgenda(tr.p) || termContainsVarForAgenda(tr.o),
7118
+ );
7119
+ return false;
7120
+ }
7121
+
6949
7122
  function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
6950
7123
  const index = {
6951
7124
  byPred: new Map(),
7125
+ byPredAll: new Map(),
6952
7126
  byPS: new Map(),
6953
7127
  byPO: new Map(),
7128
+ allIriPred: [],
6954
7129
  wildPred: [],
6955
7130
  wildPS: new Map(),
6956
7131
  wildPO: new Map(),
@@ -6972,8 +7147,8 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
6972
7147
  if (!isSinglePremiseAgendaRuleSafe(r, backRules)) continue;
6973
7148
 
6974
7149
  const goal = r.premise[0];
6975
- const goalSKey = termFastKey(goal.s);
6976
- const goalOKey = termFastKey(goal.o);
7150
+ const goalSKey = termLookupKey(goal.s);
7151
+ const goalOKey = termLookupKey(goal.o);
6977
7152
  const fastSubjectVar = goal.p instanceof Iri && goal.s instanceof Var && goalOKey !== null ? goal.s.name : null;
6978
7153
  const fastObjectVar = goal.p instanceof Iri && goal.o instanceof Var && goalSKey !== null ? goal.o.name : null;
6979
7154
  const entry = {
@@ -6992,6 +7167,8 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
6992
7167
  index.size += 1;
6993
7168
 
6994
7169
  if (entry.goalPredTid !== null) {
7170
+ addToMapArray(index.byPredAll, entry.goalPredTid, entry);
7171
+ index.allIriPred.push(entry);
6995
7172
  if (entry.goalSKey === null && entry.goalOKey === null) addToMapArray(index.byPred, entry.goalPredTid, entry);
6996
7173
  if (entry.goalSKey !== null) {
6997
7174
  let ps = index.byPS.get(entry.goalPredTid);
@@ -7022,25 +7199,37 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
7022
7199
  function getSinglePremiseAgendaCandidates(index, fact) {
7023
7200
  if (!index || index.size === 0) return null;
7024
7201
 
7025
- const sk = termFastKey(fact.s);
7026
- const ok = termFastKey(fact.o);
7202
+ const sk = termLookupKey(fact.s);
7203
+ const ok = termLookupKey(fact.o);
7027
7204
 
7028
7205
  let exact = null;
7029
7206
  if (fact.p instanceof Iri) {
7030
7207
  const pk = fact.p.__tid;
7031
- const byPred = index.byPred.get(pk) || null;
7032
- let byPS = null;
7033
- if (sk !== null) {
7208
+ if ((sk === null && termContainsVarForAgenda(fact.s)) || (ok === null && termContainsVarForAgenda(fact.o))) {
7209
+ // A fact with a variable-bearing subject/object (most importantly a
7210
+ // top-level variable fact such as `?S :p ?O.`) can match rules whose
7211
+ // premise is fixed in that position. The ordinary `(p,s)` / `(p,o)` lookup
7212
+ // would miss those rules, so fall back to all agenda-indexed rules for
7213
+ // this predicate. Do not do this merely for non-fast quoted formulas:
7214
+ // they are not wildcards, and broad fallback would over-fire rules that
7215
+ // rely on protected blank-node/formula unification semantics.
7216
+ exact = index.byPredAll.get(pk) || null;
7217
+ } else {
7218
+ const byPred = index.byPred.get(pk) || null;
7219
+ let byPS = null;
7034
7220
  const ps = index.byPS.get(pk);
7035
7221
  if (ps) byPS = ps.get(sk) || null;
7036
- }
7037
- let byPO = null;
7038
- if (ok !== null) {
7222
+ let byPO = null;
7039
7223
  const po = index.byPO.get(pk);
7040
7224
  if (po) byPO = po.get(ok) || null;
7041
- }
7042
7225
 
7043
- exact = mergeSinglePremiseAgendaBuckets(byPred, byPS, byPO);
7226
+ exact = mergeSinglePremiseAgendaBuckets(byPred, byPS, byPO);
7227
+ }
7228
+ } else if (fact.p instanceof Var) {
7229
+ // A variable-predicate fact can match any IRI-predicate agenda rule.
7230
+ // This is deliberately broad and relies on final unification below; such
7231
+ // facts are uncommon and correctness matters more than over-indexing them.
7232
+ exact = index.allIriPred.length ? index.allIriPred : null;
7044
7233
  }
7045
7234
 
7046
7235
  const wildPred = index.wildPred.length ? index.wildPred : null;
@@ -9647,15 +9836,6 @@ function isIdentChar(c) {
9647
9836
  return c === ':' || isPnChars(c);
9648
9837
  }
9649
9838
 
9650
- function canContinueAfterDot(next) {
9651
- // PN_LOCAL allows '.' but it cannot appear at the end.
9652
- // We include '.' only if it is followed by something that could continue a name.
9653
- if (next === null) return false;
9654
- if (isIdentChar(next)) return true;
9655
- if (next === '%' || next === '\\') return true;
9656
- return false;
9657
- }
9658
-
9659
9839
  function isForbiddenNoncharacterCodePoint(cp) {
9660
9840
  return (cp & 0xffff) === 0xfffe || (cp & 0xffff) === 0xffff;
9661
9841
  }