eyeling 1.25.5 → 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
@@ -780,9 +780,11 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
780
780
  });
781
781
 
782
782
  // Initialize proof/output helpers (implemented in lib/explain.js).
783
- const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
783
+ const { printExplanation, renderProofDocument, collectOutputStringsFromFacts } = makeExplain({
784
784
  applySubstTerm,
785
785
  skolemKeyFromTerm,
786
+ isBuiltinPred,
787
+ findBackwardProofForGoal,
786
788
  });
787
789
 
788
790
  function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
@@ -2433,6 +2435,217 @@ function unifyTriple(pat, fact, subst) {
2433
2435
  return s3;
2434
2436
  }
2435
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
+
2436
2649
  // Strategy: when the substitution is "large" or search depth is high,
2437
2650
  // keep only bindings that are still relevant to:
2438
2651
  // - variables appearing in the remaining goals
@@ -3947,11 +4160,19 @@ function reasonStream(input, opts = {}) {
3947
4160
  ? facts
3948
4161
  : derived.map((d) => d.fact);
3949
4162
 
3950
- const closureN3 = useRdfCompatibility
3951
- ? closureTriples.map((t) => tripleToRdfCompatible(t, prefixes)).join('\n')
3952
- : Array.isArray(logQueryRules) && logQueryRules.length && !proof
3953
- ? prettyPrintQueryTriples(closureTriples, prefixes)
3954
- : 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');
3955
4176
 
3956
4177
  const __out = {
3957
4178
  prefixes,
@@ -4078,6 +4299,7 @@ module.exports = {
4078
4299
  collectLogQueryConclusions,
4079
4300
  forwardChainAndCollectLogQueryConclusions,
4080
4301
  collectOutputStringsFromFacts,
4302
+ renderProofDocument,
4081
4303
  main,
4082
4304
  version,
4083
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,
package/lib/explain.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  'use strict';
8
8
 
9
- const { LOG_NS, Literal, Iri, Blank, Var, varsInRule, literalParts } = require('./prelude');
9
+ const { LOG_NS, Literal, Iri, Blank, Var, GraphTerm, varsInRule, literalParts, PrefixEnv } = require('./prelude');
10
10
 
11
11
  const { termToN3, tripleToN3 } = require('./printing');
12
12
  const { parseNumericLiteralInfo, termToJsString } = require('./builtins');
@@ -14,6 +14,8 @@ const { parseNumericLiteralInfo, termToJsString } = require('./builtins');
14
14
  function makeExplain(deps) {
15
15
  const applySubstTerm = deps.applySubstTerm;
16
16
  const skolemKeyFromTerm = deps.skolemKeyFromTerm;
17
+ const isBuiltinPred = typeof deps.isBuiltinPred === 'function' ? deps.isBuiltinPred : () => false;
18
+ const findBackwardProofForGoal = typeof deps.findBackwardProofForGoal === 'function' ? deps.findBackwardProofForGoal : null;
17
19
 
18
20
  function printExplanation(df, prefixes) {
19
21
  console.log('# ----------------------------------------------------------------------');
@@ -111,6 +113,282 @@ function makeExplain(deps) {
111
113
  console.log('# ----------------------------------------------------------------------\n');
112
114
  }
113
115
 
116
+
117
+ const PE_NS = 'https://eyereasoner.github.io/pe#';
118
+
119
+ function n3String(value) {
120
+ return JSON.stringify(String(value));
121
+ }
122
+
123
+ function lineIndent(text, prefix) {
124
+ return String(text)
125
+ .split(/\r?\n/)
126
+ .map((line) => (line.length ? prefix + line : line))
127
+ .join('\n');
128
+ }
129
+
130
+ function graphForTriple(tr, prefixes) {
131
+ const body = tripleToN3(tr, prefixes).trimEnd();
132
+ if (!body.includes('\n')) return `{ ${body} }`;
133
+ return `{
134
+ ${lineIndent(body, ' ')}
135
+ }`;
136
+ }
137
+
138
+
139
+ function clonePrefixEnvWithProofVocabulary(prefixes) {
140
+ const map = { ...(prefixes && prefixes.map ? prefixes.map : {}) };
141
+ if (!map.pe) map.pe = PE_NS;
142
+ return new PrefixEnv(map, (prefixes && prefixes.baseIri) || '');
143
+ }
144
+
145
+ function proofTripleKey(tr) {
146
+ if (!tr) return '';
147
+ return `${skolemKeyFromTerm(tr.s)}\t${skolemKeyFromTerm(tr.p)}\t${skolemKeyFromTerm(tr.o)}`;
148
+ }
149
+
150
+ function sourceLabelForProof(source) {
151
+ if (!source || typeof source.label !== 'string' || !source.label) return '<unknown>';
152
+ // Keep proof output stable when the CLI is invoked with paths such as examples/foo.n3.
153
+ return source.label.replace(/\\/g, '/').split('/').pop() || source.label;
154
+ }
155
+
156
+ function byBlankNode(kind, source) {
157
+ const src = source || {};
158
+ const props = [`pe:${kind} ${n3String(sourceLabelForProof(src))}`];
159
+ if (Number.isInteger(src.line) && src.line > 0) props.push(`pe:line ${src.line}`);
160
+ return `[ ${props.join('; ')} ]`;
161
+ }
162
+
163
+ function renderBindingList(df, prefixes) {
164
+ if (!df || !df.rule || !df.subst) return '';
165
+ const ruleVars = varsInRule(df.rule);
166
+ const visibleNames = Object.keys(df.subst)
167
+ .filter((name) => ruleVars.has(name))
168
+ .sort();
169
+ if (!visibleNames.length) return '';
170
+ 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(', ');
178
+ }
179
+
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}`;
185
+ }
186
+
187
+ function proofEntryKey(kind, fact, source, extra) {
188
+ return `${kind}\t${proofTripleKey(fact)}\t${proofSourceKey(source)}\t${extra || ''}`;
189
+ }
190
+
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);
195
+ }
196
+
197
+ function collectProofEntries(rootDf, derivedByKey, baseFactByKey, resolveBackwardProof) {
198
+ const entries = [];
199
+ const seen = new Set();
200
+
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);
206
+ }
207
+
208
+ function visitProofNode(proof, fallbackTriple) {
209
+ if (!proof) {
210
+ visitFactTriple(fallbackTriple);
211
+ return;
212
+ }
213
+ const key = entryKeyForProof(proof, fallbackTriple);
214
+ if (!key || seen.has(key)) return;
215
+ if (proof.kind === 'rule' && proof.df) {
216
+ visitDerivedFact(proof.df, proof.children || null);
217
+ return;
218
+ }
219
+ seen.add(key);
220
+ 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 });
228
+ }
229
+ }
230
+
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);
237
+ return;
238
+ }
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 });
245
+ return;
246
+ }
247
+ 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 });
252
+ return;
253
+ }
254
+ if (resolveBackwardProof) {
255
+ const proof = resolveBackwardProof(tr);
256
+ if (proof) {
257
+ visitProofNode(proof, tr);
258
+ return;
259
+ }
260
+ }
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 });
265
+ }
266
+
267
+ 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 });
272
+
273
+ if (Array.isArray(children) && children.length) {
274
+ for (const child of children) visitProofNode(child);
275
+ return;
276
+ }
277
+ for (const prem of df.premises || []) visitFactTriple(prem);
278
+ }
279
+
280
+ visitDerivedFact(rootDf);
281
+ return entries;
282
+ }
283
+
284
+ function collectProofOutputTriples(outputDerived) {
285
+ const out = [];
286
+ const seen = new Set();
287
+ for (const df of outputDerived || []) {
288
+ if (!df || !df.fact) continue;
289
+ const key = proofTripleKey(df.fact);
290
+ if (seen.has(key)) continue;
291
+ seen.add(key);
292
+ out.push(df.fact);
293
+ }
294
+ return out;
295
+ }
296
+
297
+ function renderProofEntry(entry, prefixes) {
298
+ if (!entry) return '';
299
+ if (entry.kind === 'fact') {
300
+ return ` ${graphForTriple(entry.fact, prefixes)}\n pe:by ${byBlankNode('fact', entry.source)}.`;
301
+ }
302
+ if (entry.kind === 'builtin') {
303
+ return ` ${graphForTriple(entry.fact, prefixes)}\n pe:by [ pe:builtin ${termToN3(entry.builtin, prefixes)} ].`;
304
+ }
305
+
306
+ 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
+ }
313
+
314
+ 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`;
317
+ }
318
+ return out.trimEnd();
319
+ }
320
+
321
+ function renderProofBlock(rootDf, derivedByKey, baseFactByKey, prefixes, resolveBackwardProof) {
322
+ const entries = collectProofEntries(rootDf, derivedByKey, baseFactByKey, resolveBackwardProof);
323
+ const rootGraph = graphForTriple(rootDf.fact, prefixes);
324
+ const proofBody = entries.map((entry) => renderProofEntry(entry, prefixes)).join('\n\n');
325
+ const proofGraph = proofBody ? `{
326
+ ${proofBody}
327
+ }` : '{}';
328
+ return `${rootGraph} pe:why ${proofGraph}.`;
329
+ }
330
+
331
+ function renderProofDocument(outputDerived, allDerived, baseFacts, prefixes, backRules) {
332
+ const selectedDerived = Array.isArray(outputDerived) ? outputDerived.filter((df) => df && df.fact) : [];
333
+ if (!selectedDerived.length) return '';
334
+
335
+ const proofPrefixes = clonePrefixEnvWithProofVocabulary(prefixes);
336
+ const derivedByKey = new Map();
337
+ for (const df of allDerived || []) {
338
+ if (!df || !df.fact) continue;
339
+ const key = proofTripleKey(df.fact);
340
+ if (!derivedByKey.has(key)) derivedByKey.set(key, df);
341
+ }
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.
344
+
345
+ const baseFactByKey = new Map();
346
+ for (const tr of baseFacts || []) {
347
+ if (!tr) continue;
348
+ const key = proofTripleKey(tr);
349
+ if (!baseFactByKey.has(key)) baseFactByKey.set(key, tr);
350
+ }
351
+
352
+ const resolveBackwardProof = findBackwardProofForGoal
353
+ ? (tr) => findBackwardProofForGoal(tr, baseFacts || [], backRules || [], { maxDepth: 64 })
354
+ : null;
355
+
356
+ const outputTriples = collectProofOutputTriples(selectedDerived);
357
+ const proofRelationTriples = [];
358
+ for (const df of selectedDerived) {
359
+ proofRelationTriples.push({ s: new GraphTerm([df.fact]), p: new Iri(PE_NS + 'why'), o: new GraphTerm([]) });
360
+ for (const entry of collectProofEntries(df, derivedByKey, baseFactByKey, resolveBackwardProof)) {
361
+ const fact = entry.kind === 'rule' ? entry.df.fact : entry.fact;
362
+ const byObject = entry.kind === 'builtin' && entry.builtin ? entry.builtin : new Iri(PE_NS + 'source');
363
+ proofRelationTriples.push({ s: new GraphTerm([fact]), p: new Iri(PE_NS + 'by'), o: byObject });
364
+ if (entry.kind === 'rule') {
365
+ for (const prem of entry.df.premises || []) proofRelationTriples.push({ s: new GraphTerm([fact]), p: new Iri(PE_NS + 'uses'), o: new GraphTerm([prem]) });
366
+ }
367
+ }
368
+ }
369
+
370
+ const usedPrefixes = proofPrefixes.prefixesUsedForOutput(outputTriples.concat(proofRelationTriples));
371
+ if (!usedPrefixes.some(([pfx]) => pfx === 'pe')) usedPrefixes.push(['pe', PE_NS]);
372
+ usedPrefixes.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
373
+
374
+ const parts = [];
375
+ for (const [pfx, base] of usedPrefixes) {
376
+ if (!base) continue;
377
+ if (pfx === '') parts.push(`@prefix : <${base}> .`);
378
+ else parts.push(`@prefix ${pfx}: <${base}> .`);
379
+ }
380
+ if (parts.length) parts.push('');
381
+
382
+ parts.push(...outputTriples.map((tr) => tripleToN3(tr, proofPrefixes)));
383
+ parts.push('');
384
+ for (let i = 0; i < selectedDerived.length; i++) {
385
+ if (i > 0) parts.push('');
386
+ parts.push(renderProofBlock(selectedDerived[i], derivedByKey, baseFactByKey, proofPrefixes, resolveBackwardProof));
387
+ }
388
+
389
+ return parts.join('\n').replace(/[ \t]+$/gm, '').replace(/\s*$/g, '') + '\n';
390
+ }
391
+
114
392
  // ===========================================================================
115
393
  // CLI entry point
116
394
  // ===========================================================================
@@ -217,7 +495,7 @@ function makeExplain(deps) {
217
495
  return addMarkdownHardBreaks(pairs.map((p) => p.text).join(''));
218
496
  }
219
497
 
220
- return { printExplanation, collectOutputStringsFromFacts };
498
+ return { printExplanation, renderProofDocument, collectOutputStringsFromFacts };
221
499
  }
222
500
 
223
501
  module.exports = { makeExplain };