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/HANDBOOK.md +8 -8
- package/dist/browser/eyeling.browser.js +614 -25
- package/examples/output/socrates.n3 +0 -1
- package/examples/proof/age.n3 +31 -0
- package/examples/proof/socrates.n3 +19 -0
- package/examples/proof/witch.n3 +120 -0
- package/examples/socrates.n3 +0 -3
- package/eyeling.js +614 -25
- package/index.d.ts +1 -0
- package/index.js +16 -13
- package/lib/cli.js +10 -4
- package/lib/engine.js +228 -6
- package/lib/entry.js +1 -0
- package/lib/explain.js +280 -2
- package/lib/multisource.js +74 -7
- package/lib/parser.js +20 -6
- package/package.json +1 -1
- package/test/api.test.js +6 -6
- package/tools/bundle.js +3 -0
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -19,21 +19,24 @@ function reason(opt = {}, input = '') {
|
|
|
19
19
|
|
|
20
20
|
const args = [];
|
|
21
21
|
|
|
22
|
-
// default: proof
|
|
23
|
-
// set {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
|
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 =
|
|
3951
|
-
?
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
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 };
|