eyeling 1.25.4 → 1.26.0

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/index.d.ts CHANGED
@@ -151,6 +151,7 @@ declare module 'eyeling' {
151
151
  }
152
152
 
153
153
  export interface ReasonOptions {
154
+ proof?: boolean;
154
155
  proofComments?: boolean;
155
156
  noProofComments?: boolean;
156
157
  args?: string[];
package/index.js CHANGED
@@ -19,21 +19,24 @@ function reason(opt = {}, input = '') {
19
19
 
20
20
  const args = [];
21
21
 
22
- // default: proof comments OFF for API output (machine-friendly)
23
- // set { proofComments: true } to keep them
24
- const proofCommentsSpecified = typeof opt.proofComments === 'boolean' || typeof opt.noProofComments === 'boolean';
25
-
26
- const proofComments =
27
- typeof opt.proofComments === 'boolean'
28
- ? opt.proofComments
29
- : typeof opt.noProofComments === 'boolean'
30
- ? !opt.noProofComments
31
- : false;
22
+ // default: proof output OFF for API output (machine-friendly)
23
+ // set { proof: true } to include N3 proof explanations.
24
+ // proofComments/noProofComments are accepted as legacy aliases.
25
+ const proofSpecified =
26
+ typeof opt.proof === 'boolean' || typeof opt.proofComments === 'boolean' || typeof opt.noProofComments === 'boolean';
27
+
28
+ const proof =
29
+ typeof opt.proof === 'boolean'
30
+ ? opt.proof
31
+ : typeof opt.proofComments === 'boolean'
32
+ ? opt.proofComments
33
+ : typeof opt.noProofComments === 'boolean'
34
+ ? !opt.noProofComments
35
+ : false;
32
36
 
33
37
  // Only pass a flag when the caller explicitly asked.
34
- // (CLI default is now: no proof comments.)
35
- if (proofCommentsSpecified) {
36
- if (proofComments) args.push('--proof-comments');
38
+ if (proofSpecified) {
39
+ if (proof) args.push('--proof');
37
40
  else args.push('--no-proof-comments');
38
41
  }
39
42
 
package/lib/cli.js CHANGED
@@ -106,7 +106,7 @@ function main() {
106
106
  ` -d, --deterministic-skolem Make log:skolem stable across reasoning runs.\n` +
107
107
  ` -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
108
108
  ` -h, --help Show this help and exit.\n` +
109
- ` -p, --proof-comments Enable proof explanations.\n` +
109
+ ` -p, --proof Enable proof explanations.\n` +
110
110
  ` -r, --rdf Enable RDF/TriG input/output compatibility.\n` +
111
111
  ` -s, --super-restricted Disable all builtins except => and <=.\n` +
112
112
  ` -t, --stream Stream derived triples as soon as they are derived.\n` +
@@ -161,8 +161,8 @@ function main() {
161
161
  if (typeof engine.setDeterministicSkolemEnabled === 'function') engine.setDeterministicSkolemEnabled(true);
162
162
  }
163
163
 
164
- // --proof-comments / -p: enable proof explanations
165
- if (argv.includes('--proof-comments') || argv.includes('-p')) {
164
+ // --proof / -p: enable proof explanations as N3 proof graphs
165
+ if (argv.includes('--proof') || argv.includes('--proof-comments') || argv.includes('-p')) {
166
166
  engine.setProofCommentsEnabled(true);
167
167
  }
168
168
 
@@ -211,6 +211,7 @@ function main() {
211
211
  baseIri: __sourceLabelToBaseIri(sourceLabel),
212
212
  label: sourceLabel,
213
213
  collectUsedPrefixes: streamMode,
214
+ collectSourceLocations: engine.getProofCommentsEnabled(),
214
215
  keepSourceArtifacts: false,
215
216
  rdf: rdfMode,
216
217
  }),
@@ -301,7 +302,7 @@ function main() {
301
302
  const hasQueries = Array.isArray(qrules) && qrules.length;
302
303
  const mayAutoRenderOutputStrings = programMayProduceOutputStrings(triples, frules, qrules);
303
304
 
304
- if (streamMode && !hasQueries && !mayAutoRenderOutputStrings) {
305
+ if (streamMode && !hasQueries && !mayAutoRenderOutputStrings && !engine.getProofCommentsEnabled()) {
305
306
  const usedInInput = mergedDocument.usedPrefixes instanceof Set ? new Set(mergedDocument.usedPrefixes) : new Set();
306
307
  const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
307
308
 
@@ -366,6 +367,11 @@ function main() {
366
367
  return;
367
368
  }
368
369
 
370
+ if (engine.getProofCommentsEnabled()) {
371
+ process.stdout.write(engine.renderProofDocument(outDerived, derived, triples, prefixes, brules));
372
+ return;
373
+ }
374
+
369
375
  let bodyText = '';
370
376
  if (rdfMode && !engine.getProofCommentsEnabled()) {
371
377
  bodyText = outTriples.map((tr) => engine.tripleToRdfCompatible(tr, prefixes)).join('\n');
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;
@@ -628,9 +780,11 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
628
780
  });
629
781
 
630
782
  // Initialize proof/output helpers (implemented in lib/explain.js).
631
- const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
783
+ const { printExplanation, renderProofDocument, collectOutputStringsFromFacts } = makeExplain({
632
784
  applySubstTerm,
633
785
  skolemKeyFromTerm,
786
+ isBuiltinPred,
787
+ findBackwardProofForGoal,
634
788
  });
635
789
 
636
790
  function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
@@ -1669,7 +1823,7 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1669
1823
  goalPredTid: goal.p instanceof Iri ? goal.p.__tid : null,
1670
1824
  goalSKey,
1671
1825
  goalOKey,
1672
- needsSkipCheck: !!r.__needsForwardSkipCheck,
1826
+ needsSkipCheck: !!(r.__needsForwardSkipCheck || (r.__scopedStratumInfo && r.__scopedStratumInfo.needsSnap)),
1673
1827
  fastSubjectVar,
1674
1828
  fastObjectVar,
1675
1829
  };
@@ -2281,6 +2435,217 @@ function unifyTriple(pat, fact, subst) {
2281
2435
  return s3;
2282
2436
  }
2283
2437
 
2438
+
2439
+ function __copyProofSourceMetadata(from, to) {
2440
+ if (!from || !to) return to;
2441
+ for (const key of ['__sourceOffset', '__source']) {
2442
+ if (hasOwn.call(from, key)) {
2443
+ Object.defineProperty(to, key, {
2444
+ value: from[key],
2445
+ enumerable: false,
2446
+ writable: false,
2447
+ configurable: true,
2448
+ });
2449
+ }
2450
+ }
2451
+ return to;
2452
+ }
2453
+
2454
+ function __standardizeRuleForProof(rule, gen) {
2455
+ const proofVarNames = Object.create(null);
2456
+
2457
+ function renameTerm(t, vmapName, vmapVar, genArr) {
2458
+ if (t instanceof Var) {
2459
+ if (!hasOwn.call(vmapVar, t.name)) {
2460
+ const name = `${t.name}__proof_${genArr[0]}`;
2461
+ genArr[0] += 1;
2462
+ vmapName[t.name] = name;
2463
+ vmapVar[t.name] = new Var(name);
2464
+ proofVarNames[name] = t.name;
2465
+ }
2466
+ return vmapVar[t.name];
2467
+ }
2468
+ if (t instanceof ListTerm) {
2469
+ let changed = false;
2470
+ const elems2 = t.elems.map((e) => {
2471
+ const e2 = renameTerm(e, vmapName, vmapVar, genArr);
2472
+ if (e2 !== e) changed = true;
2473
+ return e2;
2474
+ });
2475
+ return changed ? new ListTerm(elems2) : t;
2476
+ }
2477
+ if (t instanceof OpenListTerm) {
2478
+ let changed = false;
2479
+ const newXs = t.prefix.map((e) => {
2480
+ const e2 = renameTerm(e, vmapName, vmapVar, genArr);
2481
+ if (e2 !== e) changed = true;
2482
+ return e2;
2483
+ });
2484
+ if (!hasOwn.call(vmapName, t.tailVar)) {
2485
+ const name = `${t.tailVar}__proof_${genArr[0]}`;
2486
+ genArr[0] += 1;
2487
+ vmapName[t.tailVar] = name;
2488
+ vmapVar[t.tailVar] = new Var(name);
2489
+ proofVarNames[name] = t.tailVar;
2490
+ }
2491
+ const newTail = vmapName[t.tailVar];
2492
+ if (newTail !== t.tailVar) changed = true;
2493
+ return changed ? new OpenListTerm(newXs, newTail) : t;
2494
+ }
2495
+ if (t instanceof GraphTerm) {
2496
+ let changed = false;
2497
+ const triples2 = t.triples.map((tr) => {
2498
+ const s2 = renameTerm(tr.s, vmapName, vmapVar, genArr);
2499
+ const p2 = renameTerm(tr.p, vmapName, vmapVar, genArr);
2500
+ const o2 = renameTerm(tr.o, vmapName, vmapVar, genArr);
2501
+ if (s2 !== tr.s || p2 !== tr.p || o2 !== tr.o) changed = true;
2502
+ return s2 === tr.s && p2 === tr.p && o2 === tr.o ? tr : new Triple(s2, p2, o2);
2503
+ });
2504
+ return changed ? copyQuotedGraphMetadata(t, new GraphTerm(triples2)) : t;
2505
+ }
2506
+ return t;
2507
+ }
2508
+
2509
+ const vmapName = Object.create(null);
2510
+ const vmapVar = Object.create(null);
2511
+ const premise = rule.premise.map((tr) => {
2512
+ const s2 = renameTerm(tr.s, vmapName, vmapVar, gen);
2513
+ const p2 = renameTerm(tr.p, vmapName, vmapVar, gen);
2514
+ const o2 = renameTerm(tr.o, vmapName, vmapVar, gen);
2515
+ return s2 === tr.s && p2 === tr.p && o2 === tr.o ? tr : new Triple(s2, p2, o2);
2516
+ });
2517
+ const conclusion = rule.conclusion.map((tr) => {
2518
+ const s2 = renameTerm(tr.s, vmapName, vmapVar, gen);
2519
+ const p2 = renameTerm(tr.p, vmapName, vmapVar, gen);
2520
+ const o2 = renameTerm(tr.o, vmapName, vmapVar, gen);
2521
+ return s2 === tr.s && p2 === tr.p && o2 === tr.o ? tr : new Triple(s2, p2, o2);
2522
+ });
2523
+ const out = new Rule(premise, conclusion, rule.isForward, rule.isFuse, rule.headBlankLabels);
2524
+ Object.defineProperty(out, '__proofVarSourceNames', {
2525
+ value: proofVarNames,
2526
+ enumerable: false,
2527
+ writable: false,
2528
+ configurable: true,
2529
+ });
2530
+ return __copyProofSourceMetadata(rule, out);
2531
+ }
2532
+
2533
+ function __mergeProofSubst(base, delta) {
2534
+ const out = __cloneSubst(base);
2535
+ for (const k in delta || {}) {
2536
+ if (!hasOwn.call(delta, k)) continue;
2537
+ const v = applySubstTerm(delta[k], out);
2538
+ if (hasOwn.call(out, k)) {
2539
+ if (!termsEqual(applySubstTerm(out[k], out), v)) return null;
2540
+ } else {
2541
+ out[k] = v;
2542
+ }
2543
+ }
2544
+ return out;
2545
+ }
2546
+
2547
+ function __sourceSortKey(obj) {
2548
+ const src = obj && obj.__source;
2549
+ const label = src && typeof src.label === 'string' ? src.label : '';
2550
+ const line = src && Number.isInteger(src.line) ? src.line : 0;
2551
+ return `${label}\t${line}`;
2552
+ }
2553
+
2554
+ function __candidateFactsForProof(facts, goal) {
2555
+ if (!Array.isArray(facts)) return [];
2556
+ ensureFactIndexes(facts);
2557
+ const candidates = candidateFacts(facts, goal);
2558
+ if (!candidates) return facts;
2559
+ const out = [];
2560
+ for (let i = 0; i < candidates.exactLen; i++) out.push(facts[candidates.exact[i]]);
2561
+ for (let i = 0; i < candidates.wildLen; i++) out.push(facts[candidates.wild[i]]);
2562
+ return out;
2563
+ }
2564
+
2565
+ function findBackwardProofForGoal(goal, facts, backRules, opts = {}) {
2566
+ const maxDepth = Number.isInteger(opts.maxDepth) && opts.maxDepth > 0 ? opts.maxDepth : 64;
2567
+ const varGen = [0];
2568
+
2569
+ function proofKey(tr) {
2570
+ return `${skolemKeyFromTerm(tr.s)}\t${skolemKeyFromTerm(tr.p)}\t${skolemKeyFromTerm(tr.o)}`;
2571
+ }
2572
+
2573
+ function proveGoal(goal0, subst, depth, visited) {
2574
+ if (depth > maxDepth) return [];
2575
+ const g = applySubstTriple(goal0, subst);
2576
+ const key = proofKey(g);
2577
+ if (visited.has(key)) return [];
2578
+
2579
+ if (isBuiltinPred(g.p)) {
2580
+ const builtinGoalForEval = g.p instanceof Iri && g.p.value === LOG_NS + 'rawType' ? goal0 : g;
2581
+ const builtinSubstForEval = g.p instanceof Iri && g.p.value === LOG_NS + 'rawType' ? subst : __emptySubst();
2582
+ const deltas = evalBuiltin(builtinGoalForEval, builtinSubstForEval, facts, backRules, depth, varGen, 1) || [];
2583
+ const out = [];
2584
+ for (const delta of deltas) {
2585
+ const subst2 = __mergeProofSubst(subst, delta);
2586
+ if (subst2 === null) continue;
2587
+ const fact = applySubstTriple(g, subst2);
2588
+ out.push({ subst: subst2, proof: { kind: 'builtin', fact, builtin: fact.p } });
2589
+ break;
2590
+ }
2591
+ return out;
2592
+ }
2593
+
2594
+ const factResults = [];
2595
+ for (const f of __candidateFactsForProof(facts, g)) {
2596
+ const subst2 = unifyTriple(g, f, subst);
2597
+ if (subst2 === null) continue;
2598
+ factResults.push({ subst: subst2, proof: { kind: 'fact', fact: applySubstTriple(f, subst2), source: f.__source } });
2599
+ break;
2600
+ }
2601
+ if (factResults.length) return factResults;
2602
+
2603
+ if (!(g.p instanceof Iri)) return [];
2604
+ ensureBackRuleIndexes(backRules);
2605
+ const rules = (backRules.__byHeadPred.get(g.p.__tid) || []).concat(backRules.__wildHeadPred || []);
2606
+ rules.sort((a, b) => (__sourceSortKey(a) < __sourceSortKey(b) ? -1 : __sourceSortKey(a) > __sourceSortKey(b) ? 1 : 0));
2607
+
2608
+ const visited2 = new Set(visited);
2609
+ visited2.add(key);
2610
+
2611
+ for (const r of rules) {
2612
+ if (!r || !Array.isArray(r.conclusion) || r.conclusion.length !== 1) continue;
2613
+ const rStd = __standardizeRuleForProof(r, varGen);
2614
+ const head = rStd.conclusion[0];
2615
+ if (head.p instanceof Iri && head.p.__tid !== g.p.__tid) continue;
2616
+ const subst1 = unifyTriple(head, g, subst);
2617
+ if (subst1 === null) continue;
2618
+ const bodyProof = proveGoalList(rStd.premise || [], subst1, depth + 1, visited2);
2619
+ if (!bodyProof) continue;
2620
+ const fact = applySubstTriple(g, bodyProof.subst);
2621
+ const premises = (rStd.premise || []).map((prem) => applySubstTriple(prem, bodyProof.subst));
2622
+ const df = new DerivedFact(fact, rStd, premises, bodyProof.subst);
2623
+ return [{ subst: bodyProof.subst, proof: { kind: 'rule', df, children: bodyProof.proofs } }];
2624
+ }
2625
+
2626
+ return [];
2627
+ }
2628
+
2629
+ function proveGoalList(goals, subst, depth, visited) {
2630
+ let states = [{ subst, proofs: [] }];
2631
+ for (const goal1 of goals || []) {
2632
+ const next = [];
2633
+ for (const state of states) {
2634
+ const proofs = proveGoal(goal1, state.subst, depth, visited);
2635
+ for (const pr of proofs) next.push({ subst: pr.subst, proofs: state.proofs.concat([pr.proof]) });
2636
+ }
2637
+ if (!next.length) return null;
2638
+ // Keep proof reconstruction deterministic and cheap: one proof is enough
2639
+ // for an explanation of the already-derived enclosing fact.
2640
+ states = [next[0]];
2641
+ }
2642
+ return states[0] || { subst, proofs: [] };
2643
+ }
2644
+
2645
+ const found = proveGoal(goal, __emptySubst(), 0, new Set());
2646
+ return found.length ? found[0].proof : null;
2647
+ }
2648
+
2284
2649
  // Strategy: when the substitution is "large" or search depth is high,
2285
2650
  // keep only bindings that are still relevant to:
2286
2651
  // - variables appearing in the remaining goals
@@ -3117,7 +3482,10 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3117
3482
  let scopedClosureLevel = 0;
3118
3483
 
3119
3484
  // Scan known rules for the maximum requested closure priority in scoped log:* goals.
3120
- let maxScopedClosurePriorityNeeded = __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules);
3485
+ let maxScopedClosurePriorityNeeded = Math.max(
3486
+ __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules),
3487
+ __computeForwardRuleScopedStrata(forwardRules),
3488
+ );
3121
3489
 
3122
3490
  // Conservative fast-skip for forward rules that cannot possibly succeed
3123
3491
  // until a scoped snapshot exists (or a given closure level is reached).
@@ -3171,12 +3539,16 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3171
3539
  // until a snapshot exists (and a certain closure level is reached).
3172
3540
  // This prevents expensive proofs that will definitely fail in Phase A
3173
3541
  // and in early closure levels.
3174
- const info = r.__scopedSkipInfo;
3542
+ const info = r.__scopedStratumInfo || r.__scopedSkipInfo;
3175
3543
  if (info && info.needsSnap) {
3176
3544
  const snapHere = facts.__scopedSnapshot || null;
3177
3545
  const lvlHere = (facts && typeof facts.__scopedClosureLevel === 'number' && facts.__scopedClosureLevel) || 0;
3178
3546
  if (!snapHere) return true;
3179
- if (lvlHere < info.requiredLevel) return true;
3547
+ if (info.exactLevel) {
3548
+ if (lvlHere !== info.requiredLevel) return true;
3549
+ } else if (lvlHere < info.requiredLevel) {
3550
+ return true;
3551
+ }
3180
3552
  }
3181
3553
 
3182
3554
  // Optimization: if the rule head is **structurally ground** (no vars anywhere, even inside
@@ -3376,6 +3748,10 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3376
3748
 
3377
3749
  const outcome = __emitForwardRuleSolution(r, entry.ruleIndex, s);
3378
3750
  if (outcome.rulesChanged) {
3751
+ maxScopedClosurePriorityNeeded = Math.max(
3752
+ __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules),
3753
+ __computeForwardRuleScopedStrata(forwardRules),
3754
+ );
3379
3755
  agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
3380
3756
  agendaCursor = 0;
3381
3757
  }
@@ -3389,7 +3765,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3389
3765
  for (let i = 0; i < forwardRules.length; i++) {
3390
3766
  const r = forwardRules[i];
3391
3767
  if (agendaIndex.indexed.has(r)) continue;
3392
- if (r.__needsForwardSkipCheck && __skipForwardRuleNow(r)) continue;
3768
+ if ((r.__needsForwardSkipCheck || (r.__scopedStratumInfo && r.__scopedStratumInfo.needsSnap)) && __skipForwardRuleNow(r)) continue;
3393
3769
 
3394
3770
  const headIsStrictGround = r.__headIsStrictGround;
3395
3771
  const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
@@ -3410,6 +3786,10 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3410
3786
  for (const s of sols) {
3411
3787
  const outcome = __emitForwardRuleSolution(r, i, s);
3412
3788
  if (outcome.rulesChanged) {
3789
+ maxScopedClosurePriorityNeeded = Math.max(
3790
+ __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules),
3791
+ __computeForwardRuleScopedStrata(forwardRules),
3792
+ );
3413
3793
  agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
3414
3794
  agendaCursor = 0;
3415
3795
  }
@@ -3437,8 +3817,8 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3437
3817
  // Rules may have been added dynamically (rule-producing triples), possibly
3438
3818
  // introducing scoped builtins and/or higher closure priorities.
3439
3819
  maxScopedClosurePriorityNeeded = Math.max(
3440
- maxScopedClosurePriorityNeeded,
3441
3820
  __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules),
3821
+ __computeForwardRuleScopedStrata(forwardRules),
3442
3822
  );
3443
3823
 
3444
3824
  // If there are no scoped builtins in the entire program, Phase B is pure
@@ -3456,8 +3836,8 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3456
3836
 
3457
3837
  // Phase B can also derive rule-producing triples.
3458
3838
  maxScopedClosurePriorityNeeded = Math.max(
3459
- maxScopedClosurePriorityNeeded,
3460
3839
  __computeMaxScopedClosurePriorityNeeded(forwardRules, backRules),
3840
+ __computeForwardRuleScopedStrata(forwardRules),
3461
3841
  );
3462
3842
 
3463
3843
  if (!changedA && !changedB && scopedClosureLevel >= maxScopedClosurePriorityNeeded) break;
@@ -3780,11 +4160,19 @@ function reasonStream(input, opts = {}) {
3780
4160
  ? facts
3781
4161
  : derived.map((d) => d.fact);
3782
4162
 
3783
- const closureN3 = useRdfCompatibility
3784
- ? closureTriples.map((t) => tripleToRdfCompatible(t, prefixes)).join('\n')
3785
- : Array.isArray(logQueryRules) && logQueryRules.length && !proof
3786
- ? prettyPrintQueryTriples(closureTriples, prefixes)
3787
- : closureTriples.map((t) => tripleToN3(t, prefixes)).join('\n');
4163
+ const closureN3 = proof
4164
+ ? renderProofDocument(
4165
+ Array.isArray(logQueryRules) && logQueryRules.length ? queryDerived : derived,
4166
+ derived,
4167
+ triples,
4168
+ prefixes,
4169
+ brules,
4170
+ ).replace(/\n$/g, '')
4171
+ : useRdfCompatibility
4172
+ ? closureTriples.map((t) => tripleToRdfCompatible(t, prefixes)).join('\n')
4173
+ : Array.isArray(logQueryRules) && logQueryRules.length
4174
+ ? prettyPrintQueryTriples(closureTriples, prefixes)
4175
+ : closureTriples.map((t) => tripleToN3(t, prefixes)).join('\n');
3788
4176
 
3789
4177
  const __out = {
3790
4178
  prefixes,
@@ -3911,6 +4299,7 @@ module.exports = {
3911
4299
  collectLogQueryConclusions,
3912
4300
  forwardChainAndCollectLogQueryConclusions,
3913
4301
  collectOutputStringsFromFacts,
4302
+ renderProofDocument,
3914
4303
  main,
3915
4304
  version,
3916
4305
  N3SyntaxError,
package/lib/entry.js CHANGED
@@ -32,6 +32,7 @@ module.exports = {
32
32
  materializeRdfLists: engine.materializeRdfLists,
33
33
  isGroundTriple: engine.isGroundTriple,
34
34
  printExplanation: engine.printExplanation,
35
+ renderProofDocument: engine.renderProofDocument,
35
36
  tripleToN3: engine.tripleToN3,
36
37
  collectOutputStringsFromFacts: engine.collectOutputStringsFromFacts,
37
38
  prettyPrintQueryTriples: engine.prettyPrintQueryTriples,