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/HANDBOOK.md +8 -8
- package/dist/browser/eyeling.browser.js +555 -32
- package/examples/output/socrates.n3 +0 -1
- package/examples/proof/age.n3 +40 -0
- package/examples/proof/socrates.n3 +24 -0
- package/examples/proof/witch.n3 +138 -0
- package/examples/socrates.n3 +0 -3
- package/eyeling.js +555 -32
- package/index.d.ts +1 -0
- package/index.js +16 -13
- package/lib/cli.js +10 -4
- package/lib/engine.js +150 -13
- package/lib/entry.js +1 -0
- package/lib/explain.js +299 -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
|
|
|
@@ -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
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
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
|
-
|
|
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 =
|
|
3951
|
-
?
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
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 };
|