eyeling 1.16.2 → 1.16.3

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/eyeling.js CHANGED
@@ -4055,10 +4055,12 @@ function main() {
4055
4055
  // collect log:outputString triples from the instantiated query conclusions.
4056
4056
  let outTriples;
4057
4057
  if (hasQueries) {
4058
- const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules);
4058
+ const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules, null, {
4059
+ captureExplanations: engine.getProofCommentsEnabled(),
4060
+ });
4059
4061
  outTriples = res.queryTriples;
4060
4062
  } else {
4061
- engine.forwardChain(facts, frules, brules);
4063
+ engine.forwardChain(facts, frules, brules, null, { captureExplanations: false });
4062
4064
  outTriples = facts;
4063
4065
  }
4064
4066
 
@@ -4169,15 +4171,21 @@ function main() {
4169
4171
  }
4170
4172
  if (entries.length) console.log();
4171
4173
 
4172
- engine.forwardChain(facts, frules, brules, (df) => {
4173
- if (engine.getProofCommentsEnabled()) {
4174
- engine.printExplanation(df, outPrefixes);
4175
- console.log(engine.tripleToN3(df.fact, outPrefixes));
4176
- console.log();
4177
- } else {
4178
- console.log(engine.tripleToN3(df.fact, outPrefixes));
4179
- }
4180
- });
4174
+ engine.forwardChain(
4175
+ facts,
4176
+ frules,
4177
+ brules,
4178
+ (df) => {
4179
+ if (engine.getProofCommentsEnabled()) {
4180
+ engine.printExplanation(df, outPrefixes);
4181
+ console.log(engine.tripleToN3(df.fact, outPrefixes));
4182
+ console.log();
4183
+ } else {
4184
+ console.log(engine.tripleToN3(df.fact, outPrefixes));
4185
+ }
4186
+ },
4187
+ { captureExplanations: engine.getProofCommentsEnabled() },
4188
+ );
4181
4189
  return;
4182
4190
  }
4183
4191
 
@@ -4197,7 +4205,9 @@ function main() {
4197
4205
  outTriples = res.queryTriples;
4198
4206
  outDerived = res.queryDerived;
4199
4207
  } else {
4200
- derived = engine.forwardChain(facts, frules, brules);
4208
+ derived = engine.forwardChain(facts, frules, brules, null, {
4209
+ captureExplanations: engine.getProofCommentsEnabled(),
4210
+ });
4201
4211
  outDerived = derived;
4202
4212
  outTriples = derived.map((df) => df.fact);
4203
4213
  }
@@ -4726,7 +4736,7 @@ const {
4726
4736
  // In N3/Turtle, rdf:nil is the canonical IRI for the empty RDF list.
4727
4737
  // Eyeling represents list literals with ListTerm; ensure rdf:nil unifies with ().
4728
4738
  const RDF_NIL_IRI = RDF_NS + 'nil';
4729
- const __EMPTY_LIST = new ListTerm([]);
4739
+ const EMPTY_LIST_TERM = new ListTerm([]);
4730
4740
 
4731
4741
  const { lex, N3SyntaxError } = require('./lexer');
4732
4742
  const { Parser } = require('./parser');
@@ -4768,13 +4778,13 @@ let version = 'dev';
4768
4778
  try {
4769
4779
  // Node: keep package.json version if available
4770
4780
  if (typeof require === 'function') version = require('./package.json').version || version;
4771
- } catch (_) {}
4781
+ } catch {}
4772
4782
 
4773
4783
  let nodeCrypto = null;
4774
4784
  try {
4775
4785
  // Node: crypto available
4776
4786
  if (typeof require === 'function') nodeCrypto = require('crypto');
4777
- } catch (_) {}
4787
+ } catch {}
4778
4788
  // For a single reasoning run, this maps a canonical representation
4779
4789
  // of the subject term in log:skolem to a Skolem IRI.
4780
4790
  const skolemCache = new Map();
@@ -4786,10 +4796,10 @@ const skolemCache = new Map();
4786
4796
  // - Across reasoning runs (default): same subject -> different Skolem IRI.
4787
4797
  // - Optional legacy mode: stable across runs (CLI: --deterministic-skolem).
4788
4798
  let deterministicSkolemAcrossRuns = false;
4789
- let __skolemRunDepth = 0;
4790
- let __skolemRunSalt = null;
4799
+ let skolemRunDepth = 0;
4800
+ let skolemRunSalt = null;
4791
4801
 
4792
- function __makeSkolemRunSalt() {
4802
+ function makeSkolemRunSalt() {
4793
4803
  // Prefer WebCrypto if present (browser/worker)
4794
4804
  try {
4795
4805
  const g = typeof globalThis !== 'undefined' ? globalThis : null;
@@ -4803,7 +4813,7 @@ function __makeSkolemRunSalt() {
4803
4813
  .join('');
4804
4814
  }
4805
4815
  }
4806
- } catch (_) {}
4816
+ } catch {}
4807
4817
 
4808
4818
  // Node.js crypto
4809
4819
  try {
@@ -4811,7 +4821,7 @@ function __makeSkolemRunSalt() {
4811
4821
  if (typeof nodeCrypto.randomUUID === 'function') return nodeCrypto.randomUUID();
4812
4822
  if (typeof nodeCrypto.randomBytes === 'function') return nodeCrypto.randomBytes(16).toString('hex');
4813
4823
  }
4814
- } catch (_) {}
4824
+ } catch {}
4815
4825
 
4816
4826
  // Last-resort fallback (not cryptographically strong)
4817
4827
  return (
@@ -4819,30 +4829,30 @@ function __makeSkolemRunSalt() {
4819
4829
  );
4820
4830
  }
4821
4831
 
4822
- function __enterReasoningRun() {
4823
- __skolemRunDepth += 1;
4824
- if (__skolemRunDepth === 1) {
4832
+ function enterReasoningRun() {
4833
+ skolemRunDepth += 1;
4834
+ if (skolemRunDepth === 1) {
4825
4835
  skolemCache.clear();
4826
- __skolemRunSalt = deterministicSkolemAcrossRuns ? '' : __makeSkolemRunSalt();
4836
+ skolemRunSalt = deterministicSkolemAcrossRuns ? '' : makeSkolemRunSalt();
4827
4837
  }
4828
4838
  }
4829
4839
 
4830
- function __exitReasoningRun() {
4831
- if (__skolemRunDepth > 0) __skolemRunDepth -= 1;
4832
- if (__skolemRunDepth === 0) {
4840
+ function exitReasoningRun() {
4841
+ if (skolemRunDepth > 0) skolemRunDepth -= 1;
4842
+ if (skolemRunDepth === 0) {
4833
4843
  // Clear the salt so a future top-level run gets a fresh one (default mode).
4834
- __skolemRunSalt = null;
4844
+ skolemRunSalt = null;
4835
4845
  }
4836
4846
  }
4837
4847
 
4838
- function __skolemIdForKey(key) {
4848
+ function skolemIdForKey(key) {
4839
4849
  if (deterministicSkolemAcrossRuns) return deterministicSkolemIdFromKey(key);
4840
4850
  // Ensure we have a run salt even if log:skolem is invoked outside forwardChain().
4841
- if (__skolemRunSalt === null) {
4851
+ if (skolemRunSalt === null) {
4842
4852
  skolemCache.clear();
4843
- __skolemRunSalt = __makeSkolemRunSalt();
4853
+ skolemRunSalt = makeSkolemRunSalt();
4844
4854
  }
4845
- return deterministicSkolemIdFromKey(__skolemRunSalt + '|' + key);
4855
+ return deterministicSkolemIdFromKey(skolemRunSalt + '|' + key);
4846
4856
  }
4847
4857
 
4848
4858
  function getDeterministicSkolemEnabled() {
@@ -4852,8 +4862,8 @@ function getDeterministicSkolemEnabled() {
4852
4862
  function setDeterministicSkolemEnabled(v) {
4853
4863
  deterministicSkolemAcrossRuns = !!v;
4854
4864
  // Reset per-run state so the new mode takes effect immediately for the next run.
4855
- if (__skolemRunDepth === 0) {
4856
- __skolemRunSalt = null;
4865
+ if (skolemRunDepth === 0) {
4866
+ skolemRunSalt = null;
4857
4867
  skolemCache.clear();
4858
4868
  }
4859
4869
  }
@@ -4939,6 +4949,7 @@ function __computeHeadIsStrictGround(r) {
4939
4949
  if (r.isFuse) return false;
4940
4950
  // Dynamic heads depend on runtime bindings; treat as non-ground.
4941
4951
  if (r.__dynamicConclusionTerm) return false;
4952
+ if (r.__fromRulePromotion) return false;
4942
4953
  if (r.headBlankLabels && r.headBlankLabels.size) return false;
4943
4954
  for (const tr of r.conclusion) if (!__isStrictGroundTriple(tr)) return false;
4944
4955
  return true;
@@ -5814,10 +5825,13 @@ function candidateFacts(facts, goal) {
5814
5825
  else if (wildPO) wild = wildPO;
5815
5826
  else wild = facts.__wildPred.length ? facts.__wildPred : null;
5816
5827
 
5817
- if (exact && wild) return exact.concat(wild);
5818
- if (exact) return exact;
5819
- if (wild) return wild;
5820
- return [];
5828
+ return {
5829
+ exact: exact || null,
5830
+ wild: wild || null,
5831
+ exactLen: exact ? exact.length : 0,
5832
+ wildLen: wild ? wild.length : 0,
5833
+ totalLen: (exact ? exact.length : 0) + (wild ? wild.length : 0),
5834
+ };
5821
5835
  }
5822
5836
 
5823
5837
  return null;
@@ -5860,6 +5874,11 @@ function pushFactIndexed(facts, tr) {
5860
5874
  indexFact(facts, tr, idx);
5861
5875
  }
5862
5876
 
5877
+ function makeDerivedRecord(fact, rule, premises, subst, captureExplanations) {
5878
+ if (captureExplanations === false) return { fact };
5879
+ return new DerivedFact(fact, rule, premises.slice(), { ...subst });
5880
+ }
5881
+
5863
5882
  function ensureBackRuleIndexes(backRules) {
5864
5883
  if (backRules.__byHeadPred && backRules.__wildHeadPred) return;
5865
5884
 
@@ -5893,6 +5912,164 @@ function indexBackRule(backRules, r) {
5893
5912
  }
5894
5913
  }
5895
5914
 
5915
+ function isSinglePremiseAgendaRuleSafe(r, backRules) {
5916
+ if (!r || r.isFuse || !Array.isArray(r.premise) || r.premise.length !== 1) return false;
5917
+
5918
+ // Keep agenda firing restricted to rules whose observable output order is
5919
+ // already stable in the legacy engine. Dynamic heads and head-blank
5920
+ // skolemization are deliberately left on the old path so example outputs keep
5921
+ // the same derived blank labels and rule-promotion behavior.
5922
+ if (r.__dynamicConclusionTerm) return false;
5923
+ if (r.__fromRulePromotion) return false;
5924
+ if (r.headBlankLabels && r.headBlankLabels.size) return false;
5925
+
5926
+ const goal = r.premise[0];
5927
+
5928
+ // Builtin-only bodies need the normal proveGoals path because they can
5929
+ // succeed without matching an extensional fact and may depend on scoped state.
5930
+ if (isBuiltinPred(goal.p)) return false;
5931
+
5932
+ // Safe only when the sole premise cannot be satisfied via backward rules.
5933
+ // Otherwise matching just against newly-seen facts would be incomplete.
5934
+ ensureBackRuleIndexes(backRules);
5935
+ if (goal.p instanceof Iri) {
5936
+ if ((backRules.__byHeadPred.get(goal.p.__tid) || []).length) return false;
5937
+ if (backRules.__wildHeadPred.length) return false;
5938
+ return true;
5939
+ }
5940
+
5941
+ return backRules.__wildHeadPred.length === 0;
5942
+ }
5943
+
5944
+ function mergeSinglePremiseAgendaBuckets() {
5945
+ let out = null;
5946
+ let seen = null;
5947
+
5948
+ for (let i = 0; i < arguments.length; i++) {
5949
+ const bucket = arguments[i];
5950
+ if (!bucket || bucket.length === 0) continue;
5951
+
5952
+ if (out === null) {
5953
+ out = bucket.length === 1 ? [bucket[0]] : bucket.slice();
5954
+ if (bucket.length > 1) seen = new Set(out);
5955
+ continue;
5956
+ }
5957
+
5958
+ if (!seen) seen = new Set(out);
5959
+ for (let j = 0; j < bucket.length; j++) {
5960
+ const entry = bucket[j];
5961
+ if (seen.has(entry)) continue;
5962
+ seen.add(entry);
5963
+ out.push(entry);
5964
+ }
5965
+ }
5966
+
5967
+ return out;
5968
+ }
5969
+
5970
+ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
5971
+ const index = {
5972
+ byPred: new Map(),
5973
+ byPS: new Map(),
5974
+ byPO: new Map(),
5975
+ wildPred: [],
5976
+ wildPS: new Map(),
5977
+ wildPO: new Map(),
5978
+ indexed: new Set(),
5979
+ size: 0,
5980
+ };
5981
+
5982
+ function addToMapArray(m, k, v) {
5983
+ let bucket = m.get(k);
5984
+ if (!bucket) {
5985
+ bucket = [];
5986
+ m.set(k, bucket);
5987
+ }
5988
+ bucket.push(v);
5989
+ }
5990
+
5991
+ for (let i = 0; i < forwardRules.length; i++) {
5992
+ const r = forwardRules[i];
5993
+ if (!isSinglePremiseAgendaRuleSafe(r, backRules)) continue;
5994
+
5995
+ const goal = r.premise[0];
5996
+ const entry = {
5997
+ rule: r,
5998
+ ruleIndex: i,
5999
+ goal,
6000
+ goalPredTid: goal.p instanceof Iri ? goal.p.__tid : null,
6001
+ goalSKey: termFastKey(goal.s),
6002
+ goalOKey: termFastKey(goal.o),
6003
+ };
6004
+
6005
+ index.indexed.add(r);
6006
+ index.size += 1;
6007
+
6008
+ if (entry.goalPredTid !== null) {
6009
+ if (entry.goalSKey === null && entry.goalOKey === null) addToMapArray(index.byPred, entry.goalPredTid, entry);
6010
+ if (entry.goalSKey !== null) {
6011
+ let ps = index.byPS.get(entry.goalPredTid);
6012
+ if (!ps) {
6013
+ ps = new Map();
6014
+ index.byPS.set(entry.goalPredTid, ps);
6015
+ }
6016
+ addToMapArray(ps, entry.goalSKey, entry);
6017
+ }
6018
+ if (entry.goalOKey !== null) {
6019
+ let po = index.byPO.get(entry.goalPredTid);
6020
+ if (!po) {
6021
+ po = new Map();
6022
+ index.byPO.set(entry.goalPredTid, po);
6023
+ }
6024
+ addToMapArray(po, entry.goalOKey, entry);
6025
+ }
6026
+ } else {
6027
+ if (entry.goalSKey === null && entry.goalOKey === null) index.wildPred.push(entry);
6028
+ if (entry.goalSKey !== null) addToMapArray(index.wildPS, entry.goalSKey, entry);
6029
+ if (entry.goalOKey !== null) addToMapArray(index.wildPO, entry.goalOKey, entry);
6030
+ }
6031
+ }
6032
+
6033
+ return index;
6034
+ }
6035
+
6036
+ function getSinglePremiseAgendaCandidates(index, fact) {
6037
+ if (!index || index.size === 0) return null;
6038
+
6039
+ const sk = termFastKey(fact.s);
6040
+ const ok = termFastKey(fact.o);
6041
+
6042
+ let exact = null;
6043
+ if (fact.p instanceof Iri) {
6044
+ const pk = fact.p.__tid;
6045
+ const byPred = index.byPred.get(pk) || null;
6046
+ let byPS = null;
6047
+ if (sk !== null) {
6048
+ const ps = index.byPS.get(pk);
6049
+ if (ps) byPS = ps.get(sk) || null;
6050
+ }
6051
+ let byPO = null;
6052
+ if (ok !== null) {
6053
+ const po = index.byPO.get(pk);
6054
+ if (po) byPO = po.get(ok) || null;
6055
+ }
6056
+
6057
+ exact = mergeSinglePremiseAgendaBuckets(byPred, byPS, byPO);
6058
+ }
6059
+
6060
+ const wildPred = index.wildPred.length ? index.wildPred : null;
6061
+ let wildPS = null;
6062
+ if (sk !== null) wildPS = index.wildPS.get(sk) || null;
6063
+
6064
+ let wildPO = null;
6065
+ if (ok !== null) wildPO = index.wildPO.get(ok) || null;
6066
+
6067
+ const wild = mergeSinglePremiseAgendaBuckets(wildPred, wildPS, wildPO);
6068
+
6069
+ if (!exact && !wild) return null;
6070
+ return { exact, wild, exactLen: exact ? exact.length : 0, wildLen: wild ? wild.length : 0 };
6071
+ }
6072
+
5896
6073
  // ===========================================================================
5897
6074
  // Special predicate helpers
5898
6075
  // ===========================================================================
@@ -5918,7 +6095,7 @@ function isLogImpliedBy(p) {
5918
6095
  // So this improves reuse across repeated backward proofs without changing the
5919
6096
  // semantics of recursive goals.
5920
6097
 
5921
- function __goalTableScopeVersion(facts, backRules) {
6098
+ function goalTableScopeVersion(facts, backRules) {
5922
6099
  const factCount = Array.isArray(facts) ? facts.length : 0;
5923
6100
  const backRuleCount = Array.isArray(backRules) ? backRules.length : 0;
5924
6101
  const scopedLevel = facts && typeof facts.__scopedClosureLevel === 'number' ? facts.__scopedClosureLevel : 0;
@@ -5935,26 +6112,26 @@ function __makeGoalTable() {
5935
6112
 
5936
6113
  function __attachGoalTable(scopeCarrier, goalTable) {
5937
6114
  if (!scopeCarrier) return goalTable;
5938
- if (!hasOwn.call(scopeCarrier, '__goalTable')) {
5939
- Object.defineProperty(scopeCarrier, '__goalTable', {
6115
+ if (!hasOwn.call(scopeCarrier, 'goalTable')) {
6116
+ Object.defineProperty(scopeCarrier, 'goalTable', {
5940
6117
  value: goalTable,
5941
6118
  enumerable: false,
5942
6119
  writable: true,
5943
6120
  configurable: true,
5944
6121
  });
5945
6122
  } else {
5946
- scopeCarrier.__goalTable = goalTable;
6123
+ scopeCarrier.goalTable = goalTable;
5947
6124
  }
5948
6125
  return goalTable;
5949
6126
  }
5950
6127
 
5951
6128
  function __ensureGoalTable(facts, backRules) {
5952
- let table = (facts && facts.__goalTable) || (backRules && backRules.__goalTable) || null;
6129
+ let table = (facts && facts.goalTable) || (backRules && backRules.goalTable) || null;
5953
6130
  if (!table) table = __makeGoalTable();
5954
6131
  __attachGoalTable(facts, table);
5955
6132
  __attachGoalTable(backRules, table);
5956
6133
 
5957
- const version = __goalTableScopeVersion(facts, backRules);
6134
+ const version = goalTableScopeVersion(facts, backRules);
5958
6135
  if (table.scopeVersion !== version) {
5959
6136
  table.scopeVersion = version;
5960
6137
  table.entries.clear();
@@ -6000,6 +6177,7 @@ function __canStoreGoalMemo(visited, maxResults) {
6000
6177
  // ===========================================================================
6001
6178
 
6002
6179
  function containsVarTerm(t, v) {
6180
+ if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return false;
6003
6181
  if (t instanceof Var) return t.name === v;
6004
6182
  if (t instanceof ListTerm) return t.elems.some((e) => containsVarTerm(e, v));
6005
6183
  if (t instanceof OpenListTerm) return t.prefix.some((e) => containsVarTerm(e, v)) || t.tailVar === v;
@@ -6023,6 +6201,7 @@ function isGroundTripleInGraph(tr) {
6023
6201
  }
6024
6202
 
6025
6203
  function isGroundTerm(t) {
6204
+ if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return true;
6026
6205
  if (t instanceof Var) return false;
6027
6206
  if (t instanceof ListTerm) return t.elems.every((e) => isGroundTerm(e));
6028
6207
  if (t instanceof OpenListTerm) return false;
@@ -6056,7 +6235,7 @@ function skolemIriFromGroundTerm(t) {
6056
6235
  const key = skolemKeyFromTerm(t);
6057
6236
  let iri = skolemCache.get(key);
6058
6237
  if (!iri) {
6059
- const id = __skolemIdForKey(key);
6238
+ const id = skolemIdForKey(key);
6060
6239
  iri = internIri(SKOLEM_NS + id);
6061
6240
  skolemCache.set(key, iri);
6062
6241
  }
@@ -6064,6 +6243,9 @@ function skolemIriFromGroundTerm(t) {
6064
6243
  }
6065
6244
 
6066
6245
  function applySubstTerm(t, s) {
6246
+ // Hot fast path: most terms are already-ground atomic terms.
6247
+ if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return t;
6248
+
6067
6249
  // Common case: variable
6068
6250
  if (t instanceof Var) {
6069
6251
  const first = s[t.name];
@@ -6258,8 +6440,8 @@ function unifyTermWithOptions(a, b, subst, opts) {
6258
6440
 
6259
6441
  // Normalize rdf:nil IRI to the empty list term, so it unifies with () and
6260
6442
  // list builtins treat it consistently.
6261
- if (a instanceof Iri && a.value === RDF_NIL_IRI) a = __EMPTY_LIST;
6262
- if (b instanceof Iri && b.value === RDF_NIL_IRI) b = __EMPTY_LIST;
6443
+ if (a instanceof Iri && a.value === RDF_NIL_IRI) a = EMPTY_LIST_TERM;
6444
+ if (b instanceof Iri && b.value === RDF_NIL_IRI) b = EMPTY_LIST_TERM;
6263
6445
 
6264
6446
  // Variable binding
6265
6447
  if (a instanceof Var) {
@@ -6489,10 +6671,10 @@ function __builtinIsSatisfiableWhenFullyUnbound(pIriVal) {
6489
6671
  }
6490
6672
 
6491
6673
  function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxResults, opts) {
6492
- const __goalTable = __canLookupGoalMemo(visited) ? __ensureGoalTable(facts, backRules) : null;
6493
- const __goalMemoKeyNow = __goalTable ? __goalMemoKey(goals, subst, facts, opts) : null;
6494
- if (__goalTable && __goalTable.entries.has(__goalMemoKeyNow)) {
6495
- const cached = __goalTable.entries.get(__goalMemoKeyNow) || [];
6674
+ const goalTable = __canLookupGoalMemo(visited) ? __ensureGoalTable(facts, backRules) : null;
6675
+ const goalMemoKeyNow = goalTable ? __goalMemoKey(goals, subst, facts, opts) : null;
6676
+ if (goalTable && goalTable.entries.has(goalMemoKeyNow)) {
6677
+ const cached = goalTable.entries.get(goalMemoKeyNow) || [];
6496
6678
  const cloned = __cloneGoalSolutions(cached);
6497
6679
  if (typeof maxResults === 'number' && maxResults > 0 && cloned.length > maxResults)
6498
6680
  return cloned.slice(0, maxResults);
@@ -6511,7 +6693,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6511
6693
 
6512
6694
  // IMPORTANT: Goal reordering / deferral is only enabled when explicitly
6513
6695
  // requested by the caller (used for forward rules).
6514
- const __allowDeferBuiltins = !!(opts && opts.deferBuiltins);
6696
+ const allowDeferredBuiltins = !!(opts && opts.deferBuiltins);
6515
6697
 
6516
6698
  const initialGoals = Array.isArray(goals) ? goals.slice() : [];
6517
6699
  const substMut = subst ? { ...subst } : {};
@@ -6526,8 +6708,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6526
6708
 
6527
6709
  if (!initialGoals.length) {
6528
6710
  results.push(gcCompactForGoals(substMut, [], answerVars));
6529
- if (__goalTable && __canStoreGoalMemo(visited, maxResults)) {
6530
- __goalTable.entries.set(__goalMemoKeyNow, __cloneGoalSolutions(results));
6711
+ if (goalTable && __canStoreGoalMemo(visited, maxResults)) {
6712
+ goalTable.entries.set(goalMemoKeyNow, __cloneGoalSolutions(results));
6531
6713
  }
6532
6714
  return results;
6533
6715
  }
@@ -6567,14 +6749,14 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6567
6749
  const visitedCounts = new Map(); // key -> count
6568
6750
  const visitedTrail = []; // stack of keys in insertion order
6569
6751
 
6570
- const __termKeyCache = typeof WeakMap === 'function' ? new WeakMap() : null;
6752
+ const termKeyCache = typeof WeakMap === 'function' ? new WeakMap() : null;
6571
6753
 
6572
- function __termKeyForVisited(t) {
6754
+ function termKeyForVisited(t) {
6573
6755
  if (t instanceof Iri && t.value === RDF_NIL_IRI) return '()';
6574
6756
  if (t instanceof ListTerm && t.elems.length === 0) return '()';
6575
6757
 
6576
- if (__termKeyCache && t && typeof t === 'object') {
6577
- const cached = __termKeyCache.get(t);
6758
+ if (termKeyCache && t && typeof t === 'object') {
6759
+ const cached = termKeyCache.get(t);
6578
6760
  if (cached) return cached;
6579
6761
  }
6580
6762
 
@@ -6605,14 +6787,14 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6605
6787
  // Iri / Blank and other atomic interned terms
6606
6788
  out = 'T' + t.__tid;
6607
6789
  } else if (t instanceof ListTerm) {
6608
- out = '[' + t.elems.map(__termKeyForVisited).join(',') + ']';
6790
+ out = '[' + t.elems.map(termKeyForVisited).join(',') + ']';
6609
6791
  } else if (t instanceof OpenListTerm) {
6610
- out = '[open:' + t.prefix.map(__termKeyForVisited).join(',') + '|tail:' + t.tailVar + ']';
6792
+ out = '[open:' + t.prefix.map(termKeyForVisited).join(',') + '|tail:' + t.tailVar + ']';
6611
6793
  } else if (t instanceof GraphTerm) {
6612
6794
  out =
6613
6795
  '{' +
6614
6796
  t.triples
6615
- .map((tr) => __termKeyForVisited(tr.s) + ' ' + __termKeyForVisited(tr.p) + ' ' + __termKeyForVisited(tr.o))
6797
+ .map((tr) => termKeyForVisited(tr.s) + ' ' + termKeyForVisited(tr.p) + ' ' + termKeyForVisited(tr.o))
6616
6798
  .join(';') +
6617
6799
  '}';
6618
6800
  } else {
@@ -6620,20 +6802,20 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6620
6802
  out = skolemKeyFromTerm(t);
6621
6803
  }
6622
6804
 
6623
- if (__termKeyCache && t && typeof t === 'object') __termKeyCache.set(t, out);
6805
+ if (termKeyCache && t && typeof t === 'object') termKeyCache.set(t, out);
6624
6806
  return out;
6625
6807
  }
6626
6808
 
6627
- function __tripleKeyForVisited(tr) {
6628
- return __termKeyForVisited(tr.s) + '\t' + __termKeyForVisited(tr.p) + '\t' + __termKeyForVisited(tr.o);
6809
+ function tripleKeyForVisited(tr) {
6810
+ return termKeyForVisited(tr.s) + '\t' + termKeyForVisited(tr.p) + '\t' + termKeyForVisited(tr.o);
6629
6811
  }
6630
6812
 
6631
- function __pushVisited(key) {
6813
+ function pushVisitedKey(key) {
6632
6814
  visitedTrail.push(key);
6633
6815
  visitedCounts.set(key, (visitedCounts.get(key) || 0) + 1);
6634
6816
  }
6635
6817
 
6636
- function __undoVisitedTo(mark) {
6818
+ function undoVisitedKeysTo(mark) {
6637
6819
  for (let i = visitedTrail.length - 1; i >= mark; i--) {
6638
6820
  const k = visitedTrail[i];
6639
6821
  const c = visitedCounts.get(k);
@@ -6643,7 +6825,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6643
6825
  visitedTrail.length = mark;
6644
6826
  }
6645
6827
 
6646
- for (const tr of initialVisited) __pushVisited(__tripleKeyForVisited(tr));
6828
+ for (const tr of initialVisited) pushVisitedKey(tripleKeyForVisited(tr));
6647
6829
 
6648
6830
  // ---------------------------------------------------------------------------
6649
6831
  // In-place unification into the mutable substitution + trail.
@@ -6675,8 +6857,10 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6675
6857
 
6676
6858
  // Normalize rdf:nil IRI to the empty list term, so it unifies with () and
6677
6859
  // list builtins treat it consistently.
6678
- if (a instanceof Iri && a.value === RDF_NIL_IRI) a = __EMPTY_LIST;
6679
- if (b instanceof Iri && b.value === RDF_NIL_IRI) b = __EMPTY_LIST;
6860
+ if (a instanceof Iri && a.value === RDF_NIL_IRI) a = EMPTY_LIST_TERM;
6861
+ if (b instanceof Iri && b.value === RDF_NIL_IRI) b = EMPTY_LIST_TERM;
6862
+
6863
+ if (a === b) return true;
6680
6864
 
6681
6865
  // Variable binding
6682
6866
  if (a instanceof Var) return bindVarTrail(a.name, b);
@@ -6795,7 +6979,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6795
6979
  kind: 'node',
6796
6980
  goalsNow: initialGoals,
6797
6981
  curDepth: depth || 0,
6798
- canDeferBuiltins: __allowDeferBuiltins,
6982
+ canDeferBuiltins: allowDeferredBuiltins,
6799
6983
  deferCount: 0,
6800
6984
  });
6801
6985
 
@@ -6804,7 +6988,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6804
6988
 
6805
6989
  if (frame.kind === 'undo') {
6806
6990
  undoTo(frame.substMark);
6807
- __undoVisitedTo(frame.visitedMark);
6991
+ undoVisitedKeysTo(frame.visitedMark);
6808
6992
  continue;
6809
6993
  }
6810
6994
 
@@ -6863,15 +7047,15 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6863
7047
  // still preventing trivial non-termination in mutually recursive rule
6864
7048
  // cycles.
6865
7049
  if (frame.goalWasVisited && rStd.premise && rStd.premise.length) {
6866
- let __cycle = false;
7050
+ let hasCycle = false;
6867
7051
  for (let i = 0; i < rStd.premise.length; i++) {
6868
- const premKey = __tripleKeyForVisited(applySubstTriple(rStd.premise[i], substMut));
7052
+ const premKey = tripleKeyForVisited(applySubstTriple(rStd.premise[i], substMut));
6869
7053
  if (visitedCounts.has(premKey)) {
6870
- __cycle = true;
7054
+ hasCycle = true;
6871
7055
  break;
6872
7056
  }
6873
7057
  }
6874
- if (__cycle) {
7058
+ if (hasCycle) {
6875
7059
  undoTo(mark);
6876
7060
  continue;
6877
7061
  }
@@ -6880,7 +7064,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6880
7064
  const newGoals = rStd.premise.concat(frame.restGoals);
6881
7065
 
6882
7066
  const vMark = visitedTrail.length;
6883
- __pushVisited(frame.goalKey);
7067
+ pushVisitedKey(frame.goalKey);
6884
7068
 
6885
7069
  // Explore the rule body; then undo; then resume trying further rules.
6886
7070
  stack.push(frame);
@@ -6902,8 +7086,15 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6902
7086
  const candidates = frame.candidates;
6903
7087
  const isIndexed = !!candidates;
6904
7088
 
6905
- while (frame.idx < (isIndexed ? candidates.length : factsList.length) && results.length < max) {
6906
- const f = isIndexed ? factsList[candidates[frame.idx++]] : factsList[frame.idx++];
7089
+ while (frame.idx < (isIndexed ? candidates.totalLen : factsList.length) && results.length < max) {
7090
+ let f;
7091
+ if (isIndexed) {
7092
+ const idxNow = frame.idx++;
7093
+ if (idxNow < candidates.exactLen) f = factsList[candidates.exact[idxNow]];
7094
+ else f = factsList[candidates.wild[idxNow - candidates.exactLen]];
7095
+ } else {
7096
+ f = factsList[frame.idx++];
7097
+ }
6907
7098
 
6908
7099
  const mark = trail.length;
6909
7100
  if (!unifyTripleTrail(frame.goal0, f)) {
@@ -6946,13 +7137,13 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6946
7137
  const goal0 = applySubstTriple(rawGoal, substMut);
6947
7138
 
6948
7139
  // 1) Builtins
6949
- const __pv0 = goal0.p instanceof Iri ? goal0.p.value : null;
6950
- const __rdfFirstOrRest = __pv0 === RDF_NS + 'first' || __pv0 === RDF_NS + 'rest';
6951
- const __treatBuiltin =
7140
+ const goalPredicateIri = goal0.p instanceof Iri ? goal0.p.value : null;
7141
+ const isRdfFirstOrRest = goalPredicateIri === RDF_NS + 'first' || goalPredicateIri === RDF_NS + 'rest';
7142
+ const shouldTreatAsBuiltin =
6952
7143
  isBuiltinPred(goal0.p) &&
6953
- !(__rdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
7144
+ !(isRdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
6954
7145
 
6955
- if (__treatBuiltin) {
7146
+ if (shouldTreatAsBuiltin) {
6956
7147
  const remaining = max - results.length;
6957
7148
  if (remaining <= 0) continue;
6958
7149
  const builtinMax = Number.isFinite(remaining) && !restGoals.length ? remaining : undefined;
@@ -6960,11 +7151,11 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6960
7151
  let deltas = evalBuiltin(goal0, {}, facts, backRules, frame.curDepth, varGen, builtinMax);
6961
7152
 
6962
7153
  const dc = typeof frame.deferCount === 'number' ? frame.deferCount : 0;
6963
- const __vacuous = deltas.length > 0 && deltas.every((d) => Object.keys(d).length === 0);
7154
+ const builtinDeltasAreVacuous = deltas.length > 0 && deltas.every((d) => Object.keys(d).length === 0);
6964
7155
 
6965
7156
  if (
6966
7157
  frame.canDeferBuiltins &&
6967
- (!deltas.length || __vacuous) &&
7158
+ (!deltas.length || builtinDeltasAreVacuous) &&
6968
7159
  restGoals.length &&
6969
7160
  __tripleHasVarOrBlank(goal0) &&
6970
7161
  dc < goalsNow.length
@@ -6980,14 +7171,14 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6980
7171
  continue;
6981
7172
  }
6982
7173
 
6983
- const __fullyUnboundSO =
7174
+ const subjectAndObjectAreFullyUnbound =
6984
7175
  (goal0.s instanceof Var || goal0.s instanceof Blank) && (goal0.o instanceof Var || goal0.o instanceof Blank);
6985
7176
 
6986
7177
  if (
6987
7178
  frame.canDeferBuiltins &&
6988
7179
  !deltas.length &&
6989
- __builtinIsSatisfiableWhenFullyUnbound(__pv0) &&
6990
- __fullyUnboundSO &&
7180
+ __builtinIsSatisfiableWhenFullyUnbound(goalPredicateIri) &&
7181
+ subjectAndObjectAreFullyUnbound &&
6991
7182
  (!restGoals.length || dc >= goalsNow.length)
6992
7183
  ) {
6993
7184
  deltas = [{}];
@@ -7022,7 +7213,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
7022
7213
  // We therefore *allow* re-entering a visited goal, but when a goal is
7023
7214
  // already visited we avoid applying backward rules whose premises would
7024
7215
  // immediately re-enter any visited goal again (a cheap cycle guard).
7025
- const goalKey = __tripleKeyForVisited(goal0);
7216
+ const goalKey = tripleKeyForVisited(goal0);
7026
7217
  const goalWasVisited = visitedCounts.has(goalKey);
7027
7218
 
7028
7219
  // 3) Backward rules (indexed by head predicate) — explored first
@@ -7072,8 +7263,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
7072
7263
  }
7073
7264
  }
7074
7265
 
7075
- if (__goalTable && __canStoreGoalMemo(visited, maxResults)) {
7076
- __goalTable.entries.set(__goalMemoKeyNow, __cloneGoalSolutions(results));
7266
+ if (goalTable && __canStoreGoalMemo(visited, maxResults)) {
7267
+ goalTable.entries.set(goalMemoKeyNow, __cloneGoalSolutions(results));
7077
7268
  }
7078
7269
 
7079
7270
  return results;
@@ -7083,8 +7274,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
7083
7274
  // Forward chaining to fixpoint
7084
7275
  // ===========================================================================
7085
7276
 
7086
- function forwardChain(facts, forwardRules, backRules, onDerived /* optional */) {
7087
- __enterReasoningRun();
7277
+ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */, opts = {}) {
7278
+ enterReasoningRun();
7088
7279
  try {
7089
7280
  ensureFactIndexes(facts);
7090
7281
  ensureBackRuleIndexes(backRules);
@@ -7093,7 +7284,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
7093
7284
  __attachGoalTable(facts, goalTable);
7094
7285
  __attachGoalTable(backRules, goalTable);
7095
7286
 
7096
- const factList = facts.slice();
7287
+ const captureExplanations = !(opts && opts.captureExplanations === false);
7097
7288
  const derivedForward = [];
7098
7289
  const varGen = [0];
7099
7290
  const skCounter = [0];
@@ -7169,46 +7360,216 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
7169
7360
  return snap;
7170
7361
  }
7171
7362
 
7363
+ function __skipForwardRuleNow(r) {
7364
+ // Skip forward rules that are guaranteed to "delay" due to scoped
7365
+ // builtins (log:collectAllIn / log:forAllIn / log:includes / log:notIncludes)
7366
+ // until a snapshot exists (and a certain closure level is reached).
7367
+ // This prevents expensive proofs that will definitely fail in Phase A
7368
+ // and in early closure levels.
7369
+ const info = r.__scopedSkipInfo;
7370
+ if (info && info.needsSnap) {
7371
+ const snapHere = facts.__scopedSnapshot || null;
7372
+ const lvlHere = (facts && typeof facts.__scopedClosureLevel === 'number' && facts.__scopedClosureLevel) || 0;
7373
+ if (!snapHere) return true;
7374
+ if (lvlHere < info.requiredLevel) return true;
7375
+ }
7376
+
7377
+ // Optimization: if the rule head is **structurally ground** (no vars anywhere, even inside
7378
+ // quoted formulas) and has no head blanks, then the head does not depend on which body
7379
+ // solution we pick. In that case, we only need *one* proof of the body, and once all head
7380
+ // triples are already known we can skip proving the body entirely.
7381
+ const headIsStrictGround = r.__headIsStrictGround;
7382
+ if (headIsStrictGround) {
7383
+ let allKnown = true;
7384
+ for (const tr of r.conclusion) {
7385
+ if (!hasFactIndexed(facts, tr)) {
7386
+ allKnown = false;
7387
+ break;
7388
+ }
7389
+ }
7390
+ if (allKnown) return true;
7391
+ }
7392
+
7393
+ return false;
7394
+ }
7395
+
7396
+ function __emitForwardRuleSolution(r, ruleIndex, s) {
7397
+ let changedHere = false;
7398
+ let rulesChanged = false;
7399
+
7400
+ // IMPORTANT: one skolem map per *rule firing*
7401
+ const skMap = {};
7402
+ const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
7403
+ const fireKey = __firingKey(ruleIndex, instantiatedPremises);
7404
+
7405
+ // Support "dynamic" rule heads where the consequent is a term that
7406
+ // (after substitution) evaluates to a quoted formula.
7407
+ // Example: { :a :b ?C } => ?C.
7408
+ let dynamicHeadTriples = null;
7409
+ let headBlankLabelsHere = r.headBlankLabels;
7410
+ if (r.__dynamicConclusionTerm) {
7411
+ const dynTerm = applySubstTerm(r.__dynamicConclusionTerm, s);
7412
+
7413
+ // Allow dynamic fuses: ... => ?X. where ?X becomes false
7414
+ if (dynTerm instanceof Literal && dynTerm.value === 'false') {
7415
+ console.log('# Inference fuse triggered: dynamic head resolved to false.');
7416
+ process.exit(2);
7417
+ }
7418
+
7419
+ const dynTriples = __graphTriplesOrTrue(dynTerm);
7420
+ dynamicHeadTriples = dynTriples !== null ? dynTriples : [];
7421
+
7422
+ // If the dynamic head contains explicit blank nodes, treat them as
7423
+ // head blanks for skolemization.
7424
+ const dynHeadBlankLabels =
7425
+ dynamicHeadTriples && dynamicHeadTriples.length ? collectBlankLabelsInTriples(dynamicHeadTriples) : null;
7426
+ if (dynHeadBlankLabels && dynHeadBlankLabels.size) {
7427
+ headBlankLabelsHere = new Set([...headBlankLabelsHere, ...dynHeadBlankLabels]);
7428
+ }
7429
+ }
7430
+
7431
+ const headPatterns =
7432
+ dynamicHeadTriples && dynamicHeadTriples.length ? r.conclusion.concat(dynamicHeadTriples) : r.conclusion;
7433
+
7434
+ for (const cpat of headPatterns) {
7435
+ const instantiated = applySubstTriple(cpat, s);
7436
+
7437
+ const subj = instantiated.s;
7438
+ const obj = instantiated.o;
7439
+
7440
+ const subjIsGraph = subj instanceof GraphTerm;
7441
+ const objIsGraph = obj instanceof GraphTerm;
7442
+ const subjIsTrue = subj instanceof Literal && subj.value === 'true';
7443
+ const objIsTrue = obj instanceof Literal && obj.value === 'true';
7444
+
7445
+ const isFwRuleTriple =
7446
+ isLogImplies(instantiated.p) &&
7447
+ ((subjIsGraph && objIsGraph) || (subjIsTrue && objIsGraph) || (subjIsGraph && objIsTrue));
7448
+
7449
+ const isBwRuleTriple =
7450
+ isLogImpliedBy(instantiated.p) &&
7451
+ ((subjIsGraph && objIsGraph) || (subjIsGraph && objIsTrue) || (subjIsTrue && objIsGraph));
7452
+
7453
+ if (isFwRuleTriple || isBwRuleTriple) {
7454
+ if (!hasFactIndexed(facts, instantiated)) {
7455
+ pushFactIndexed(facts, instantiated);
7456
+ const df = makeDerivedRecord(instantiated, r, instantiatedPremises, s, captureExplanations);
7457
+ derivedForward.push(df);
7458
+ if (typeof onDerived === 'function') onDerived(df);
7459
+ changedHere = true;
7460
+ }
7461
+
7462
+ // Promote rule-producing triples to live rules, treating literal true as {}.
7463
+ const left = __graphTriplesOrTrue(subj);
7464
+ const right = __graphTriplesOrTrue(obj);
7465
+
7466
+ if (left !== null && right !== null) {
7467
+ if (isFwRuleTriple) {
7468
+ const [premise, conclusion] = liftBlankRuleVars(left, right);
7469
+ const headBlankLabels = collectBlankLabelsInTriples(conclusion);
7470
+ const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
7471
+ __prepareForwardRule(newRule);
7472
+
7473
+ const key = __ruleKey(
7474
+ newRule.isForward,
7475
+ newRule.isFuse,
7476
+ newRule.premise,
7477
+ newRule.conclusion,
7478
+ newRule.__dynamicConclusionTerm || null,
7479
+ );
7480
+ if (!forwardRules.__ruleKeySet.has(key)) {
7481
+ forwardRules.__ruleKeySet.add(key);
7482
+ forwardRules.push(newRule);
7483
+ rulesChanged = true;
7484
+ }
7485
+ } else if (isBwRuleTriple) {
7486
+ const [premise, conclusion] = liftBlankRuleVars(right, left);
7487
+ const headBlankLabels = collectBlankLabelsInTriples(conclusion);
7488
+ const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
7489
+
7490
+ const key = __ruleKey(
7491
+ newRule.isForward,
7492
+ newRule.isFuse,
7493
+ newRule.premise,
7494
+ newRule.conclusion,
7495
+ newRule.__dynamicConclusionTerm || null,
7496
+ );
7497
+ if (!backRules.__ruleKeySet.has(key)) {
7498
+ backRules.__ruleKeySet.add(key);
7499
+ backRules.push(newRule);
7500
+ indexBackRule(backRules, newRule);
7501
+ rulesChanged = true;
7502
+ }
7503
+ }
7504
+ }
7505
+
7506
+ continue; // skip normal fact handling
7507
+ }
7508
+
7509
+ // Only skolemize blank nodes that occur explicitly in the rule head
7510
+ const inst = skolemizeTripleForHeadBlanks(
7511
+ instantiated,
7512
+ headBlankLabelsHere,
7513
+ skMap,
7514
+ skCounter,
7515
+ fireKey,
7516
+ headSkolemCache,
7517
+ );
7518
+
7519
+ if (!isGroundTriple(inst)) continue;
7520
+ if (hasFactIndexed(facts, inst)) continue;
7521
+
7522
+ pushFactIndexed(facts, inst);
7523
+ const df = makeDerivedRecord(inst, r, instantiatedPremises, s, captureExplanations);
7524
+ derivedForward.push(df);
7525
+ if (typeof onDerived === 'function') onDerived(df);
7526
+
7527
+ changedHere = true;
7528
+ }
7529
+
7530
+ return { changedHere, rulesChanged };
7531
+ }
7532
+
7172
7533
  function runFixpoint() {
7173
7534
  let anyChange = false;
7535
+ let agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
7536
+ let agendaCursor = 0;
7174
7537
 
7175
7538
  while (true) {
7176
7539
  let changed = false;
7177
7540
 
7178
- for (let i = 0; i < forwardRules.length; i++) {
7179
- const r = forwardRules[i];
7541
+ while (agendaCursor < facts.length && agendaIndex.size) {
7542
+ const fact = facts[agendaCursor++];
7543
+ const candidates = getSinglePremiseAgendaCandidates(agendaIndex, fact);
7544
+ if (!candidates) continue;
7180
7545
 
7181
- // Skip forward rules that are guaranteed to "delay" due to scoped
7182
- // builtins (log:collectAllIn / log:forAllIn / log:includes / log:notIncludes)
7183
- // until a snapshot exists (and a certain closure level is reached).
7184
- // This prevents expensive proofs that will definitely fail in Phase A
7185
- // and in early closure levels.
7186
- const info = r.__scopedSkipInfo;
7187
- if (info && info.needsSnap) {
7188
- const snapHere = facts.__scopedSnapshot || null;
7189
- const lvlHere =
7190
- (facts && typeof facts.__scopedClosureLevel === 'number' && facts.__scopedClosureLevel) || 0;
7191
- if (!snapHere) continue;
7192
- if (lvlHere < info.requiredLevel) continue;
7193
- }
7546
+ const total = candidates.exactLen + candidates.wildLen;
7547
+ for (let ci = 0; ci < total; ci++) {
7548
+ const entry = ci < candidates.exactLen ? candidates.exact[ci] : candidates.wild[ci - candidates.exactLen];
7549
+ const r = entry.rule;
7550
+ if (__skipForwardRuleNow(r)) continue;
7194
7551
 
7195
- // Optimization: if the rule head is **structurally ground** (no vars anywhere, even inside
7196
- // quoted formulas) and has no head blanks, then the head does not depend on which body
7197
- // solution we pick. In that case, we only need *one* proof of the body, and once all head
7198
- // triples are already known we can skip proving the body entirely.
7199
- const headIsStrictGround = r.__headIsStrictGround;
7552
+ const s = unifyTriple(entry.goal, fact, {});
7553
+ if (s === null) continue;
7200
7554
 
7201
- if (headIsStrictGround) {
7202
- let allKnown = true;
7203
- for (const tr of r.conclusion) {
7204
- if (!hasFactIndexed(facts, tr)) {
7205
- allKnown = false;
7206
- break;
7207
- }
7555
+ const outcome = __emitForwardRuleSolution(r, entry.ruleIndex, s);
7556
+ if (outcome.rulesChanged) {
7557
+ agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
7558
+ agendaCursor = 0;
7559
+ }
7560
+ if (outcome.changedHere) {
7561
+ changed = true;
7562
+ anyChange = true;
7208
7563
  }
7209
- if (allKnown) continue;
7210
7564
  }
7565
+ }
7566
+
7567
+ for (let i = 0; i < forwardRules.length; i++) {
7568
+ const r = forwardRules[i];
7569
+ if (agendaIndex.indexed.has(r)) continue;
7570
+ if (__skipForwardRuleNow(r)) continue;
7211
7571
 
7572
+ const headIsStrictGround = r.__headIsStrictGround;
7212
7573
  const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
7213
7574
  // Enable builtin deferral / goal reordering for forward rules only.
7214
7575
  // This keeps forward-chaining conjunctions order-insensitive while
@@ -7225,145 +7586,22 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
7225
7586
  }
7226
7587
 
7227
7588
  for (const s of sols) {
7228
- // IMPORTANT: one skolem map per *rule firing*
7229
- const skMap = {};
7230
- const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
7231
- const fireKey = __firingKey(i, instantiatedPremises);
7232
-
7233
- // Support "dynamic" rule heads where the consequent is a term that
7234
- // (after substitution) evaluates to a quoted formula.
7235
- // Example: { :a :b ?C } => ?C.
7236
- let dynamicHeadTriples = null;
7237
- let headBlankLabelsHere = r.headBlankLabels;
7238
- if (r.__dynamicConclusionTerm) {
7239
- const dynTerm = applySubstTerm(r.__dynamicConclusionTerm, s);
7240
-
7241
- // Allow dynamic fuses: ... => ?X. where ?X becomes false
7242
- if (dynTerm instanceof Literal && dynTerm.value === 'false') {
7243
- console.log('# Inference fuse triggered: dynamic head resolved to false.');
7244
- process.exit(2);
7245
- }
7246
-
7247
- const dynTriples = __graphTriplesOrTrue(dynTerm);
7248
- dynamicHeadTriples = dynTriples !== null ? dynTriples : [];
7249
-
7250
- // If the dynamic head contains explicit blank nodes, treat them as
7251
- // head blanks for skolemization.
7252
- const dynHeadBlankLabels =
7253
- dynamicHeadTriples && dynamicHeadTriples.length
7254
- ? collectBlankLabelsInTriples(dynamicHeadTriples)
7255
- : null;
7256
- if (dynHeadBlankLabels && dynHeadBlankLabels.size) {
7257
- headBlankLabelsHere = new Set([...headBlankLabelsHere, ...dynHeadBlankLabels]);
7258
- }
7589
+ const outcome = __emitForwardRuleSolution(r, i, s);
7590
+ if (outcome.rulesChanged) {
7591
+ agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
7592
+ agendaCursor = 0;
7259
7593
  }
7260
-
7261
- const headPatterns =
7262
- dynamicHeadTriples && dynamicHeadTriples.length ? r.conclusion.concat(dynamicHeadTriples) : r.conclusion;
7263
-
7264
- for (const cpat of headPatterns) {
7265
- const instantiated = applySubstTriple(cpat, s);
7266
-
7267
- const subj = instantiated.s;
7268
- const obj = instantiated.o;
7269
-
7270
- const subjIsGraph = subj instanceof GraphTerm;
7271
- const objIsGraph = obj instanceof GraphTerm;
7272
- const subjIsTrue = subj instanceof Literal && subj.value === 'true';
7273
- const objIsTrue = obj instanceof Literal && obj.value === 'true';
7274
-
7275
- const isFwRuleTriple =
7276
- isLogImplies(instantiated.p) &&
7277
- ((subjIsGraph && objIsGraph) || (subjIsTrue && objIsGraph) || (subjIsGraph && objIsTrue));
7278
-
7279
- const isBwRuleTriple =
7280
- isLogImpliedBy(instantiated.p) &&
7281
- ((subjIsGraph && objIsGraph) || (subjIsGraph && objIsTrue) || (subjIsTrue && objIsGraph));
7282
-
7283
- if (isFwRuleTriple || isBwRuleTriple) {
7284
- if (!hasFactIndexed(facts, instantiated)) {
7285
- factList.push(instantiated);
7286
- pushFactIndexed(facts, instantiated);
7287
- const df = new DerivedFact(instantiated, r, instantiatedPremises.slice(), { ...s });
7288
- derivedForward.push(df);
7289
- if (typeof onDerived === 'function') onDerived(df);
7290
-
7291
- changed = true;
7292
- }
7293
-
7294
- // Promote rule-producing triples to live rules, treating literal true as {}.
7295
- const left = __graphTriplesOrTrue(subj);
7296
- const right = __graphTriplesOrTrue(obj);
7297
-
7298
- if (left !== null && right !== null) {
7299
- if (isFwRuleTriple) {
7300
- const [premise, conclusion] = liftBlankRuleVars(left, right);
7301
- const headBlankLabels = collectBlankLabelsInTriples(conclusion);
7302
- const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
7303
- __prepareForwardRule(newRule);
7304
-
7305
- const key = __ruleKey(
7306
- newRule.isForward,
7307
- newRule.isFuse,
7308
- newRule.premise,
7309
- newRule.conclusion,
7310
- newRule.__dynamicConclusionTerm || null,
7311
- );
7312
- if (!forwardRules.__ruleKeySet.has(key)) {
7313
- forwardRules.__ruleKeySet.add(key);
7314
- forwardRules.push(newRule);
7315
- }
7316
- } else if (isBwRuleTriple) {
7317
- const [premise, conclusion] = liftBlankRuleVars(right, left);
7318
- const headBlankLabels = collectBlankLabelsInTriples(conclusion);
7319
- const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
7320
-
7321
- const key = __ruleKey(
7322
- newRule.isForward,
7323
- newRule.isFuse,
7324
- newRule.premise,
7325
- newRule.conclusion,
7326
- newRule.__dynamicConclusionTerm || null,
7327
- );
7328
- if (!backRules.__ruleKeySet.has(key)) {
7329
- backRules.__ruleKeySet.add(key);
7330
- backRules.push(newRule);
7331
- indexBackRule(backRules, newRule);
7332
- }
7333
- }
7334
- }
7335
-
7336
- continue; // skip normal fact handling
7337
- }
7338
-
7339
- // Only skolemize blank nodes that occur explicitly in the rule head
7340
- const inst = skolemizeTripleForHeadBlanks(
7341
- instantiated,
7342
- headBlankLabelsHere,
7343
- skMap,
7344
- skCounter,
7345
- fireKey,
7346
- headSkolemCache,
7347
- );
7348
-
7349
- if (!isGroundTriple(inst)) continue;
7350
- if (hasFactIndexed(facts, inst)) continue;
7351
-
7352
- factList.push(inst);
7353
- pushFactIndexed(facts, inst);
7354
- const df = new DerivedFact(inst, r, instantiatedPremises.slice(), {
7355
- ...s,
7356
- });
7357
- derivedForward.push(df);
7358
- if (typeof onDerived === 'function') onDerived(df);
7359
-
7594
+ if (outcome.changedHere) {
7360
7595
  changed = true;
7596
+ anyChange = true;
7361
7597
  }
7362
7598
  }
7363
7599
  }
7364
7600
 
7365
- if (!changed) break;
7366
- anyChange = true;
7601
+ if (!changed) {
7602
+ if (agendaCursor < facts.length && agendaIndex.size) continue;
7603
+ break;
7604
+ }
7367
7605
  }
7368
7606
 
7369
7607
  return anyChange;
@@ -7407,7 +7645,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
7407
7645
 
7408
7646
  return derivedForward;
7409
7647
  } finally {
7410
- __exitReasoningRun();
7648
+ exitReasoningRun();
7411
7649
  }
7412
7650
  }
7413
7651
 
@@ -7479,7 +7717,7 @@ function __withScopedSnapshotForQueries(facts, fn) {
7479
7717
  }
7480
7718
  }
7481
7719
 
7482
- function collectLogQueryConclusions(logQueryRules, facts, backRules) {
7720
+ function collectLogQueryConclusions(logQueryRules, facts, backRules, opts = {}) {
7483
7721
  const queryTriples = [];
7484
7722
  const queryDerived = [];
7485
7723
  const seen = new Set();
@@ -7495,6 +7733,8 @@ function collectLogQueryConclusions(logQueryRules, facts, backRules) {
7495
7733
  __attachGoalTable(facts, goalTable);
7496
7734
  __attachGoalTable(backRules, goalTable);
7497
7735
 
7736
+ const captureExplanations = !(opts && opts.captureExplanations === false);
7737
+
7498
7738
  // Shared state across all query firings (mirrors forwardChain()).
7499
7739
  const varGen = [0];
7500
7740
  const skCounter = [0];
@@ -7546,7 +7786,7 @@ function collectLogQueryConclusions(logQueryRules, facts, backRules) {
7546
7786
  if (seen.has(k)) continue;
7547
7787
  seen.add(k);
7548
7788
  queryTriples.push(inst);
7549
- queryDerived.push(new DerivedFact(inst, r, instantiatedPremises.slice(), { ...s }));
7789
+ queryDerived.push(makeDerivedRecord(inst, r, instantiatedPremises, s, captureExplanations));
7550
7790
  }
7551
7791
  }
7552
7792
  }
@@ -7555,16 +7795,23 @@ function collectLogQueryConclusions(logQueryRules, facts, backRules) {
7555
7795
  });
7556
7796
  }
7557
7797
 
7558
- function forwardChainAndCollectLogQueryConclusions(facts, forwardRules, backRules, logQueryRules, onDerived) {
7559
- __enterReasoningRun();
7798
+ function forwardChainAndCollectLogQueryConclusions(
7799
+ facts,
7800
+ forwardRules,
7801
+ backRules,
7802
+ logQueryRules,
7803
+ onDerived,
7804
+ opts = {},
7805
+ ) {
7806
+ enterReasoningRun();
7560
7807
  try {
7561
7808
  // Forward chain first (saturates `facts`).
7562
- const derived = forwardChain(facts, forwardRules, backRules, onDerived);
7809
+ const derived = forwardChain(facts, forwardRules, backRules, onDerived, opts);
7563
7810
  // Then collect query conclusions against the saturated closure.
7564
- const { queryTriples, queryDerived } = collectLogQueryConclusions(logQueryRules, facts, backRules);
7811
+ const { queryTriples, queryDerived } = collectLogQueryConclusions(logQueryRules, facts, backRules, opts);
7565
7812
  return { derived, queryTriples, queryDerived };
7566
7813
  } finally {
7567
- __exitReasoningRun();
7814
+ exitReasoningRun();
7568
7815
  }
7569
7816
  }
7570
7817