eyeling 1.25.5 → 1.26.1

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
 
@@ -212,6 +212,7 @@ function main() {
212
212
  label: sourceLabel,
213
213
  collectUsedPrefixes: streamMode,
214
214
  keepSourceArtifacts: false,
215
+ sourceLocations: engine.getProofCommentsEnabled(),
215
216
  rdf: rdfMode,
216
217
  }),
217
218
  );
@@ -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.concat(outDerived || []), 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
@@ -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 { parseN3Text, parseN3SourceList } = require('./multisource');
46
47
 
47
48
  const {
48
49
  makeBuiltins,
@@ -780,9 +781,11 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
780
781
  });
781
782
 
782
783
  // Initialize proof/output helpers (implemented in lib/explain.js).
783
- const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
784
+ const { printExplanation, renderProofDocument, collectOutputStringsFromFacts } = makeExplain({
784
785
  applySubstTerm,
785
786
  skolemKeyFromTerm,
787
+ isBuiltinPred,
788
+ findBackwardProofForGoal,
786
789
  });
787
790
 
788
791
  function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
@@ -3154,6 +3157,120 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
3154
3157
  return results;
3155
3158
  }
3156
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
+
3157
3274
  // ===========================================================================
3158
3275
  // Forward chaining to fixpoint
3159
3276
  // ===========================================================================
@@ -3839,7 +3956,7 @@ function reasonStream(input, opts = {}) {
3839
3956
 
3840
3957
  const useRdfCompatibility = !!rdf;
3841
3958
 
3842
- const parsedSourceList = parseN3SourceList(input, { baseIri, rdf: useRdfCompatibility });
3959
+ const parsedSourceList = parseN3SourceList(input, { baseIri, rdf: useRdfCompatibility, sourceLocations: proof });
3843
3960
  const parsedInput = parsedSourceList || normalizeParsedReasonerInputSync(input);
3844
3961
  const rdfFactory = rdfjs ? getDataFactory(dataFactory) : null;
3845
3962
 
@@ -3875,11 +3992,22 @@ function reasonStream(input, opts = {}) {
3875
3992
  if (baseIri) prefixes.setBase(baseIri);
3876
3993
  } else {
3877
3994
  const n3Text = normalizeReasonerInputSync(input);
3878
- const toks = lex(n3Text, { rdf: useRdfCompatibility });
3879
- const parser = new Parser(toks);
3880
- if (baseIri) parser.prefixes.setBase(baseIri);
3995
+ if (proof) {
3996
+ const parsed = parseN3Text(n3Text, {
3997
+ label: 'input.n3',
3998
+ baseIri: baseIri || '',
3999
+ keepSourceArtifacts: false,
4000
+ sourceLocations: true,
4001
+ rdf: useRdfCompatibility,
4002
+ });
4003
+ ({ prefixes, triples, frules, brules, logQueryRules } = parsed);
4004
+ } else {
4005
+ const toks = lex(n3Text, { rdf: useRdfCompatibility });
4006
+ const parser = new Parser(toks);
4007
+ if (baseIri) parser.prefixes.setBase(baseIri);
3881
4008
 
3882
- [prefixes, triples, frules, brules, logQueryRules] = parser.parseDocument();
4009
+ [prefixes, triples, frules, brules, logQueryRules] = parser.parseDocument();
4010
+ }
3883
4011
  }
3884
4012
  // Make the parsed prefixes available to log:trace output
3885
4013
  trace.setTracePrefixes(prefixes);
@@ -3947,11 +4075,19 @@ function reasonStream(input, opts = {}) {
3947
4075
  ? facts
3948
4076
  : derived.map((d) => d.fact);
3949
4077
 
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');
4078
+ const closureN3 = proof
4079
+ ? renderProofDocument(
4080
+ Array.isArray(logQueryRules) && logQueryRules.length ? queryDerived : derived,
4081
+ derived.concat(queryDerived || []),
4082
+ triples,
4083
+ prefixes,
4084
+ brules,
4085
+ ).replace(/\n$/g, '')
4086
+ : useRdfCompatibility
4087
+ ? closureTriples.map((t) => tripleToRdfCompatible(t, prefixes)).join('\n')
4088
+ : Array.isArray(logQueryRules) && logQueryRules.length
4089
+ ? prettyPrintQueryTriples(closureTriples, prefixes)
4090
+ : closureTriples.map((t) => tripleToN3(t, prefixes)).join('\n');
3955
4091
 
3956
4092
  const __out = {
3957
4093
  prefixes,
@@ -3995,7 +4131,7 @@ function reasonRdfJs(input, opts = {}) {
3995
4131
 
3996
4132
  Promise.resolve().then(async () => {
3997
4133
  try {
3998
- const normalizedInput = parseN3SourceList(input, restOpts) || (await normalizeReasonerInputAsync(input));
4134
+ const normalizedInput = parseN3SourceList(input, { ...restOpts, sourceLocations: !!(restOpts.proof || restOpts.proofComments) }) || (await normalizeReasonerInputAsync(input));
3999
4135
  reasonStream(normalizedInput, {
4000
4136
  ...restOpts,
4001
4137
  rdfjs: false,
@@ -4078,6 +4214,7 @@ module.exports = {
4078
4214
  collectLogQueryConclusions,
4079
4215
  forwardChainAndCollectLogQueryConclusions,
4080
4216
  collectOutputStringsFromFacts,
4217
+ renderProofDocument,
4081
4218
  main,
4082
4219
  version,
4083
4220
  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,301 @@ 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
+ function clonePrefixEnvWithProofVocabulary(prefixes) {
139
+ const map = { ...(prefixes && prefixes.map ? prefixes.map : {}) };
140
+ if (!map.pe) map.pe = PE_NS;
141
+ return new PrefixEnv(map, (prefixes && prefixes.baseIri) || '');
142
+ }
143
+
144
+ function proofTripleKey(tr) {
145
+ if (!tr) return '';
146
+ return `${skolemKeyFromTerm(tr.s)}\t${skolemKeyFromTerm(tr.p)}\t${skolemKeyFromTerm(tr.o)}`;
147
+ }
148
+
149
+ function sourceLabelForProof(source) {
150
+ if (!source || typeof source.label !== 'string' || !source.label) return '<unknown>';
151
+ // Keep proof output stable when the CLI is invoked with paths such as examples/foo.n3.
152
+ return source.label.replace(/\\/g, '/').split('/').pop() || source.label;
153
+ }
154
+
155
+ function sourceKeyForProof(source) {
156
+ if (!source) return '<unknown>';
157
+ return `${sourceLabelForProof(source)}:${Number.isInteger(source.line) ? source.line : ''}`;
158
+ }
159
+
160
+ function byBlankNode(kind, source) {
161
+ const src = source || {};
162
+ const props = [`pe:${kind} ${n3String(sourceLabelForProof(src))}`];
163
+ if (Number.isInteger(src.line) && src.line > 0) props.push(`pe:line ${src.line}`);
164
+ return `[ ${props.join('; ')} ]`;
165
+ }
166
+
167
+ function renderBindingItems(df, prefixes) {
168
+ if (!df || !df.rule || !df.subst) return [];
169
+ const ruleVars = varsInRule(df.rule);
170
+ const visibleNames = Object.keys(df.subst)
171
+ .filter((name) => ruleVars.has(name))
172
+ .sort();
173
+ if (!visibleNames.length) return [];
174
+ const sourceNames = (df.rule && df.rule.__proofVarSourceNames) || null;
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
+ });
180
+ }
181
+
182
+ function withLastLineSuffix(text, suffix) {
183
+ const lines = String(text).split(/\r?\n/);
184
+ lines[lines.length - 1] += suffix;
185
+ return lines.join('\n');
186
+ }
187
+
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;
201
+ }
202
+
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)}`;
213
+ }
214
+
215
+ function collectProofEntries(rootDf, derivedByKey, baseFactByKey, resolveBackwardProof) {
216
+ const entries = [];
217
+ const seen = new Set();
218
+
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];
231
+ }
232
+
233
+ function visitProofNode(proof, fallbackTriple, parentDf) {
234
+ if (!proof) {
235
+ visitFactTriple(fallbackTriple, parentDf);
236
+ return;
237
+ }
238
+ if (proof.kind === 'rule' && proof.df) {
239
+ visitDerivedFact(proof.df, proof.children || null);
240
+ return;
241
+ }
242
+ if (proof.kind === 'builtin') {
243
+ remember({ kind: 'builtin', fact: proof.fact || fallbackTriple, builtin: proof.builtin || (proof.fact && proof.fact.p) });
244
+ return;
245
+ }
246
+ remember({ kind: 'fact', fact: proof.fact || fallbackTriple, source: proof.source });
247
+ }
248
+
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 });
254
+ return;
255
+ }
256
+
257
+ const candidates = derivedCandidatesForKey(key);
258
+ const df = candidates.find((candidate) => candidate !== parentDf) || null;
259
+ if (df) {
260
+ visitDerivedFact(df);
261
+ return;
262
+ }
263
+
264
+ if (tr && isBuiltinPred(tr.p)) {
265
+ remember({ kind: 'builtin', fact: tr, builtin: tr.p });
266
+ return;
267
+ }
268
+
269
+ if (resolveBackwardProof) {
270
+ const proof = resolveBackwardProof(tr);
271
+ if (proof) {
272
+ visitProofNode(proof, tr, parentDf);
273
+ return;
274
+ }
275
+ }
276
+
277
+ remember({ kind: 'fact', fact: tr, source: null });
278
+ }
279
+
280
+ function visitDerivedFact(df, children) {
281
+ if (!df || !df.fact) return;
282
+ const entry = { kind: 'rule', df };
283
+ if (!remember(entry)) return;
284
+
285
+ if (Array.isArray(children) && children.length) {
286
+ for (const child of children) visitProofNode(child, null, df);
287
+ return;
288
+ }
289
+ for (const prem of df.premises || []) visitFactTriple(prem, df);
290
+ }
291
+
292
+ visitDerivedFact(rootDf);
293
+ return entries;
294
+ }
295
+
296
+ function collectProofOutputTriples(outputDerived) {
297
+ const out = [];
298
+ const seen = new Set();
299
+ for (const df of outputDerived || []) {
300
+ if (!df || !df.fact) continue;
301
+ const key = proofTripleKey(df.fact);
302
+ if (seen.has(key)) continue;
303
+ seen.add(key);
304
+ out.push(df.fact);
305
+ }
306
+ return out;
307
+ }
308
+
309
+ function renderProofEntry(entry, prefixes) {
310
+ if (!entry) return '';
311
+ if (entry.kind === 'fact') {
312
+ return ` ${graphForTriple(entry.fact, prefixes)}\n pe:by ${byBlankNode('fact', entry.source)}.`;
313
+ }
314
+ if (entry.kind === 'builtin') {
315
+ return ` ${graphForTriple(entry.fact, prefixes)}\n pe:by [ pe:builtin ${termToN3(entry.builtin, prefixes)} ].`;
316
+ }
317
+
318
+ const df = entry.df;
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);
326
+
327
+ let out = ` ${graphForTriple(df.fact, prefixes)}\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';
331
+ }
332
+ return out.trimEnd();
333
+ }
334
+
335
+ function renderProofBlock(rootDf, derivedByKey, baseFactByKey, prefixes, resolveBackwardProof) {
336
+ const entries = collectProofEntries(rootDf, derivedByKey, baseFactByKey, resolveBackwardProof);
337
+ const rootGraph = graphForTriple(rootDf.fact, prefixes);
338
+ const proofBody = entries.map((entry) => renderProofEntry(entry, prefixes)).join('\n\n');
339
+ const proofGraph = proofBody ? `{
340
+ ${proofBody}
341
+ }` : '{}';
342
+ return `${rootGraph} pe:why ${proofGraph}.`;
343
+ }
344
+
345
+ function renderProofDocument(outputDerived, allDerived, baseFacts, prefixes, backRules) {
346
+ const selectedDerived = Array.isArray(outputDerived) ? outputDerived.filter((df) => df && df.fact) : [];
347
+ if (!selectedDerived.length) return '';
348
+
349
+ const proofPrefixes = clonePrefixEnvWithProofVocabulary(prefixes);
350
+ const derivedByKey = new Map();
351
+ function addDerived(df) {
352
+ if (!df || !df.fact) return;
353
+ const key = proofTripleKey(df.fact);
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);
360
+ }
361
+ for (const df of allDerived || []) addDerived(df);
362
+ for (const df of selectedDerived) addDerived(df);
363
+
364
+ const baseFactByKey = new Map();
365
+ for (const tr of baseFacts || []) {
366
+ if (!tr) continue;
367
+ const key = proofTripleKey(tr);
368
+ if (!baseFactByKey.has(key)) baseFactByKey.set(key, tr);
369
+ }
370
+
371
+ const resolveBackwardProof = findBackwardProofForGoal
372
+ ? (tr) => findBackwardProofForGoal(tr, baseFacts || [], backRules || [], { maxDepth: 64 })
373
+ : null;
374
+
375
+ const outputTriples = collectProofOutputTriples(selectedDerived);
376
+ const proofRelationTriples = [];
377
+ for (const df of selectedDerived) {
378
+ proofRelationTriples.push({ s: new GraphTerm([df.fact]), p: new Iri(PE_NS + 'why'), o: new GraphTerm([]) });
379
+ for (const entry of collectProofEntries(df, derivedByKey, baseFactByKey, resolveBackwardProof)) {
380
+ const fact = entry.kind === 'rule' ? entry.df.fact : entry.fact;
381
+ const byObject = entry.kind === 'builtin' && entry.builtin ? entry.builtin : new Iri(PE_NS + 'source');
382
+ proofRelationTriples.push({ s: new GraphTerm([fact]), p: new Iri(PE_NS + 'by'), o: byObject });
383
+ if (entry.kind === 'rule') {
384
+ for (const prem of entry.df.premises || []) proofRelationTriples.push({ s: new GraphTerm([fact]), p: new Iri(PE_NS + 'uses'), o: new GraphTerm([prem]) });
385
+ }
386
+ }
387
+ }
388
+
389
+ const usedPrefixes = proofPrefixes.prefixesUsedForOutput(outputTriples.concat(proofRelationTriples));
390
+ if (!usedPrefixes.some(([pfx]) => pfx === 'pe')) usedPrefixes.push(['pe', PE_NS]);
391
+ usedPrefixes.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
392
+
393
+ const parts = [];
394
+ for (const [pfx, base] of usedPrefixes) {
395
+ if (!base) continue;
396
+ if (pfx === '') parts.push(`@prefix : <${base}> .`);
397
+ else parts.push(`@prefix ${pfx}: <${base}> .`);
398
+ }
399
+ if (parts.length) parts.push('');
400
+
401
+ parts.push(...outputTriples.map((tr) => tripleToN3(tr, proofPrefixes)));
402
+ parts.push('');
403
+ for (let i = 0; i < selectedDerived.length; i++) {
404
+ if (i > 0) parts.push('');
405
+ parts.push(renderProofBlock(selectedDerived[i], derivedByKey, baseFactByKey, proofPrefixes, resolveBackwardProof));
406
+ }
407
+
408
+ return parts.join('\n').replace(/[ \t]+$/gm, '').replace(/\s*$/g, '') + '\n';
409
+ }
410
+
114
411
  // ===========================================================================
115
412
  // CLI entry point
116
413
  // ===========================================================================
@@ -217,7 +514,7 @@ function makeExplain(deps) {
217
514
  return addMarkdownHardBreaks(pairs.map((p) => p.text).join(''));
218
515
  }
219
516
 
220
- return { printExplanation, collectOutputStringsFromFacts };
517
+ return { printExplanation, renderProofDocument, collectOutputStringsFromFacts };
221
518
  }
222
519
 
223
520
  module.exports = { makeExplain };