eyeling 1.25.3 → 1.25.5

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/lib/engine.js CHANGED
@@ -484,6 +484,158 @@ function __varOccursElsewhereInPremise(premise, name, idx, field) {
484
484
  return false;
485
485
  }
486
486
 
487
+
488
+ function __scopedPriorityForTerm(t) {
489
+ if (t instanceof GraphTerm) return 0;
490
+ const p0 = __logNaturalPriorityFromTerm(t);
491
+ return p0 !== null && p0 >= 1 ? p0 : 1;
492
+ }
493
+
494
+ function __pushScopedQueryTriplesFromGraph(term, out) {
495
+ if (!(term instanceof GraphTerm)) return;
496
+ for (const tr of term.triples) out.push(tr);
497
+ }
498
+
499
+ function __analyzeForwardRuleScopedUse(rule) {
500
+ const out = {
501
+ needsSnap: false,
502
+ baseLevel: 0,
503
+ queryTriples: [],
504
+ hasScopedAggregate: false,
505
+ };
506
+
507
+ if (!rule || !Array.isArray(rule.premise)) return out;
508
+
509
+ for (let i = 0; i < rule.premise.length; i++) {
510
+ const tr = rule.premise[i];
511
+ if (!(tr && tr.p instanceof Iri)) continue;
512
+ const pv = tr.p.value;
513
+
514
+ if (pv === LOG_NS + 'includes' || pv === LOG_NS + 'notIncludes') {
515
+ // Explicit quoted scopes are local formulas, not snapshots of the global closure.
516
+ if (tr.s instanceof GraphTerm) continue;
517
+
518
+ // A scope variable that is bound by another premise may become an explicit GraphTerm.
519
+ // Still, if it remains unbound at runtime the builtin treats it as priority 1.
520
+ // We therefore record the scoped use, but only use quoted object triples for
521
+ // dependency analysis; variables inside the quoted object do not bind the scope.
522
+ out.needsSnap = true;
523
+ out.baseLevel = Math.max(out.baseLevel, __scopedPriorityForTerm(tr.s));
524
+ __pushScopedQueryTriplesFromGraph(tr.o, out.queryTriples);
525
+ continue;
526
+ }
527
+
528
+ if (pv === LOG_NS + 'collectAllIn') {
529
+ if (tr.o instanceof GraphTerm) continue;
530
+
531
+ out.needsSnap = true;
532
+ out.hasScopedAggregate = true;
533
+ out.baseLevel = Math.max(out.baseLevel, __scopedPriorityForTerm(tr.o));
534
+ if (tr.s instanceof ListTerm && tr.s.elems.length === 3) {
535
+ __pushScopedQueryTriplesFromGraph(tr.s.elems[1], out.queryTriples);
536
+ }
537
+ continue;
538
+ }
539
+
540
+ if (pv === LOG_NS + 'forAllIn') {
541
+ if (tr.o instanceof GraphTerm) continue;
542
+
543
+ out.needsSnap = true;
544
+ out.hasScopedAggregate = true;
545
+ out.baseLevel = Math.max(out.baseLevel, __scopedPriorityForTerm(tr.o));
546
+ if (tr.s instanceof ListTerm && tr.s.elems.length === 2) {
547
+ __pushScopedQueryTriplesFromGraph(tr.s.elems[0], out.queryTriples);
548
+ __pushScopedQueryTriplesFromGraph(tr.s.elems[1], out.queryTriples);
549
+ }
550
+ }
551
+ }
552
+
553
+ return out;
554
+ }
555
+
556
+ function __triplePatternsMayOverlap(a, b) {
557
+ return unifyTriple(a, b, __emptySubst()) !== null || unifyTriple(b, a, __emptySubst()) !== null;
558
+ }
559
+
560
+ function __ruleConclusionsMayFeedScopedQueries(producer, consumerMeta) {
561
+ if (!consumerMeta || !consumerMeta.queryTriples || consumerMeta.queryTriples.length === 0) return false;
562
+
563
+ // Dynamic conclusions are only known after a proof solution. Conservatively
564
+ // assume they can feed any scoped query if the producing rule is scoped.
565
+ if (producer && producer.__dynamicConclusionTerm) return true;
566
+
567
+ if (!producer || !Array.isArray(producer.conclusion) || producer.conclusion.length === 0) return false;
568
+ for (const q of consumerMeta.queryTriples) {
569
+ for (const h of producer.conclusion) {
570
+ if (__triplePatternsMayOverlap(q, h)) return true;
571
+ }
572
+ }
573
+ return false;
574
+ }
575
+
576
+ function __setForwardRuleScopedStratumInfo(rule, level) {
577
+ const value =
578
+ level > 0
579
+ ? { needsSnap: true, requiredLevel: level, exactLevel: true }
580
+ : { needsSnap: false, requiredLevel: 0, exactLevel: false };
581
+
582
+ if (!hasOwn.call(rule, '__scopedStratumInfo')) {
583
+ Object.defineProperty(rule, '__scopedStratumInfo', {
584
+ value,
585
+ enumerable: false,
586
+ writable: true,
587
+ configurable: true,
588
+ });
589
+ } else {
590
+ rule.__scopedStratumInfo = value;
591
+ }
592
+ }
593
+
594
+ function __computeForwardRuleScopedStrata(forwardRules) {
595
+ if (!Array.isArray(forwardRules) || forwardRules.length === 0) return 0;
596
+
597
+ const metas = forwardRules.map((r) => __analyzeForwardRuleScopedUse(r));
598
+ const levels = metas.map((m) => (m.needsSnap ? Math.max(1, m.baseLevel || 1) : 0));
599
+
600
+ let maxLevel = 0;
601
+ for (const lvl of levels) if (lvl > maxLevel) maxLevel = lvl;
602
+
603
+ // Stratify scoped rules by data dependency: if a scoped query can read facts
604
+ // produced by another scoped rule, the reader must run against a later frozen
605
+ // snapshot. This prevents non-monotonic scoped builtins such as collectAllIn
606
+ // from emitting an early result before lower strata have derived their facts.
607
+ const passLimit = Math.max(1, forwardRules.length) + maxLevel + 1;
608
+ for (let pass = 0; pass < passLimit; pass++) {
609
+ let changed = false;
610
+ for (let ci = 0; ci < forwardRules.length; ci++) {
611
+ if (!metas[ci].needsSnap || !metas[ci].hasScopedAggregate) continue;
612
+ let needed = levels[ci];
613
+ for (let pi = 0; pi < forwardRules.length; pi++) {
614
+ if (pi === ci) continue;
615
+ if (levels[pi] <= 0) continue;
616
+ if (metas[pi].hasScopedAggregate) continue;
617
+ if (!__ruleConclusionsMayFeedScopedQueries(forwardRules[pi], metas[ci])) continue;
618
+ needed = Math.max(needed, levels[pi] + 1);
619
+ }
620
+ if (needed !== levels[ci]) {
621
+ levels[ci] = needed;
622
+ if (needed > maxLevel) maxLevel = needed;
623
+ changed = true;
624
+ }
625
+ }
626
+ if (!changed) break;
627
+ }
628
+
629
+
630
+ maxLevel = 0;
631
+ for (let i = 0; i < forwardRules.length; i++) {
632
+ __setForwardRuleScopedStratumInfo(forwardRules[i], levels[i]);
633
+ if (levels[i] > maxLevel) maxLevel = levels[i];
634
+ }
635
+
636
+ return maxLevel;
637
+ }
638
+
487
639
  function __computeForwardRuleScopedSkipInfo(rule) {
488
640
  let needsSnap = false;
489
641
  let requiredLevel = 0;
@@ -1034,6 +1186,12 @@ function alphaEqGraphTriples(xs, ys, opts) {
1034
1186
  // - __byPred: Map<predicateId, number[]> (indices into facts array)
1035
1187
  // - __byPS: Map<predicateId, Map<subjectId, number[]>>
1036
1188
  // - __byPO: Map<predicateId, Map<objectId, number[]>>
1189
+ // - __byPNonFastS / __byPNonFastO: Map<predicateId, number[]>
1190
+ // IRI-predicate facts whose subject/object cannot be indexed by fast key.
1191
+ // These are the fallback clauses for a constrained subject/object, just
1192
+ // like a Prolog clause index keeps variable-headed clauses as fallback.
1193
+ // - __varPred* indexes: facts whose predicate is a Var. Only these non-IRI
1194
+ // predicate facts can unify with a ground IRI predicate goal.
1037
1195
  // - __keySet: Set<"S\tP\tO"> for Iri/Literal/Blank-only triples (fast dup check)
1038
1196
  //
1039
1197
  // Backward rules:
@@ -1049,6 +1207,8 @@ const __compoundKeyToTid = new Map();
1049
1207
  // Use a negative id space so we never collide with __tid (which is positive).
1050
1208
  let __nextCompoundTid = -1;
1051
1209
 
1210
+ const EMPTY_FACT_INDEX_BUCKET = Object.freeze([]);
1211
+
1052
1212
  function __internCompoundTid(key) {
1053
1213
  const hit = __compoundKeyToTid.get(key);
1054
1214
  if (hit !== undefined) return hit;
@@ -1100,6 +1260,71 @@ function termFastKey(t) {
1100
1260
  return null;
1101
1261
  }
1102
1262
 
1263
+ function encodeLookupKeyPart(k) {
1264
+ if (typeof k === 'number') return 'T' + k;
1265
+ const s = String(k);
1266
+ return 'K' + s.length + ':' + s;
1267
+ }
1268
+
1269
+ function literalLookupKey(t) {
1270
+ const boolInfo = parseBooleanLiteralInfo(t);
1271
+ if (boolInfo) return '\u0000B' + (boolInfo.value ? '1' : '0');
1272
+
1273
+ const numInfo = parseNumericLiteralInfo(t);
1274
+ if (numInfo) {
1275
+ if (numInfo.kind === 'bigint') return '\u0000N' + numInfo.dt + '\u0000' + numInfo.value.toString();
1276
+
1277
+ const n = numInfo.value;
1278
+ // Normal unification intentionally does not make NaN value-equal to NaN;
1279
+ // only identical lexical literals match through the ordinary __tid path.
1280
+ if (!Number.isNaN(n)) return '\u0000N' + numInfo.dt + '\u0000' + String(n);
1281
+ }
1282
+
1283
+ // Covers exact literals plus plain string / xsd:string canonicalization, which
1284
+ // Literal construction already normalizes into a shared __tid.
1285
+ return termFastKey(t);
1286
+ }
1287
+
1288
+ function termLookupKey(t) {
1289
+ // Lookup keys summarize the equality accepted by ordinary unifyTerm(), not
1290
+ // merely object identity. This keeps literal-index pruning complete for
1291
+ // value-equivalent booleans/numerics such as true/"1"^^xsd:boolean and
1292
+ // 1.0/1.00, while preserving exact fast ids for IRIs, blanks, and strings.
1293
+ if (t instanceof Iri) {
1294
+ if (t.value === RDF_NIL_IRI) return '\u0000L0';
1295
+ return t.__tid;
1296
+ }
1297
+ if (t instanceof Blank) return t.__tid;
1298
+ if (t instanceof Literal) return literalLookupKey(t);
1299
+
1300
+ if (t instanceof ListTerm) {
1301
+ const cached = t.__lookupKey;
1302
+ if (cached !== undefined) return cached === false ? null : cached;
1303
+
1304
+ const xs = t.elems;
1305
+ if (xs.length === 0) {
1306
+ Object.defineProperty(t, '__lookupKey', { value: '\u0000L0', enumerable: false });
1307
+ return '\u0000L0';
1308
+ }
1309
+
1310
+ const parts = new Array(xs.length);
1311
+ for (let i = 0; i < xs.length; i++) {
1312
+ const k = termLookupKey(xs[i]);
1313
+ if (k === null) {
1314
+ Object.defineProperty(t, '__lookupKey', { value: false, enumerable: false });
1315
+ return null;
1316
+ }
1317
+ parts[i] = encodeLookupKeyPart(k);
1318
+ }
1319
+
1320
+ const key = '\u0000L' + xs.length + '\u0001' + parts.join('\u0001');
1321
+ Object.defineProperty(t, '__lookupKey', { value: key, enumerable: false });
1322
+ return key;
1323
+ }
1324
+
1325
+ return null;
1326
+ }
1327
+
1103
1328
  function tripleFastKey(tr) {
1104
1329
  const ks = termFastKey(tr.s);
1105
1330
  const kp = termFastKey(tr.p);
@@ -1113,9 +1338,13 @@ function ensureFactIndexes(facts) {
1113
1338
  facts.__byPred &&
1114
1339
  facts.__byPS &&
1115
1340
  facts.__byPO &&
1116
- facts.__wildPred &&
1117
- facts.__wildPS &&
1118
- facts.__wildPO &&
1341
+ facts.__byPNonFastS &&
1342
+ facts.__byPNonFastO &&
1343
+ facts.__varPred &&
1344
+ facts.__varPredPS &&
1345
+ facts.__varPredPO &&
1346
+ facts.__varPredNonFastS &&
1347
+ facts.__varPredNonFastO &&
1119
1348
  facts.__keySet
1120
1349
  )
1121
1350
  return;
@@ -1135,21 +1364,41 @@ function ensureFactIndexes(facts) {
1135
1364
  enumerable: false,
1136
1365
  writable: true,
1137
1366
  });
1138
- Object.defineProperty(facts, '__wildPred', {
1367
+ Object.defineProperty(facts, '__byPNonFastS', {
1368
+ value: new Map(),
1369
+ enumerable: false,
1370
+ writable: true,
1371
+ });
1372
+ Object.defineProperty(facts, '__byPNonFastO', {
1373
+ value: new Map(),
1374
+ enumerable: false,
1375
+ writable: true,
1376
+ });
1377
+ Object.defineProperty(facts, '__varPred', {
1139
1378
  value: [],
1140
1379
  enumerable: false,
1141
1380
  writable: true,
1142
1381
  });
1143
- Object.defineProperty(facts, '__wildPS', {
1382
+ Object.defineProperty(facts, '__varPredPS', {
1144
1383
  value: new Map(),
1145
1384
  enumerable: false,
1146
1385
  writable: true,
1147
1386
  });
1148
- Object.defineProperty(facts, '__wildPO', {
1387
+ Object.defineProperty(facts, '__varPredPO', {
1149
1388
  value: new Map(),
1150
1389
  enumerable: false,
1151
1390
  writable: true,
1152
1391
  });
1392
+ Object.defineProperty(facts, '__varPredNonFastS', {
1393
+ value: [],
1394
+ enumerable: false,
1395
+ writable: true,
1396
+ });
1397
+ Object.defineProperty(facts, '__varPredNonFastO', {
1398
+ value: [],
1399
+ enumerable: false,
1400
+ writable: true,
1401
+ });
1153
1402
  Object.defineProperty(facts, '__keySet', {
1154
1403
  value: new Set(),
1155
1404
  enumerable: false,
@@ -1190,16 +1439,53 @@ function cloneFactIndexesForSnapshot(src, dest) {
1190
1439
  Object.defineProperty(dest, '__byPred', { value: cloneArrayMap(src.__byPred), enumerable: false, writable: true });
1191
1440
  Object.defineProperty(dest, '__byPS', { value: cloneNestedArrayMap(src.__byPS), enumerable: false, writable: true });
1192
1441
  Object.defineProperty(dest, '__byPO', { value: cloneNestedArrayMap(src.__byPO), enumerable: false, writable: true });
1193
- Object.defineProperty(dest, '__wildPred', { value: src.__wildPred.slice(), enumerable: false, writable: true });
1194
- Object.defineProperty(dest, '__wildPS', { value: cloneArrayMap(src.__wildPS), enumerable: false, writable: true });
1195
- Object.defineProperty(dest, '__wildPO', { value: cloneArrayMap(src.__wildPO), enumerable: false, writable: true });
1442
+ Object.defineProperty(dest, '__byPNonFastS', {
1443
+ value: cloneArrayMap(src.__byPNonFastS),
1444
+ enumerable: false,
1445
+ writable: true,
1446
+ });
1447
+ Object.defineProperty(dest, '__byPNonFastO', {
1448
+ value: cloneArrayMap(src.__byPNonFastO),
1449
+ enumerable: false,
1450
+ writable: true,
1451
+ });
1452
+ Object.defineProperty(dest, '__varPred', { value: src.__varPred.slice(), enumerable: false, writable: true });
1453
+ Object.defineProperty(dest, '__varPredPS', {
1454
+ value: cloneArrayMap(src.__varPredPS),
1455
+ enumerable: false,
1456
+ writable: true,
1457
+ });
1458
+ Object.defineProperty(dest, '__varPredPO', {
1459
+ value: cloneArrayMap(src.__varPredPO),
1460
+ enumerable: false,
1461
+ writable: true,
1462
+ });
1463
+ Object.defineProperty(dest, '__varPredNonFastS', {
1464
+ value: src.__varPredNonFastS.slice(),
1465
+ enumerable: false,
1466
+ writable: true,
1467
+ });
1468
+ Object.defineProperty(dest, '__varPredNonFastO', {
1469
+ value: src.__varPredNonFastO.slice(),
1470
+ enumerable: false,
1471
+ writable: true,
1472
+ });
1196
1473
  Object.defineProperty(dest, '__keySet', { value: new Set(src.__keySet), enumerable: false, writable: true });
1197
1474
  Object.defineProperty(dest, '__keySetComplete', { value: !!src.__keySetComplete, enumerable: false, writable: true });
1198
1475
  }
1199
1476
 
1477
+ function addToIndexArrayMap(map, key, value) {
1478
+ let bucket = map.get(key);
1479
+ if (!bucket) {
1480
+ bucket = [];
1481
+ map.set(key, bucket);
1482
+ }
1483
+ bucket.push(value);
1484
+ }
1485
+
1200
1486
  function indexFact(facts, tr, idx, addKeySet = true) {
1201
- const sk = termFastKey(tr.s);
1202
- const ok = termFastKey(tr.o);
1487
+ const sk = termLookupKey(tr.s);
1488
+ const ok = termLookupKey(tr.o);
1203
1489
  let pkForKey = null;
1204
1490
 
1205
1491
  if (tr.p instanceof Iri) {
@@ -1220,12 +1506,9 @@ function indexFact(facts, tr, idx, addKeySet = true) {
1220
1506
  ps = new Map();
1221
1507
  facts.__byPS.set(pk, ps);
1222
1508
  }
1223
- let psb = ps.get(sk);
1224
- if (!psb) {
1225
- psb = [];
1226
- ps.set(sk, psb);
1227
- }
1228
- psb.push(idx);
1509
+ addToIndexArrayMap(ps, sk, idx);
1510
+ } else {
1511
+ addToIndexArrayMap(facts.__byPNonFastS, pk, idx);
1229
1512
  }
1230
1513
 
1231
1514
  if (ok !== null) {
@@ -1234,32 +1517,23 @@ function indexFact(facts, tr, idx, addKeySet = true) {
1234
1517
  po = new Map();
1235
1518
  facts.__byPO.set(pk, po);
1236
1519
  }
1237
- let pob = po.get(ok);
1238
- if (!pob) {
1239
- pob = [];
1240
- po.set(ok, pob);
1241
- }
1242
- pob.push(idx);
1520
+ addToIndexArrayMap(po, ok, idx);
1521
+ } else {
1522
+ addToIndexArrayMap(facts.__byPNonFastO, pk, idx);
1243
1523
  }
1244
- } else {
1245
- facts.__wildPred.push(idx);
1524
+ } else if (tr.p instanceof Var) {
1525
+ facts.__varPred.push(idx);
1246
1526
 
1247
1527
  if (sk !== null) {
1248
- let psb = facts.__wildPS.get(sk);
1249
- if (!psb) {
1250
- psb = [];
1251
- facts.__wildPS.set(sk, psb);
1252
- }
1253
- psb.push(idx);
1528
+ addToIndexArrayMap(facts.__varPredPS, sk, idx);
1529
+ } else {
1530
+ facts.__varPredNonFastS.push(idx);
1254
1531
  }
1255
1532
 
1256
1533
  if (ok !== null) {
1257
- let pob = facts.__wildPO.get(ok);
1258
- if (!pob) {
1259
- pob = [];
1260
- facts.__wildPO.set(ok, pob);
1261
- }
1262
- pob.push(idx);
1534
+ addToIndexArrayMap(facts.__varPredPO, ok, idx);
1535
+ } else {
1536
+ facts.__varPredNonFastO.push(idx);
1263
1537
  }
1264
1538
  }
1265
1539
 
@@ -1269,55 +1543,86 @@ function indexFact(facts, tr, idx, addKeySet = true) {
1269
1543
  }
1270
1544
  }
1271
1545
 
1546
+ function mergeIndexBuckets(primary, fallback) {
1547
+ const a = primary && primary.length ? primary : null;
1548
+ const b = fallback && fallback.length ? fallback : null;
1549
+ if (!a && !b) return EMPTY_FACT_INDEX_BUCKET;
1550
+ if (!a) return b;
1551
+ if (!b) return a;
1552
+ const out = new Array(a.length + b.length);
1553
+ for (let i = 0; i < a.length; i++) out[i] = a[i];
1554
+ for (let i = 0; i < b.length; i++) out[a.length + i] = b[i];
1555
+ return out;
1556
+ }
1557
+
1558
+ function selectPositionIndexedCandidates(all, exactByS, fallbackS, sk, exactByO, fallbackO, ok) {
1559
+ if (sk === null && ok === null) return all && all.length ? all : EMPTY_FACT_INDEX_BUCKET;
1560
+
1561
+ let sBucket = null;
1562
+ if (sk !== null) sBucket = mergeIndexBuckets(exactByS || null, fallbackS || null);
1563
+
1564
+ let oBucket = null;
1565
+ if (ok !== null) oBucket = mergeIndexBuckets(exactByO || null, fallbackO || null);
1566
+
1567
+ if (sk !== null && ok !== null) return sBucket.length <= oBucket.length ? sBucket : oBucket;
1568
+ return sk !== null ? sBucket : oBucket;
1569
+ }
1570
+
1272
1571
  function candidateFacts(facts, goal) {
1273
1572
  ensureFactIndexes(facts);
1274
1573
 
1275
1574
  if (goal.p instanceof Iri) {
1276
1575
  const pk = goal.p.__tid;
1277
1576
 
1278
- const sk = termFastKey(goal.s);
1279
- const ok = termFastKey(goal.o);
1577
+ const sk = termLookupKey(goal.s);
1578
+ const ok = termLookupKey(goal.o);
1280
1579
 
1281
- /** @type {number[] | null} */
1282
1580
  let byPS = null;
1283
1581
  if (sk !== null) {
1284
1582
  const ps = facts.__byPS.get(pk);
1285
1583
  if (ps) byPS = ps.get(sk) || null;
1286
1584
  }
1585
+ const byPNonFastS = sk !== null ? facts.__byPNonFastS.get(pk) || null : null;
1287
1586
 
1288
- /** @type {number[] | null} */
1289
1587
  let byPO = null;
1290
1588
  if (ok !== null) {
1291
1589
  const po = facts.__byPO.get(pk);
1292
1590
  if (po) byPO = po.get(ok) || null;
1293
1591
  }
1592
+ const byPNonFastO = ok !== null ? facts.__byPNonFastO.get(pk) || null : null;
1593
+
1594
+ const exact = selectPositionIndexedCandidates(
1595
+ facts.__byPred.get(pk) || null,
1596
+ byPS,
1597
+ byPNonFastS,
1598
+ sk,
1599
+ byPO,
1600
+ byPNonFastO,
1601
+ ok,
1602
+ );
1294
1603
 
1295
- let exact = null;
1296
- if (byPS && byPO) exact = byPS.length <= byPO.length ? byPS : byPO;
1297
- else if (byPS) exact = byPS;
1298
- else if (byPO) exact = byPO;
1299
- else exact = facts.__byPred.get(pk) || null;
1300
-
1301
- /** @type {number[] | null} */
1302
- let wildPS = null;
1303
- if (sk !== null) wildPS = facts.__wildPS.get(sk) || null;
1604
+ let varPredPS = null;
1605
+ if (sk !== null) varPredPS = facts.__varPredPS.get(sk) || null;
1304
1606
 
1305
- /** @type {number[] | null} */
1306
- let wildPO = null;
1307
- if (ok !== null) wildPO = facts.__wildPO.get(ok) || null;
1607
+ let varPredPO = null;
1608
+ if (ok !== null) varPredPO = facts.__varPredPO.get(ok) || null;
1308
1609
 
1309
- let wild = null;
1310
- if (wildPS && wildPO) wild = wildPS.length <= wildPO.length ? wildPS : wildPO;
1311
- else if (wildPS) wild = wildPS;
1312
- else if (wildPO) wild = wildPO;
1313
- else wild = facts.__wildPred.length ? facts.__wildPred : null;
1610
+ const wild = selectPositionIndexedCandidates(
1611
+ facts.__varPred,
1612
+ varPredPS,
1613
+ sk !== null ? facts.__varPredNonFastS : null,
1614
+ sk,
1615
+ varPredPO,
1616
+ ok !== null ? facts.__varPredNonFastO : null,
1617
+ ok,
1618
+ );
1314
1619
 
1315
1620
  return {
1316
- exact: exact || null,
1317
- wild: wild || null,
1318
- exactLen: exact ? exact.length : 0,
1319
- wildLen: wild ? wild.length : 0,
1320
- totalLen: (exact ? exact.length : 0) + (wild ? wild.length : 0),
1621
+ exact,
1622
+ wild,
1623
+ exactLen: exact.length,
1624
+ wildLen: wild.length,
1625
+ totalLen: exact.length + wild.length,
1321
1626
  };
1322
1627
  }
1323
1628
 
@@ -1335,20 +1640,28 @@ function hasFactIndexed(facts, tr) {
1335
1640
 
1336
1641
  if (tr.p instanceof Iri) {
1337
1642
  const pk = tr.p.__tid;
1643
+ const sk = termLookupKey(tr.s);
1644
+ let best = null;
1645
+
1646
+ if (sk !== null) {
1647
+ const ps = facts.__byPS.get(pk);
1648
+ if (!ps) return false;
1649
+ const psb = ps.get(sk);
1650
+ if (!psb || psb.length === 0) return false;
1651
+ best = psb;
1652
+ }
1338
1653
 
1339
- const ok = termFastKey(tr.o);
1654
+ const ok = termLookupKey(tr.o);
1340
1655
  if (ok !== null) {
1341
1656
  const po = facts.__byPO.get(pk);
1342
- if (po) {
1343
- const pob = po.get(ok) || [];
1344
- // Facts are all in the same graph. Different blank node labels represent
1345
- // different existentials unless explicitly connected. Do NOT treat
1346
- // triples as duplicates modulo blank renaming, or you'll incorrectly
1347
- // drop facts like: _:sk_0 :x 8.0 (because _:b8 :x 8.0 exists).
1348
- return pob.some((i) => triplesEqual(facts[i], tr));
1349
- }
1657
+ if (!po) return false;
1658
+ const pob = po.get(ok);
1659
+ if (!pob || pob.length === 0) return false;
1660
+ if (!best || pob.length < best.length) best = pob;
1350
1661
  }
1351
1662
 
1663
+ if (best) return best.some((i) => triplesEqual(facts[i], tr));
1664
+
1352
1665
  const pb = facts.__byPred.get(pk) || [];
1353
1666
  return pb.some((i) => triplesEqual(facts[i], tr));
1354
1667
  }
@@ -1457,11 +1770,25 @@ function mergeSinglePremiseAgendaBuckets() {
1457
1770
  return out;
1458
1771
  }
1459
1772
 
1773
+ function termContainsVarForAgenda(t) {
1774
+ if (t instanceof Var) return true;
1775
+ if (t instanceof ListTerm) return t.elems.some(termContainsVarForAgenda);
1776
+ if (t instanceof OpenListTerm) return true;
1777
+ if (t instanceof GraphTerm)
1778
+ return t.triples.some(
1779
+ (tr) =>
1780
+ termContainsVarForAgenda(tr.s) || termContainsVarForAgenda(tr.p) || termContainsVarForAgenda(tr.o),
1781
+ );
1782
+ return false;
1783
+ }
1784
+
1460
1785
  function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1461
1786
  const index = {
1462
1787
  byPred: new Map(),
1788
+ byPredAll: new Map(),
1463
1789
  byPS: new Map(),
1464
1790
  byPO: new Map(),
1791
+ allIriPred: [],
1465
1792
  wildPred: [],
1466
1793
  wildPS: new Map(),
1467
1794
  wildPO: new Map(),
@@ -1483,8 +1810,8 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1483
1810
  if (!isSinglePremiseAgendaRuleSafe(r, backRules)) continue;
1484
1811
 
1485
1812
  const goal = r.premise[0];
1486
- const goalSKey = termFastKey(goal.s);
1487
- const goalOKey = termFastKey(goal.o);
1813
+ const goalSKey = termLookupKey(goal.s);
1814
+ const goalOKey = termLookupKey(goal.o);
1488
1815
  const fastSubjectVar = goal.p instanceof Iri && goal.s instanceof Var && goalOKey !== null ? goal.s.name : null;
1489
1816
  const fastObjectVar = goal.p instanceof Iri && goal.o instanceof Var && goalSKey !== null ? goal.o.name : null;
1490
1817
  const entry = {
@@ -1494,7 +1821,7 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1494
1821
  goalPredTid: goal.p instanceof Iri ? goal.p.__tid : null,
1495
1822
  goalSKey,
1496
1823
  goalOKey,
1497
- needsSkipCheck: !!r.__needsForwardSkipCheck,
1824
+ needsSkipCheck: !!(r.__needsForwardSkipCheck || (r.__scopedStratumInfo && r.__scopedStratumInfo.needsSnap)),
1498
1825
  fastSubjectVar,
1499
1826
  fastObjectVar,
1500
1827
  };
@@ -1503,6 +1830,8 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1503
1830
  index.size += 1;
1504
1831
 
1505
1832
  if (entry.goalPredTid !== null) {
1833
+ addToMapArray(index.byPredAll, entry.goalPredTid, entry);
1834
+ index.allIriPred.push(entry);
1506
1835
  if (entry.goalSKey === null && entry.goalOKey === null) addToMapArray(index.byPred, entry.goalPredTid, entry);
1507
1836
  if (entry.goalSKey !== null) {
1508
1837
  let ps = index.byPS.get(entry.goalPredTid);
@@ -1533,25 +1862,37 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1533
1862
  function getSinglePremiseAgendaCandidates(index, fact) {
1534
1863
  if (!index || index.size === 0) return null;
1535
1864
 
1536
- const sk = termFastKey(fact.s);
1537
- const ok = termFastKey(fact.o);
1865
+ const sk = termLookupKey(fact.s);
1866
+ const ok = termLookupKey(fact.o);
1538
1867
 
1539
1868
  let exact = null;
1540
1869
  if (fact.p instanceof Iri) {
1541
1870
  const pk = fact.p.__tid;
1542
- const byPred = index.byPred.get(pk) || null;
1543
- let byPS = null;
1544
- if (sk !== null) {
1871
+ if ((sk === null && termContainsVarForAgenda(fact.s)) || (ok === null && termContainsVarForAgenda(fact.o))) {
1872
+ // A fact with a variable-bearing subject/object (most importantly a
1873
+ // top-level variable fact such as `?S :p ?O.`) can match rules whose
1874
+ // premise is fixed in that position. The ordinary `(p,s)` / `(p,o)` lookup
1875
+ // would miss those rules, so fall back to all agenda-indexed rules for
1876
+ // this predicate. Do not do this merely for non-fast quoted formulas:
1877
+ // they are not wildcards, and broad fallback would over-fire rules that
1878
+ // rely on protected blank-node/formula unification semantics.
1879
+ exact = index.byPredAll.get(pk) || null;
1880
+ } else {
1881
+ const byPred = index.byPred.get(pk) || null;
1882
+ let byPS = null;
1545
1883
  const ps = index.byPS.get(pk);
1546
1884
  if (ps) byPS = ps.get(sk) || null;
1547
- }
1548
- let byPO = null;
1549
- if (ok !== null) {
1885
+ let byPO = null;
1550
1886
  const po = index.byPO.get(pk);
1551
1887
  if (po) byPO = po.get(ok) || null;
1552
- }
1553
1888
 
1554
- exact = mergeSinglePremiseAgendaBuckets(byPred, byPS, byPO);
1889
+ exact = mergeSinglePremiseAgendaBuckets(byPred, byPS, byPO);
1890
+ }
1891
+ } else if (fact.p instanceof Var) {
1892
+ // A variable-predicate fact can match any IRI-predicate agenda rule.
1893
+ // This is deliberately broad and relies on final unification below; such
1894
+ // facts are uncommon and correctness matters more than over-indexing them.
1895
+ exact = index.allIriPred.length ? index.allIriPred : null;
1555
1896
  }
1556
1897
 
1557
1898
  const wildPred = index.wildPred.length ? index.wildPred : null;
@@ -2928,7 +3269,10 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2928
3269
  let scopedClosureLevel = 0;
2929
3270
 
2930
3271
  // Scan known rules for the maximum requested closure priority in scoped log:* goals.
2931
- let maxScopedClosurePriorityNeeded = __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules);
3272
+ let maxScopedClosurePriorityNeeded = Math.max(
3273
+ __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules),
3274
+ __computeForwardRuleScopedStrata(forwardRules),
3275
+ );
2932
3276
 
2933
3277
  // Conservative fast-skip for forward rules that cannot possibly succeed
2934
3278
  // until a scoped snapshot exists (or a given closure level is reached).
@@ -2982,12 +3326,16 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2982
3326
  // until a snapshot exists (and a certain closure level is reached).
2983
3327
  // This prevents expensive proofs that will definitely fail in Phase A
2984
3328
  // and in early closure levels.
2985
- const info = r.__scopedSkipInfo;
3329
+ const info = r.__scopedStratumInfo || r.__scopedSkipInfo;
2986
3330
  if (info && info.needsSnap) {
2987
3331
  const snapHere = facts.__scopedSnapshot || null;
2988
3332
  const lvlHere = (facts && typeof facts.__scopedClosureLevel === 'number' && facts.__scopedClosureLevel) || 0;
2989
3333
  if (!snapHere) return true;
2990
- if (lvlHere < info.requiredLevel) return true;
3334
+ if (info.exactLevel) {
3335
+ if (lvlHere !== info.requiredLevel) return true;
3336
+ } else if (lvlHere < info.requiredLevel) {
3337
+ return true;
3338
+ }
2991
3339
  }
2992
3340
 
2993
3341
  // Optimization: if the rule head is **structurally ground** (no vars anywhere, even inside
@@ -3187,6 +3535,10 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3187
3535
 
3188
3536
  const outcome = __emitForwardRuleSolution(r, entry.ruleIndex, s);
3189
3537
  if (outcome.rulesChanged) {
3538
+ maxScopedClosurePriorityNeeded = Math.max(
3539
+ __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules),
3540
+ __computeForwardRuleScopedStrata(forwardRules),
3541
+ );
3190
3542
  agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
3191
3543
  agendaCursor = 0;
3192
3544
  }
@@ -3200,7 +3552,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3200
3552
  for (let i = 0; i < forwardRules.length; i++) {
3201
3553
  const r = forwardRules[i];
3202
3554
  if (agendaIndex.indexed.has(r)) continue;
3203
- if (r.__needsForwardSkipCheck && __skipForwardRuleNow(r)) continue;
3555
+ if ((r.__needsForwardSkipCheck || (r.__scopedStratumInfo && r.__scopedStratumInfo.needsSnap)) && __skipForwardRuleNow(r)) continue;
3204
3556
 
3205
3557
  const headIsStrictGround = r.__headIsStrictGround;
3206
3558
  const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
@@ -3221,6 +3573,10 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3221
3573
  for (const s of sols) {
3222
3574
  const outcome = __emitForwardRuleSolution(r, i, s);
3223
3575
  if (outcome.rulesChanged) {
3576
+ maxScopedClosurePriorityNeeded = Math.max(
3577
+ __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules),
3578
+ __computeForwardRuleScopedStrata(forwardRules),
3579
+ );
3224
3580
  agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
3225
3581
  agendaCursor = 0;
3226
3582
  }
@@ -3248,8 +3604,8 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3248
3604
  // Rules may have been added dynamically (rule-producing triples), possibly
3249
3605
  // introducing scoped builtins and/or higher closure priorities.
3250
3606
  maxScopedClosurePriorityNeeded = Math.max(
3251
- maxScopedClosurePriorityNeeded,
3252
3607
  __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules),
3608
+ __computeForwardRuleScopedStrata(forwardRules),
3253
3609
  );
3254
3610
 
3255
3611
  // If there are no scoped builtins in the entire program, Phase B is pure
@@ -3267,8 +3623,8 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3267
3623
 
3268
3624
  // Phase B can also derive rule-producing triples.
3269
3625
  maxScopedClosurePriorityNeeded = Math.max(
3270
- maxScopedClosurePriorityNeeded,
3271
3626
  __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules),
3627
+ __computeForwardRuleScopedStrata(forwardRules),
3272
3628
  );
3273
3629
 
3274
3630
  if (!changedA && !changedB && scopedClosureLevel >= maxScopedClosurePriorityNeeded) break;