eyeling 1.26.0 → 1.26.2

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/cli.js CHANGED
@@ -211,8 +211,8 @@ function main() {
211
211
  baseIri: __sourceLabelToBaseIri(sourceLabel),
212
212
  label: sourceLabel,
213
213
  collectUsedPrefixes: streamMode,
214
- collectSourceLocations: engine.getProofCommentsEnabled(),
215
214
  keepSourceArtifacts: false,
215
+ sourceLocations: engine.getProofCommentsEnabled(),
216
216
  rdf: rdfMode,
217
217
  }),
218
218
  );
@@ -368,7 +368,7 @@ function main() {
368
368
  }
369
369
 
370
370
  if (engine.getProofCommentsEnabled()) {
371
- process.stdout.write(engine.renderProofDocument(outDerived, derived, triples, prefixes, brules));
371
+ process.stdout.write(engine.renderProofDocument(outDerived, derived.concat(outDerived || []), triples, prefixes, brules));
372
372
  return;
373
373
  }
374
374
 
package/lib/engine.js CHANGED
@@ -25,6 +25,7 @@ const {
25
25
  Triple,
26
26
  Rule,
27
27
  DerivedFact,
28
+ varsInRule,
28
29
  internIri,
29
30
  collectBlankLabelsInTriples,
30
31
  copyQuotedGraphMetadata,
@@ -42,7 +43,7 @@ const EMPTY_LIST_TERM = new ListTerm([]);
42
43
  const { lex, N3SyntaxError } = require('./lexer');
43
44
  const { Parser } = require('./parser');
44
45
  const { liftBlankRuleVars } = require('./rules');
45
- const { parseN3SourceList } = require('./multisource');
46
+ const { parseN3SourceList, parseN3Text } = require('./multisource');
46
47
 
47
48
  const {
48
49
  makeBuiltins,
@@ -2435,217 +2436,6 @@ function unifyTriple(pat, fact, subst) {
2435
2436
  return s3;
2436
2437
  }
2437
2438
 
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
-
2649
2439
  // Strategy: when the substitution is "large" or search depth is high,
2650
2440
  // keep only bindings that are still relevant to:
2651
2441
  // - variables appearing in the remaining goals
@@ -3367,6 +3157,120 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
3367
3157
  return results;
3368
3158
  }
3369
3159
 
3160
+
3161
+ // Reconstruct one backward proof for N3 proof output. This is intentionally
3162
+ // called only by renderProofDocument(), so normal reasoning does not pay for it.
3163
+ function __proofTripleKey(tr) {
3164
+ if (!tr) return '';
3165
+ return `${skolemKeyFromTerm(tr.s)}\t${skolemKeyFromTerm(tr.p)}\t${skolemKeyFromTerm(tr.o)}`;
3166
+ }
3167
+
3168
+ function __copyProofSource(from, to) {
3169
+ if (from && to && Object.prototype.hasOwnProperty.call(from, '__source')) {
3170
+ Object.defineProperty(to, '__source', {
3171
+ value: from.__source,
3172
+ enumerable: false,
3173
+ writable: false,
3174
+ configurable: true,
3175
+ });
3176
+ }
3177
+ return to;
3178
+ }
3179
+
3180
+ function __annotateProofVarSourceNames(rule) {
3181
+ if (!rule) return rule;
3182
+ const sourceNames = Object.create(null);
3183
+ for (const name of varsInRule(rule)) {
3184
+ const m = /^(.*)__\d+$/.exec(name);
3185
+ sourceNames[name] = m ? m[1] : name;
3186
+ }
3187
+ Object.defineProperty(rule, '__proofVarSourceNames', {
3188
+ value: sourceNames,
3189
+ enumerable: false,
3190
+ writable: false,
3191
+ configurable: true,
3192
+ });
3193
+ return rule;
3194
+ }
3195
+
3196
+ function findBackwardProofForGoal(goal, facts, backRules, opts = {}) {
3197
+ const maxDepth = Number.isInteger(opts.maxDepth) && opts.maxDepth >= 0 ? opts.maxDepth : 64;
3198
+ const varGen = Array.isArray(opts.varGen) ? opts.varGen : [1];
3199
+ const visited = opts.visited instanceof Set ? opts.visited : new Set();
3200
+ const factList = Array.isArray(facts) ? facts : [];
3201
+ const ruleList = Array.isArray(backRules) ? backRules : [];
3202
+
3203
+ function matchingBaseFact(g) {
3204
+ const candidates = g && g.p instanceof Iri ? candidateFacts(factList, g) : null;
3205
+ const total = candidates ? candidates.totalLen : factList.length;
3206
+ for (let i = 0; i < total; i++) {
3207
+ let f;
3208
+ if (candidates) {
3209
+ if (i < candidates.exactLen) f = factList[candidates.exact[i]];
3210
+ else f = factList[candidates.wild[i - candidates.exactLen]];
3211
+ } else {
3212
+ f = factList[i];
3213
+ }
3214
+ if (unifyTriple(g, f, __emptySubst()) !== null) return f;
3215
+ }
3216
+ return null;
3217
+ }
3218
+
3219
+ function builtinProof(g) {
3220
+ if (!(g && isBuiltinPred(g.p))) return null;
3221
+ return { kind: 'builtin', fact: g, builtin: g.p };
3222
+ }
3223
+
3224
+ function solve(g, depth) {
3225
+ if (!g || depth > maxDepth) return null;
3226
+
3227
+ const base = matchingBaseFact(g);
3228
+ if (base) return { kind: 'fact', fact: base, source: base.__source };
3229
+
3230
+ const builtin = builtinProof(g);
3231
+ if (builtin) return builtin;
3232
+
3233
+ if (!(g.p instanceof Iri)) return null;
3234
+ const gKey = __proofTripleKey(g);
3235
+ if (visited.has(gKey)) return null;
3236
+ visited.add(gKey);
3237
+
3238
+ try {
3239
+ ensureBackRuleIndexes(ruleList);
3240
+ const candRules = (ruleList.__byHeadPred.get(g.p.__tid) || []).concat(ruleList.__wildHeadPred);
3241
+ for (const r of candRules) {
3242
+ if (!r || !Array.isArray(r.conclusion) || r.conclusion.length !== 1) continue;
3243
+ const rawHead = r.conclusion[0];
3244
+ if (rawHead.p instanceof Iri && rawHead.p.__tid !== g.p.__tid) continue;
3245
+
3246
+ const rStd = __annotateProofVarSourceNames(__copyProofSource(r, standardizeRule(r, varGen)));
3247
+ const head = rStd.conclusion[0];
3248
+ const s0 = unifyTriple(head, g, __emptySubst());
3249
+ if (s0 === null) continue;
3250
+
3251
+ const solutions = proveGoals(rStd.premise, s0, factList, ruleList, depth + 1, [], varGen, 1, {
3252
+ keepVars: varsInRule(rStd),
3253
+ deferBuiltins: true,
3254
+ });
3255
+ if (!solutions.length) continue;
3256
+
3257
+ const subst = solutions[0];
3258
+ const fact = applySubstTriple(head, subst);
3259
+ const premises = rStd.premise.map((prem) => applySubstTriple(prem, subst));
3260
+ const df = new DerivedFact(fact, rStd, premises, __cloneSubst(subst));
3261
+ const children = premises.map((prem) => builtinProof(prem) || solve(prem, depth + 1));
3262
+ return { kind: 'rule', df, children };
3263
+ }
3264
+ } finally {
3265
+ visited.delete(gKey);
3266
+ }
3267
+
3268
+ return null;
3269
+ }
3270
+
3271
+ return solve(goal, 0);
3272
+ }
3273
+
3370
3274
  // ===========================================================================
3371
3275
  // Forward chaining to fixpoint
3372
3276
  // ===========================================================================
@@ -4048,12 +3952,22 @@ function reasonStream(input, opts = {}) {
4048
3952
  skipUnsupportedRdfJs = false,
4049
3953
  builtinModules = null,
4050
3954
  rdf = false,
3955
+ sourceLabel = '<input>',
4051
3956
  } = opts;
4052
3957
 
4053
3958
  const useRdfCompatibility = !!rdf;
4054
3959
 
4055
- const parsedSourceList = parseN3SourceList(input, { baseIri, rdf: useRdfCompatibility });
4056
- const parsedInput = parsedSourceList || normalizeParsedReasonerInputSync(input);
3960
+ const parsedSourceList = parseN3SourceList(input, { baseIri, rdf: useRdfCompatibility, sourceLocations: proof });
3961
+ const parsedTextInput = (!parsedSourceList && proof && typeof input === 'string')
3962
+ ? parseN3Text(input, {
3963
+ baseIri: baseIri || '',
3964
+ label: sourceLabel || '<input>',
3965
+ keepSourceArtifacts: false,
3966
+ sourceLocations: true,
3967
+ rdf: useRdfCompatibility,
3968
+ })
3969
+ : null;
3970
+ const parsedInput = parsedSourceList || parsedTextInput || normalizeParsedReasonerInputSync(input);
4057
3971
  const rdfFactory = rdfjs ? getDataFactory(dataFactory) : null;
4058
3972
 
4059
3973
  const __oldEnforceHttps = deref.getEnforceHttpsEnabled();
@@ -4163,7 +4077,7 @@ function reasonStream(input, opts = {}) {
4163
4077
  const closureN3 = proof
4164
4078
  ? renderProofDocument(
4165
4079
  Array.isArray(logQueryRules) && logQueryRules.length ? queryDerived : derived,
4166
- derived,
4080
+ derived.concat(queryDerived || []),
4167
4081
  triples,
4168
4082
  prefixes,
4169
4083
  brules,
@@ -4216,7 +4130,7 @@ function reasonRdfJs(input, opts = {}) {
4216
4130
 
4217
4131
  Promise.resolve().then(async () => {
4218
4132
  try {
4219
- const normalizedInput = parseN3SourceList(input, restOpts) || (await normalizeReasonerInputAsync(input));
4133
+ const normalizedInput = parseN3SourceList(input, { ...restOpts, sourceLocations: !!(restOpts.proof || restOpts.proofComments) }) || (await normalizeReasonerInputAsync(input));
4220
4134
  reasonStream(normalizedInput, {
4221
4135
  ...restOpts,
4222
4136
  rdfjs: false,
package/lib/explain.js CHANGED
@@ -135,7 +135,6 @@ ${lineIndent(body, ' ')}
135
135
  }`;
136
136
  }
137
137
 
138
-
139
138
  function clonePrefixEnvWithProofVocabulary(prefixes) {
140
139
  const map = { ...(prefixes && prefixes.map ? prefixes.map : {}) };
141
140
  if (!map.pe) map.pe = PE_NS;
@@ -153,6 +152,11 @@ ${lineIndent(body, ' ')}
153
152
  return source.label.replace(/\\/g, '/').split('/').pop() || source.label;
154
153
  }
155
154
 
155
+ function sourceKeyForProof(source) {
156
+ if (!source) return '<unknown>';
157
+ return `${sourceLabelForProof(source)}:${Number.isInteger(source.line) ? source.line : ''}`;
158
+ }
159
+
156
160
  function byBlankNode(kind, source) {
157
161
  const src = source || {};
158
162
  const props = [`pe:${kind} ${n3String(sourceLabelForProof(src))}`];
@@ -160,121 +164,129 @@ ${lineIndent(body, ' ')}
160
164
  return `[ ${props.join('; ')} ]`;
161
165
  }
162
166
 
163
- function renderBindingList(df, prefixes) {
164
- if (!df || !df.rule || !df.subst) return '';
167
+ function renderBindingItems(df, prefixes) {
168
+ if (!df || !df.rule || !df.subst) return [];
165
169
  const ruleVars = varsInRule(df.rule);
166
170
  const visibleNames = Object.keys(df.subst)
167
171
  .filter((name) => ruleVars.has(name))
168
172
  .sort();
169
- if (!visibleNames.length) return '';
173
+ if (!visibleNames.length) return [];
170
174
  const sourceNames = (df.rule && df.rule.__proofVarSourceNames) || null;
171
- return visibleNames
172
- .map((name) => {
173
- const value = applySubstTerm(new Var(name), df.subst);
174
- const displayName = sourceNames && sourceNames[name] ? sourceNames[name] : name;
175
- return `[ pe:var ${n3String(displayName)}; pe:value ${termToN3(value, prefixes)} ]`;
176
- })
177
- .join(', ');
175
+ return visibleNames.map((name) => {
176
+ const value = applySubstTerm(new Var(name), df.subst);
177
+ const displayName = sourceNames && sourceNames[name] ? sourceNames[name] : name;
178
+ return `[ pe:var ${n3String(displayName)}; pe:value ${termToN3(value, prefixes)} ]`;
179
+ });
178
180
  }
179
181
 
180
- function proofSourceKey(source) {
181
- if (!source) return '';
182
- const label = typeof source.label === 'string' ? source.label : '';
183
- const line = Number.isInteger(source.line) ? source.line : 0;
184
- return `${label}\t${line}`;
182
+ function withLastLineSuffix(text, suffix) {
183
+ const lines = String(text).split(/\r?\n/);
184
+ lines[lines.length - 1] += suffix;
185
+ return lines.join('\n');
185
186
  }
186
187
 
187
- function proofEntryKey(kind, fact, source, extra) {
188
- return `${kind}\t${proofTripleKey(fact)}\t${proofSourceKey(source)}\t${extra || ''}`;
188
+ function renderPredicateObjects(predicate, objects, isLast) {
189
+ const values = (objects || []).filter((value) => value);
190
+ if (!values.length) return [];
191
+ const end = isLast ? '.' : ';';
192
+ if (values.length === 1 && !values[0].includes('\n')) {
193
+ return [` ${predicate} ${values[0]}${end}`];
194
+ }
195
+ const out = [` ${predicate}`];
196
+ for (let i = 0; i < values.length; i++) {
197
+ const suffix = i === values.length - 1 ? end : ',';
198
+ out.push(lineIndent(withLastLineSuffix(values[i], suffix), ' '));
199
+ }
200
+ return out;
189
201
  }
190
202
 
191
- function ruleEntryKey(df) {
192
- const rule = df && df.rule;
193
- const premises = (df && df.premises ? df.premises : []).map((prem) => proofTripleKey(prem)).join('\t');
194
- return proofEntryKey('rule', df && df.fact, rule && rule.__source, premises);
203
+ function proofEntryKey(entry) {
204
+ if (!entry) return '';
205
+ if (entry.kind === 'rule') {
206
+ const df = entry.df;
207
+ const source = df && df.rule && df.rule.__source;
208
+ const premiseKey = (df && df.premises ? df.premises : []).map(proofTripleKey).join('|');
209
+ return `rule:${proofTripleKey(df && df.fact)}:${sourceKeyForProof(source)}:${premiseKey}`;
210
+ }
211
+ if (entry.kind === 'builtin') return `builtin:${proofTripleKey(entry.fact)}`;
212
+ return `fact:${proofTripleKey(entry.fact)}:${sourceKeyForProof(entry.source)}`;
195
213
  }
196
214
 
197
215
  function collectProofEntries(rootDf, derivedByKey, baseFactByKey, resolveBackwardProof) {
198
216
  const entries = [];
199
217
  const seen = new Set();
200
218
 
201
- function entryKeyForProof(proof, fallbackTriple) {
202
- if (!proof) return proofEntryKey('fact', fallbackTriple, null);
203
- if (proof.kind === 'rule') return ruleEntryKey(proof.df);
204
- if (proof.kind === 'builtin') return proofEntryKey('builtin', proof.fact || fallbackTriple, null);
205
- return proofEntryKey('fact', proof.fact || fallbackTriple, proof.source);
219
+ function remember(entry) {
220
+ const key = proofEntryKey(entry);
221
+ if (!key || seen.has(key)) return false;
222
+ seen.add(key);
223
+ entries.push(entry);
224
+ return true;
225
+ }
226
+
227
+ function derivedCandidatesForKey(key) {
228
+ const found = derivedByKey.get(key);
229
+ if (!found) return [];
230
+ return Array.isArray(found) ? found : [found];
206
231
  }
207
232
 
208
- function visitProofNode(proof, fallbackTriple) {
233
+ function visitProofNode(proof, fallbackTriple, parentDf) {
209
234
  if (!proof) {
210
- visitFactTriple(fallbackTriple);
235
+ visitFactTriple(fallbackTriple, parentDf);
211
236
  return;
212
237
  }
213
- const key = entryKeyForProof(proof, fallbackTriple);
214
- if (!key || seen.has(key)) return;
215
238
  if (proof.kind === 'rule' && proof.df) {
216
239
  visitDerivedFact(proof.df, proof.children || null);
217
240
  return;
218
241
  }
219
- seen.add(key);
220
242
  if (proof.kind === 'builtin') {
221
- entries.push({
222
- kind: 'builtin',
223
- fact: proof.fact || fallbackTriple,
224
- builtin: proof.builtin || (proof.fact && proof.fact.p),
225
- });
226
- } else {
227
- entries.push({ kind: 'fact', fact: proof.fact || fallbackTriple, source: proof.source });
243
+ remember({ kind: 'builtin', fact: proof.fact || fallbackTriple, builtin: proof.builtin || (proof.fact && proof.fact.p) });
244
+ return;
228
245
  }
246
+ remember({ kind: 'fact', fact: proof.fact || fallbackTriple, source: proof.source });
229
247
  }
230
248
 
231
- function visitFactTriple(tr) {
232
- const tripleKey = proofTripleKey(tr);
233
- if (!tripleKey) return;
234
- const df = derivedByKey.get(tripleKey);
235
- if (df) {
236
- visitDerivedFact(df);
249
+ function visitFactTriple(tr, parentDf) {
250
+ const key = proofTripleKey(tr);
251
+ const base = baseFactByKey.get(key) || null;
252
+ if (base) {
253
+ remember({ kind: 'fact', fact: base, source: base.__source });
237
254
  return;
238
255
  }
239
- const base = baseFactByKey.get(tripleKey) || null;
240
- if (base) {
241
- const key = proofEntryKey('fact', base, base.__source);
242
- if (seen.has(key)) return;
243
- seen.add(key);
244
- entries.push({ kind: 'fact', fact: base, source: base.__source });
256
+
257
+ const candidates = derivedCandidatesForKey(key);
258
+ const df = candidates.find((candidate) => candidate !== parentDf) || null;
259
+ if (df) {
260
+ visitDerivedFact(df);
245
261
  return;
246
262
  }
263
+
247
264
  if (tr && isBuiltinPred(tr.p)) {
248
- const key = proofEntryKey('builtin', tr, null);
249
- if (seen.has(key)) return;
250
- seen.add(key);
251
- entries.push({ kind: 'builtin', fact: tr, builtin: tr.p });
265
+ remember({ kind: 'builtin', fact: tr, builtin: tr.p });
252
266
  return;
253
267
  }
268
+
254
269
  if (resolveBackwardProof) {
255
270
  const proof = resolveBackwardProof(tr);
256
271
  if (proof) {
257
- visitProofNode(proof, tr);
272
+ visitProofNode(proof, tr, parentDf);
258
273
  return;
259
274
  }
260
275
  }
261
- const key = proofEntryKey('fact', tr, null);
262
- if (seen.has(key)) return;
263
- seen.add(key);
264
- entries.push({ kind: 'fact', fact: tr, source: null });
276
+
277
+ remember({ kind: 'fact', fact: tr, source: null });
265
278
  }
266
279
 
267
280
  function visitDerivedFact(df, children) {
268
- const key = ruleEntryKey(df);
269
- if (!key || seen.has(key)) return;
270
- seen.add(key);
271
- entries.push({ kind: 'rule', df });
281
+ if (!df || !df.fact) return;
282
+ const entry = { kind: 'rule', df };
283
+ if (!remember(entry)) return;
272
284
 
273
285
  if (Array.isArray(children) && children.length) {
274
- for (const child of children) visitProofNode(child);
286
+ for (const child of children) visitProofNode(child, null, df);
275
287
  return;
276
288
  }
277
- for (const prem of df.premises || []) visitFactTriple(prem);
289
+ for (const prem of df.premises || []) visitFactTriple(prem, df);
278
290
  }
279
291
 
280
292
  visitDerivedFact(rootDf);
@@ -304,16 +316,18 @@ ${lineIndent(body, ' ')}
304
316
  }
305
317
 
306
318
  const df = entry.df;
307
- const props = [`pe:by ${byBlankNode('rule', df.rule && df.rule.__source)}`];
308
- const bindings = renderBindingList(df, prefixes);
309
- if (bindings) props.push(`pe:binding ${bindings}`);
310
- if (df.premises && df.premises.length) {
311
- props.push(`pe:uses ${df.premises.map((prem) => graphForTriple(prem, prefixes)).join(', ')}`);
312
- }
319
+ const bindingItems = renderBindingItems(df, prefixes);
320
+ const useItems = (df.premises || []).map((prem) => graphForTriple(prem, prefixes));
321
+ const propertyGroups = [
322
+ { predicate: 'pe:by', objects: [byBlankNode('rule', df.rule && df.rule.__source)] },
323
+ { predicate: 'pe:binding', objects: bindingItems },
324
+ { predicate: 'pe:uses', objects: useItems },
325
+ ].filter((group) => group.objects.length);
313
326
 
314
327
  let out = ` ${graphForTriple(df.fact, prefixes)}\n`;
315
- for (let i = 0; i < props.length; i++) {
316
- out += ` ${props[i]}${i === props.length - 1 ? '.' : ';'}\n`;
328
+ for (let i = 0; i < propertyGroups.length; i++) {
329
+ const group = propertyGroups[i];
330
+ out += renderPredicateObjects(group.predicate, group.objects, i === propertyGroups.length - 1).join('\n') + '\n';
317
331
  }
318
332
  return out.trimEnd();
319
333
  }
@@ -334,13 +348,18 @@ ${proofBody}
334
348
 
335
349
  const proofPrefixes = clonePrefixEnvWithProofVocabulary(prefixes);
336
350
  const derivedByKey = new Map();
337
- for (const df of allDerived || []) {
338
- if (!df || !df.fact) continue;
351
+ function addDerived(df) {
352
+ if (!df || !df.fact) return;
339
353
  const key = proofTripleKey(df.fact);
340
- if (!derivedByKey.has(key)) derivedByKey.set(key, df);
354
+ let bucket = derivedByKey.get(key);
355
+ if (!bucket) {
356
+ bucket = [];
357
+ derivedByKey.set(key, bucket);
358
+ }
359
+ if (!bucket.includes(df)) bucket.push(df);
341
360
  }
342
- // Do not insert query-wrapper output facts here: those wrappers often derive
343
- // the same triple they use, and would hide the underlying fact/rule proof.
361
+ for (const df of allDerived || []) addDerived(df);
362
+ for (const df of selectedDerived) addDerived(df);
344
363
 
345
364
  const baseFactByKey = new Map();
346
365
  for (const tr of baseFacts || []) {