eyeling 1.12.15 → 1.13.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 +54 -1
- package/README.md +15 -1
- package/examples/odrl-dpv-risk-ranked.n3 +1 -1
- package/examples/output/socrates.n3 +1 -0
- package/examples/socrates.n3 +3 -0
- package/eyeling-builtins.ttl +3 -0
- package/eyeling.js +243 -20
- package/lib/cli.js +34 -10
- package/lib/engine.js +194 -9
- package/lib/entry.js +2 -0
- package/lib/parser.js +8 -1
- package/lib/prelude.js +5 -0
- package/package.json +1 -1
- package/tools/n3gen.js +8 -3
package/HANDBOOK.md
CHANGED
|
@@ -761,6 +761,8 @@ A predicate is treated as builtin if:
|
|
|
761
761
|
|
|
762
762
|
Super restricted mode exists to let you treat all other predicates as ordinary facts/rules without any built-in evaluation.
|
|
763
763
|
|
|
764
|
+
**Note on `log:query`:** Eyeling also recognizes a special _top-level_ directive of the form `{...} log:query {...}.` to **select which results to print**. This is **not** a builtin predicate (it is not evaluated as part of goal solving); it is handled by the parser/CLI/output layer. See §11.3.5 below and Chapter 13 for details.
|
|
765
|
+
|
|
764
766
|
### 11.2 Built-ins return multiple solutions
|
|
765
767
|
|
|
766
768
|
Every builtin returns a list of substitution _deltas_.
|
|
@@ -1353,6 +1355,47 @@ As _builtins_, `log:implies` and `log:impliedBy` let you **inspect the currently
|
|
|
1353
1355
|
|
|
1354
1356
|
Each enumerated rule is standardized apart (fresh variable names) before unification so you can safely query over it.
|
|
1355
1357
|
|
|
1358
|
+
### Top-level directive: `log:query` (output selection)
|
|
1359
|
+
|
|
1360
|
+
**Shape (top level only):**
|
|
1361
|
+
|
|
1362
|
+
```n3
|
|
1363
|
+
{ ...premise... } log:query { ...conclusion... }.
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
`log:query` is best understood as an **output projection**, not as a rule and not as a normal builtin:
|
|
1367
|
+
|
|
1368
|
+
- Eyeling still computes the saturated forward closure (facts + rules, including backward-rule proofs where needed).
|
|
1369
|
+
- It then proves the **premise formula** as a goal (as if it were fed to `log:includes` in the global scope).
|
|
1370
|
+
- For every solution, it instantiates the **conclusion formula** and collects the resulting triples.
|
|
1371
|
+
- The final output is the **set of unique ground triples** from those instantiated conclusions.
|
|
1372
|
+
|
|
1373
|
+
This is “forward-rule-like” in spirit (premise ⇒ conclusion), but the instantiated conclusion triples are **not added back into the fact store**; they are just what Eyeling prints.
|
|
1374
|
+
|
|
1375
|
+
**Important details:**
|
|
1376
|
+
|
|
1377
|
+
- Only **top-level** `{...} log:query {...}.` directives are recognized. Inside quoted formulas (or inside rule bodies/heads) it is just an ordinary triple.
|
|
1378
|
+
- Query-mode output depends on the saturated closure, so it cannot be streamed; `--stream` has no effect when any `log:query` directives are present.
|
|
1379
|
+
- If you want _logical_ querying inside a rule/proof, use `log:includes` (and optionally `log:conclusion`) instead.
|
|
1380
|
+
|
|
1381
|
+
**Example (project a result set):**
|
|
1382
|
+
|
|
1383
|
+
```n3
|
|
1384
|
+
@prefix : <urn:ex:>.
|
|
1385
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
1386
|
+
|
|
1387
|
+
{ :a :p ?x } => { :a :q ?x }.
|
|
1388
|
+
:a :p :b.
|
|
1389
|
+
|
|
1390
|
+
{ :a :q ?x } log:query { :result :x ?x }.
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
Output (only):
|
|
1394
|
+
|
|
1395
|
+
```n3
|
|
1396
|
+
:result :x :b .
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1356
1399
|
### Scoped proof inside formulas: `log:includes` and friends
|
|
1357
1400
|
|
|
1358
1401
|
#### `log:includes`
|
|
@@ -1809,6 +1852,14 @@ See also: [Chapter 14 — Entry points: CLI, bundle exports, and npm API](#ch14)
|
|
|
1809
1852
|
|
|
1810
1853
|
By default, Eyeling prints **newly derived forward facts** (the heads of fired `=>` rules), serialized as N3. It does **not** reprint your input facts.
|
|
1811
1854
|
|
|
1855
|
+
If the input contains one or more **top-level** `log:query` directives:
|
|
1856
|
+
|
|
1857
|
+
```n3
|
|
1858
|
+
{ ...premise... } log:query { ...conclusion... }.
|
|
1859
|
+
```
|
|
1860
|
+
|
|
1861
|
+
Eyeling still computes the saturated forward closure, but it prints only the **unique instantiated conclusion triples** of those `log:query` directives (instead of all newly derived facts). This is useful when you want a forward-rule-like projection of results.
|
|
1862
|
+
|
|
1812
1863
|
For proof/explanation output and output modes, see:
|
|
1813
1864
|
|
|
1814
1865
|
- [Chapter 13 — Printing, proofs, and the user-facing output](#ch13)
|
|
@@ -1835,6 +1886,8 @@ Options:
|
|
|
1835
1886
|
-v, --version Print version and exit.
|
|
1836
1887
|
```
|
|
1837
1888
|
|
|
1889
|
+
Note: when `log:query` directives are present, Eyeling cannot stream output (the selected results depend on the saturated closure), so `--stream` has no effect in that mode.
|
|
1890
|
+
|
|
1838
1891
|
See also:
|
|
1839
1892
|
|
|
1840
1893
|
- [Chapter 13 — Printing, proofs, and the user-facing output](#ch13)
|
|
@@ -2031,7 +2084,7 @@ If you don’t want “stop the world”, derive a `:Violation` fact instead, an
|
|
|
2031
2084
|
The most robust way to keep LLM-generated logic plausible is to make it live under tests:
|
|
2032
2085
|
|
|
2033
2086
|
- Keep tiny **fixtures** (facts) alongside the rules.
|
|
2034
|
-
- Run Eyeling to produce the **derived closure** (Eyeling
|
|
2087
|
+
- Run Eyeling to produce the **derived closure** (Eyeling emits only newly derived forward facts by default, can optionally include compact proof comments, and can also use `log:query` directives to project a specific result set).
|
|
2035
2088
|
- Compare against an expected output (“golden file”) in CI.
|
|
2036
2089
|
|
|
2037
2090
|
This turns rule edits into a normal change-management loop: diffs are explicit, reviewable, and reproducible.
|
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@ A compact [Notation3 (N3)](https://notation3.org/) reasoner in **JavaScript**.
|
|
|
4
4
|
|
|
5
5
|
- Single self-contained bundle (`eyeling.js`), no external runtime deps
|
|
6
6
|
- Forward (`=>`) + backward (`<=`) chaining over Horn-style rules
|
|
7
|
-
- Outputs only **newly derived** forward facts (optionally with compact proof comments)
|
|
7
|
+
- Outputs only **newly derived** forward facts by default (optionally with compact proof comments)
|
|
8
|
+
- If the input contains one or more top-level `{ ... } log:query { ... }.` directives, the output becomes the **unique instantiated conclusion triples** of those queries (a forward-rule-like projection)
|
|
8
9
|
- Works in Node.js and fully client-side (browser/worker)
|
|
9
10
|
|
|
10
11
|
## Links
|
|
@@ -45,6 +46,16 @@ See all options:
|
|
|
45
46
|
npx eyeling --help
|
|
46
47
|
```
|
|
47
48
|
|
|
49
|
+
### log:query output selection
|
|
50
|
+
|
|
51
|
+
If your input contains one or more **top-level** directives of the form:
|
|
52
|
+
|
|
53
|
+
```n3
|
|
54
|
+
{ ?x a :Human. } log:query { ?x a :Mortal. }.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Eyeling will still compute the saturated forward closure, but it will **print only** the **unique instantiated conclusion triples** of those `log:query` directives (instead of printing all newly derived forward facts).
|
|
58
|
+
|
|
48
59
|
### JavaScript API
|
|
49
60
|
|
|
50
61
|
CommonJS:
|
|
@@ -79,6 +90,9 @@ const { closureN3 } = eyeling.reasonStream(input, {
|
|
|
79
90
|
proof: false,
|
|
80
91
|
onDerived: ({ triple }) => console.log(triple),
|
|
81
92
|
});
|
|
93
|
+
|
|
94
|
+
// With log:query directives present, closureN3 contains the query-selected triples.
|
|
95
|
+
// The return value also includes `queryMode`, `queryTriples`, and `queryDerived`.
|
|
82
96
|
```
|
|
83
97
|
|
|
84
98
|
> Note: the npm `reason()` helper shells out to the bundled `eyeling.js` CLI for simplicity and robustness.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ===========================================================================================
|
|
2
|
-
# ODRL + DPV
|
|
2
|
+
# ODRL + DPV risk assessment with ranked, explainable output.
|
|
3
3
|
#
|
|
4
4
|
# What this file does
|
|
5
5
|
# - Models an agreement as an ODRL policy (odrl:Policy) containing permissions,
|
package/examples/socrates.n3
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# ==================
|
|
4
4
|
|
|
5
5
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
|
|
6
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
6
7
|
@prefix : <http://example.org/socrates#>.
|
|
7
8
|
|
|
8
9
|
# facts
|
|
@@ -17,3 +18,5 @@
|
|
|
17
18
|
?S a ?B.
|
|
18
19
|
}.
|
|
19
20
|
|
|
21
|
+
# query
|
|
22
|
+
{ ?S a ?C } log:query { ?S a ?C }.
|
package/eyeling-builtins.ttl
CHANGED
|
@@ -256,6 +256,9 @@ log:implies a ex:Builtin ; ex:kind ex:Relation ;
|
|
|
256
256
|
log:impliedBy a ex:Builtin ; ex:kind ex:Relation ;
|
|
257
257
|
rdfs:comment "Rule/formula relation used for <=." .
|
|
258
258
|
|
|
259
|
+
log:query a ex:Builtin ; ex:kind ex:Meta ;
|
|
260
|
+
rdfs:comment "Output-selection directive: a top-level triple {premise} log:query {conclusion}. does not add facts, but selects the unique instantiated conclusion triples as Eyeling's output." .
|
|
261
|
+
|
|
259
262
|
log:includes a ex:Builtin ; ex:kind ex:Generator ;
|
|
260
263
|
rdfs:comment "Proves the object formula in the (possibly scoped) facts/rules; returns the set of proof substitutions (may bind variables)." .
|
|
261
264
|
|
package/eyeling.js
CHANGED
|
@@ -3798,11 +3798,11 @@ function main() {
|
|
|
3798
3798
|
}
|
|
3799
3799
|
|
|
3800
3800
|
let toks;
|
|
3801
|
-
let prefixes, triples, frules, brules;
|
|
3801
|
+
let prefixes, triples, frules, brules, qrules;
|
|
3802
3802
|
try {
|
|
3803
3803
|
toks = engine.lex(text);
|
|
3804
3804
|
const parser = new engine.Parser(toks);
|
|
3805
|
-
[prefixes, triples, frules, brules] = parser.parseDocument();
|
|
3805
|
+
[prefixes, triples, frules, brules, qrules] = parser.parseDocument();
|
|
3806
3806
|
// Make the parsed prefixes available to log:trace output (CLI path)
|
|
3807
3807
|
engine.setTracePrefixes(prefixes);
|
|
3808
3808
|
} catch (e) {
|
|
@@ -3822,13 +3822,17 @@ function main() {
|
|
|
3822
3822
|
}
|
|
3823
3823
|
return value;
|
|
3824
3824
|
}
|
|
3825
|
+
// For backwards compatibility, --ast prints exactly four top-level elements:
|
|
3826
|
+
// [prefixes, triples, forwardRules, backwardRules]
|
|
3827
|
+
// log:query directives are output-selection statements and are not included
|
|
3828
|
+
// in the legacy AST contract expected by test suites and downstream tools.
|
|
3825
3829
|
console.log(JSON.stringify([prefixes, triples, frules, brules], astReplacer, 2));
|
|
3826
3830
|
process.exit(0);
|
|
3827
3831
|
}
|
|
3828
3832
|
|
|
3829
3833
|
// Materialize anonymous rdf:first/rdf:rest collections into list terms.
|
|
3830
3834
|
// Named list nodes keep identity; list:* builtins can traverse them.
|
|
3831
|
-
engine.materializeRdfLists(triples, frules, brules);
|
|
3835
|
+
engine.materializeRdfLists(triples, frules.concat(qrules || []), brules);
|
|
3832
3836
|
|
|
3833
3837
|
const facts = triples.filter((tr) => engine.isGroundTriple(tr));
|
|
3834
3838
|
|
|
@@ -3924,7 +3928,9 @@ function main() {
|
|
|
3924
3928
|
}
|
|
3925
3929
|
|
|
3926
3930
|
// Streaming mode: print (input) prefixes first, then print derived triples as soon as they are found.
|
|
3927
|
-
|
|
3931
|
+
// Note: when log:query directives are present, we cannot stream output because
|
|
3932
|
+
// the selected results depend on the saturated closure.
|
|
3933
|
+
if (streamMode && !(Array.isArray(qrules) && qrules.length)) {
|
|
3928
3934
|
const usedInInput = prefixesUsedInInputTokens(toks, prefixes);
|
|
3929
3935
|
const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
|
|
3930
3936
|
|
|
@@ -3953,18 +3959,36 @@ function main() {
|
|
|
3953
3959
|
return;
|
|
3954
3960
|
}
|
|
3955
3961
|
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3962
|
+
const hasQueries = Array.isArray(qrules) && qrules.length;
|
|
3963
|
+
|
|
3964
|
+
// Default (non-streaming):
|
|
3965
|
+
// - without log:query: derive everything first, then print only newly derived facts
|
|
3966
|
+
// - with log:query: derive everything first, then print only unique instantiated
|
|
3967
|
+
// conclusion triples from the log:query directives.
|
|
3968
|
+
let derived = [];
|
|
3969
|
+
let outTriples = [];
|
|
3970
|
+
let outDerived = [];
|
|
3971
|
+
|
|
3972
|
+
if (hasQueries) {
|
|
3973
|
+
const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules);
|
|
3974
|
+
derived = res.derived;
|
|
3975
|
+
outTriples = res.queryTriples;
|
|
3976
|
+
outDerived = res.queryDerived;
|
|
3977
|
+
} else {
|
|
3978
|
+
derived = engine.forwardChain(facts, frules, brules);
|
|
3979
|
+
outDerived = derived;
|
|
3980
|
+
outTriples = derived.map((df) => df.fact);
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
const usedPrefixes = prefixes.prefixesUsedForOutput(outTriples);
|
|
3960
3984
|
|
|
3961
3985
|
for (const [pfx, base] of usedPrefixes) {
|
|
3962
3986
|
if (pfx === '') console.log(`@prefix : <${base}> .`);
|
|
3963
3987
|
else console.log(`@prefix ${pfx}: <${base}> .`);
|
|
3964
3988
|
}
|
|
3965
|
-
if (
|
|
3989
|
+
if (outTriples.length && usedPrefixes.length) console.log();
|
|
3966
3990
|
|
|
3967
|
-
for (const df of
|
|
3991
|
+
for (const df of outDerived) {
|
|
3968
3992
|
if (engine.getProofCommentsEnabled()) {
|
|
3969
3993
|
engine.printExplanation(df, prefixes);
|
|
3970
3994
|
console.log(engine.tripleToN3(df.fact, prefixes));
|
|
@@ -6820,6 +6844,159 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
6820
6844
|
}
|
|
6821
6845
|
}
|
|
6822
6846
|
|
|
6847
|
+
// ---------------------------------------------------------------------------
|
|
6848
|
+
// log:query output selection
|
|
6849
|
+
// ---------------------------------------------------------------------------
|
|
6850
|
+
// A top-level directive of the form:
|
|
6851
|
+
// { premise } log:query { conclusion }.
|
|
6852
|
+
// does not add facts to the closure. Instead, when one or more such directives
|
|
6853
|
+
// are present in the input, eyeling outputs only the **unique instantiated**
|
|
6854
|
+
// conclusion triples for each solution of the premise (similar to a forward
|
|
6855
|
+
// rule head projection).
|
|
6856
|
+
|
|
6857
|
+
function __tripleKeyForOutput(tr) {
|
|
6858
|
+
// Use a canonical structural encoding (covers lists and quoted graphs).
|
|
6859
|
+
// Note: this is used only for de-duplication of output triples.
|
|
6860
|
+
return skolemKeyFromTerm(tr.s) + '\t' + skolemKeyFromTerm(tr.p) + '\t' + skolemKeyFromTerm(tr.o);
|
|
6861
|
+
}
|
|
6862
|
+
|
|
6863
|
+
function __withScopedSnapshotForQueries(facts, fn) {
|
|
6864
|
+
// Some scoped log:* builtins "delay" unless a frozen snapshot exists.
|
|
6865
|
+
// After forwardChain completes, we create a snapshot of the saturated
|
|
6866
|
+
// closure so query premises can use scoped builtins reliably.
|
|
6867
|
+
const oldSnap = hasOwn.call(facts, '__scopedSnapshot') ? facts.__scopedSnapshot : undefined;
|
|
6868
|
+
const oldLvl = hasOwn.call(facts, '__scopedClosureLevel') ? facts.__scopedClosureLevel : undefined;
|
|
6869
|
+
|
|
6870
|
+
// Create a frozen snapshot of the saturated closure.
|
|
6871
|
+
const snap = facts.slice();
|
|
6872
|
+
ensureFactIndexes(snap);
|
|
6873
|
+
Object.defineProperty(snap, '__scopedSnapshot', {
|
|
6874
|
+
value: snap,
|
|
6875
|
+
enumerable: false,
|
|
6876
|
+
writable: true,
|
|
6877
|
+
configurable: true,
|
|
6878
|
+
});
|
|
6879
|
+
Object.defineProperty(snap, '__scopedClosureLevel', {
|
|
6880
|
+
value: Number.MAX_SAFE_INTEGER,
|
|
6881
|
+
enumerable: false,
|
|
6882
|
+
writable: true,
|
|
6883
|
+
configurable: true,
|
|
6884
|
+
});
|
|
6885
|
+
|
|
6886
|
+
// Ensure the live facts array exposes the snapshot/level for builtins.
|
|
6887
|
+
if (!hasOwn.call(facts, '__scopedSnapshot')) {
|
|
6888
|
+
Object.defineProperty(facts, '__scopedSnapshot', {
|
|
6889
|
+
value: null,
|
|
6890
|
+
enumerable: false,
|
|
6891
|
+
writable: true,
|
|
6892
|
+
configurable: true,
|
|
6893
|
+
});
|
|
6894
|
+
}
|
|
6895
|
+
if (!hasOwn.call(facts, '__scopedClosureLevel')) {
|
|
6896
|
+
Object.defineProperty(facts, '__scopedClosureLevel', {
|
|
6897
|
+
value: 0,
|
|
6898
|
+
enumerable: false,
|
|
6899
|
+
writable: true,
|
|
6900
|
+
configurable: true,
|
|
6901
|
+
});
|
|
6902
|
+
}
|
|
6903
|
+
|
|
6904
|
+
facts.__scopedSnapshot = snap;
|
|
6905
|
+
facts.__scopedClosureLevel = Number.MAX_SAFE_INTEGER;
|
|
6906
|
+
|
|
6907
|
+
try {
|
|
6908
|
+
return fn();
|
|
6909
|
+
} finally {
|
|
6910
|
+
facts.__scopedSnapshot = oldSnap === undefined ? null : oldSnap;
|
|
6911
|
+
facts.__scopedClosureLevel = oldLvl === undefined ? 0 : oldLvl;
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
|
|
6915
|
+
function collectLogQueryConclusions(logQueryRules, facts, backRules) {
|
|
6916
|
+
const queryTriples = [];
|
|
6917
|
+
const queryDerived = [];
|
|
6918
|
+
const seen = new Set();
|
|
6919
|
+
|
|
6920
|
+
if (!Array.isArray(logQueryRules) || logQueryRules.length === 0) {
|
|
6921
|
+
return { queryTriples, queryDerived };
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
ensureFactIndexes(facts);
|
|
6925
|
+
ensureBackRuleIndexes(backRules);
|
|
6926
|
+
|
|
6927
|
+
// Shared state across all query firings (mirrors forwardChain()).
|
|
6928
|
+
const varGen = [0];
|
|
6929
|
+
const skCounter = [0];
|
|
6930
|
+
const headSkolemCache = new Map();
|
|
6931
|
+
|
|
6932
|
+
return __withScopedSnapshotForQueries(facts, () => {
|
|
6933
|
+
for (let qi = 0; qi < logQueryRules.length; qi++) {
|
|
6934
|
+
const r = logQueryRules[qi];
|
|
6935
|
+
if (!r || !Array.isArray(r.premise) || !Array.isArray(r.conclusion)) continue;
|
|
6936
|
+
|
|
6937
|
+
const sols = proveGoals(r.premise, null, facts, backRules, 0, null, varGen, undefined, {
|
|
6938
|
+
deferBuiltins: true,
|
|
6939
|
+
});
|
|
6940
|
+
|
|
6941
|
+
for (const s of sols) {
|
|
6942
|
+
const skMap = {};
|
|
6943
|
+
const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
|
|
6944
|
+
const fireKey = __firingKey(1000000 + qi, instantiatedPremises);
|
|
6945
|
+
|
|
6946
|
+
// Support dynamic heads (same semantics as forwardChain).
|
|
6947
|
+
let dynamicHeadTriples = null;
|
|
6948
|
+
let headBlankLabelsHere = r.headBlankLabels;
|
|
6949
|
+
if (r.__dynamicConclusionTerm) {
|
|
6950
|
+
const dynTerm = applySubstTerm(r.__dynamicConclusionTerm, s);
|
|
6951
|
+
const dynTriples = __graphTriplesOrTrue(dynTerm);
|
|
6952
|
+
dynamicHeadTriples = dynTriples !== null ? dynTriples : [];
|
|
6953
|
+
const dynHeadBlankLabels =
|
|
6954
|
+
dynamicHeadTriples && dynamicHeadTriples.length ? collectBlankLabelsInTriples(dynamicHeadTriples) : null;
|
|
6955
|
+
if (dynHeadBlankLabels && dynHeadBlankLabels.size) {
|
|
6956
|
+
headBlankLabelsHere = new Set([...headBlankLabelsHere, ...dynHeadBlankLabels]);
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
|
|
6960
|
+
const headPatterns =
|
|
6961
|
+
dynamicHeadTriples && dynamicHeadTriples.length ? r.conclusion.concat(dynamicHeadTriples) : r.conclusion;
|
|
6962
|
+
|
|
6963
|
+
for (const cpat of headPatterns) {
|
|
6964
|
+
const instantiated = applySubstTriple(cpat, s);
|
|
6965
|
+
const inst = skolemizeTripleForHeadBlanks(
|
|
6966
|
+
instantiated,
|
|
6967
|
+
headBlankLabelsHere,
|
|
6968
|
+
skMap,
|
|
6969
|
+
skCounter,
|
|
6970
|
+
fireKey,
|
|
6971
|
+
headSkolemCache,
|
|
6972
|
+
);
|
|
6973
|
+
if (!isGroundTriple(inst)) continue;
|
|
6974
|
+
const k = __tripleKeyForOutput(inst);
|
|
6975
|
+
if (seen.has(k)) continue;
|
|
6976
|
+
seen.add(k);
|
|
6977
|
+
queryTriples.push(inst);
|
|
6978
|
+
queryDerived.push(new DerivedFact(inst, r, instantiatedPremises.slice(), { ...s }));
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6981
|
+
}
|
|
6982
|
+
|
|
6983
|
+
return { queryTriples, queryDerived };
|
|
6984
|
+
});
|
|
6985
|
+
}
|
|
6986
|
+
|
|
6987
|
+
function forwardChainAndCollectLogQueryConclusions(facts, forwardRules, backRules, logQueryRules, onDerived) {
|
|
6988
|
+
__enterReasoningRun();
|
|
6989
|
+
try {
|
|
6990
|
+
// Forward chain first (saturates `facts`).
|
|
6991
|
+
const derived = forwardChain(facts, forwardRules, backRules, onDerived);
|
|
6992
|
+
// Then collect query conclusions against the saturated closure.
|
|
6993
|
+
const { queryTriples, queryDerived } = collectLogQueryConclusions(logQueryRules, facts, backRules);
|
|
6994
|
+
return { derived, queryTriples, queryDerived };
|
|
6995
|
+
} finally {
|
|
6996
|
+
__exitReasoningRun();
|
|
6997
|
+
}
|
|
6998
|
+
}
|
|
6999
|
+
|
|
6823
7000
|
// (proof printing + log:outputString moved to lib/explain.js)
|
|
6824
7001
|
|
|
6825
7002
|
function reasonStream(n3Text, opts = {}) {
|
|
@@ -6839,32 +7016,62 @@ function reasonStream(n3Text, opts = {}) {
|
|
|
6839
7016
|
const parser = new Parser(toks);
|
|
6840
7017
|
if (baseIri) parser.prefixes.setBase(baseIri);
|
|
6841
7018
|
|
|
6842
|
-
const [prefixes, triples, frules, brules] = parser.parseDocument();
|
|
7019
|
+
const [prefixes, triples, frules, brules, logQueryRules] = parser.parseDocument();
|
|
6843
7020
|
// Make the parsed prefixes available to log:trace output
|
|
6844
7021
|
trace.setTracePrefixes(prefixes);
|
|
6845
7022
|
|
|
6846
7023
|
// Materialize anonymous rdf:first/rdf:rest collections into list terms.
|
|
6847
7024
|
// Named list nodes keep identity; list:* builtins can traverse them.
|
|
6848
|
-
materializeRdfLists(triples, frules, brules);
|
|
7025
|
+
materializeRdfLists(triples, frules.concat(logQueryRules || []), brules);
|
|
6849
7026
|
|
|
6850
7027
|
// facts becomes the saturated closure because pushFactIndexed(...) appends into it
|
|
6851
7028
|
const facts = triples.filter((tr) => isGroundTriple(tr));
|
|
6852
7029
|
|
|
6853
|
-
|
|
7030
|
+
let derived = [];
|
|
7031
|
+
let queryTriples = [];
|
|
7032
|
+
let queryDerived = [];
|
|
7033
|
+
|
|
7034
|
+
if (Array.isArray(logQueryRules) && logQueryRules.length) {
|
|
7035
|
+
// Query-selection mode: derive full closure, then output only the unique
|
|
7036
|
+
// instantiated conclusion triples of the log:query directives.
|
|
7037
|
+
const res = forwardChainAndCollectLogQueryConclusions(facts, frules, brules, logQueryRules);
|
|
7038
|
+
derived = res.derived;
|
|
7039
|
+
queryTriples = res.queryTriples;
|
|
7040
|
+
queryDerived = res.queryDerived;
|
|
7041
|
+
|
|
7042
|
+
// For compatibility with the streaming callback signature, we emit the
|
|
7043
|
+
// query-selected triples (not all derived facts).
|
|
6854
7044
|
if (typeof onDerived === 'function') {
|
|
6855
|
-
|
|
6856
|
-
triple: tripleToN3(
|
|
6857
|
-
|
|
6858
|
-
});
|
|
7045
|
+
for (const qdf of queryDerived) {
|
|
7046
|
+
onDerived({ triple: tripleToN3(qdf.fact, prefixes), df: qdf });
|
|
7047
|
+
}
|
|
6859
7048
|
}
|
|
6860
|
-
}
|
|
7049
|
+
} else {
|
|
7050
|
+
// Default mode: output only newly derived forward facts.
|
|
7051
|
+
derived = forwardChain(facts, frules, brules, (df) => {
|
|
7052
|
+
if (typeof onDerived === 'function') {
|
|
7053
|
+
onDerived({
|
|
7054
|
+
triple: tripleToN3(df.fact, prefixes),
|
|
7055
|
+
df,
|
|
7056
|
+
});
|
|
7057
|
+
}
|
|
7058
|
+
});
|
|
7059
|
+
}
|
|
6861
7060
|
|
|
6862
|
-
const closureTriples =
|
|
7061
|
+
const closureTriples =
|
|
7062
|
+
Array.isArray(logQueryRules) && logQueryRules.length
|
|
7063
|
+
? queryTriples
|
|
7064
|
+
: includeInputFactsInClosure
|
|
7065
|
+
? facts
|
|
7066
|
+
: derived.map((d) => d.fact);
|
|
6863
7067
|
|
|
6864
7068
|
const __out = {
|
|
6865
7069
|
prefixes,
|
|
6866
7070
|
facts, // saturated closure (Triple[])
|
|
6867
7071
|
derived, // DerivedFact[]
|
|
7072
|
+
queryMode: Array.isArray(logQueryRules) && logQueryRules.length ? true : false,
|
|
7073
|
+
queryTriples,
|
|
7074
|
+
queryDerived,
|
|
6868
7075
|
closureN3: closureTriples.map((t) => tripleToN3(t, prefixes)).join('\n'),
|
|
6869
7076
|
};
|
|
6870
7077
|
deref.setEnforceHttpsEnabled(__oldEnforceHttps);
|
|
@@ -6917,6 +7124,8 @@ function setTracePrefixes(v) {
|
|
|
6917
7124
|
|
|
6918
7125
|
module.exports = {
|
|
6919
7126
|
reasonStream,
|
|
7127
|
+
collectLogQueryConclusions,
|
|
7128
|
+
forwardChainAndCollectLogQueryConclusions,
|
|
6920
7129
|
collectOutputStringsFromFacts,
|
|
6921
7130
|
main,
|
|
6922
7131
|
version,
|
|
@@ -6969,6 +7178,8 @@ module.exports = {
|
|
|
6969
7178
|
lex: engine.lex,
|
|
6970
7179
|
Parser: engine.Parser,
|
|
6971
7180
|
forwardChain: engine.forwardChain,
|
|
7181
|
+
collectLogQueryConclusions: engine.collectLogQueryConclusions,
|
|
7182
|
+
forwardChainAndCollectLogQueryConclusions: engine.forwardChainAndCollectLogQueryConclusions,
|
|
6972
7183
|
materializeRdfLists: engine.materializeRdfLists,
|
|
6973
7184
|
isGroundTriple: engine.isGroundTriple,
|
|
6974
7185
|
printExplanation: engine.printExplanation,
|
|
@@ -7879,6 +8090,7 @@ const {
|
|
|
7879
8090
|
collectBlankLabelsInTriples,
|
|
7880
8091
|
isLogImplies,
|
|
7881
8092
|
isLogImpliedBy,
|
|
8093
|
+
isLogQuery,
|
|
7882
8094
|
} = require('./prelude');
|
|
7883
8095
|
|
|
7884
8096
|
const { N3SyntaxError } = require('./lexer');
|
|
@@ -7927,6 +8139,7 @@ class Parser {
|
|
|
7927
8139
|
const triples = [];
|
|
7928
8140
|
const forwardRules = [];
|
|
7929
8141
|
const backwardRules = [];
|
|
8142
|
+
const logQueries = [];
|
|
7930
8143
|
|
|
7931
8144
|
while (this.peek().typ !== 'EOF') {
|
|
7932
8145
|
if (this.peek().typ === 'AtPrefix') {
|
|
@@ -8001,6 +8214,11 @@ class Parser {
|
|
|
8001
8214
|
forwardRules.push(this.makeRule(tr.s, tr.o, true));
|
|
8002
8215
|
} else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
8003
8216
|
backwardRules.push(this.makeRule(tr.s, tr.o, false));
|
|
8217
|
+
} else if (isLogQuery(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
8218
|
+
// Output-selection directive: { premise } log:query { conclusion }.
|
|
8219
|
+
// When present at top-level, eyeling prints only the instantiated conclusion
|
|
8220
|
+
// triples (unique) instead of all newly derived facts.
|
|
8221
|
+
logQueries.push(this.makeRule(tr.s, tr.o, true));
|
|
8004
8222
|
} else {
|
|
8005
8223
|
triples.push(tr);
|
|
8006
8224
|
}
|
|
@@ -8009,7 +8227,7 @@ class Parser {
|
|
|
8009
8227
|
}
|
|
8010
8228
|
}
|
|
8011
8229
|
|
|
8012
|
-
return [this.prefixes, triples, forwardRules, backwardRules];
|
|
8230
|
+
return [this.prefixes, triples, forwardRules, backwardRules, logQueries];
|
|
8013
8231
|
}
|
|
8014
8232
|
|
|
8015
8233
|
parsePrefixDirective() {
|
|
@@ -8950,6 +9168,10 @@ function isLogImpliedBy(p) {
|
|
|
8950
9168
|
return p instanceof Iri && p.value === LOG_NS + 'impliedBy';
|
|
8951
9169
|
}
|
|
8952
9170
|
|
|
9171
|
+
function isLogQuery(p) {
|
|
9172
|
+
return p instanceof Iri && p.value === LOG_NS + 'query';
|
|
9173
|
+
}
|
|
9174
|
+
|
|
8953
9175
|
// ===========================================================================
|
|
8954
9176
|
// PREFIX ENVIRONMENT
|
|
8955
9177
|
// ===========================================================================
|
|
@@ -9161,6 +9383,7 @@ module.exports = {
|
|
|
9161
9383
|
isOwlSameAsPred,
|
|
9162
9384
|
isLogImplies,
|
|
9163
9385
|
isLogImpliedBy,
|
|
9386
|
+
isLogQuery,
|
|
9164
9387
|
PrefixEnv,
|
|
9165
9388
|
collectIrisInTerm,
|
|
9166
9389
|
varsInRule,
|
package/lib/cli.js
CHANGED
|
@@ -137,11 +137,11 @@ function main() {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
let toks;
|
|
140
|
-
let prefixes, triples, frules, brules;
|
|
140
|
+
let prefixes, triples, frules, brules, qrules;
|
|
141
141
|
try {
|
|
142
142
|
toks = engine.lex(text);
|
|
143
143
|
const parser = new engine.Parser(toks);
|
|
144
|
-
[prefixes, triples, frules, brules] = parser.parseDocument();
|
|
144
|
+
[prefixes, triples, frules, brules, qrules] = parser.parseDocument();
|
|
145
145
|
// Make the parsed prefixes available to log:trace output (CLI path)
|
|
146
146
|
engine.setTracePrefixes(prefixes);
|
|
147
147
|
} catch (e) {
|
|
@@ -161,13 +161,17 @@ function main() {
|
|
|
161
161
|
}
|
|
162
162
|
return value;
|
|
163
163
|
}
|
|
164
|
+
// For backwards compatibility, --ast prints exactly four top-level elements:
|
|
165
|
+
// [prefixes, triples, forwardRules, backwardRules]
|
|
166
|
+
// log:query directives are output-selection statements and are not included
|
|
167
|
+
// in the legacy AST contract expected by test suites and downstream tools.
|
|
164
168
|
console.log(JSON.stringify([prefixes, triples, frules, brules], astReplacer, 2));
|
|
165
169
|
process.exit(0);
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
// Materialize anonymous rdf:first/rdf:rest collections into list terms.
|
|
169
173
|
// Named list nodes keep identity; list:* builtins can traverse them.
|
|
170
|
-
engine.materializeRdfLists(triples, frules, brules);
|
|
174
|
+
engine.materializeRdfLists(triples, frules.concat(qrules || []), brules);
|
|
171
175
|
|
|
172
176
|
const facts = triples.filter((tr) => engine.isGroundTriple(tr));
|
|
173
177
|
|
|
@@ -263,7 +267,9 @@ function main() {
|
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
// Streaming mode: print (input) prefixes first, then print derived triples as soon as they are found.
|
|
266
|
-
|
|
270
|
+
// Note: when log:query directives are present, we cannot stream output because
|
|
271
|
+
// the selected results depend on the saturated closure.
|
|
272
|
+
if (streamMode && !(Array.isArray(qrules) && qrules.length)) {
|
|
267
273
|
const usedInInput = prefixesUsedInInputTokens(toks, prefixes);
|
|
268
274
|
const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
|
|
269
275
|
|
|
@@ -292,18 +298,36 @@ function main() {
|
|
|
292
298
|
return;
|
|
293
299
|
}
|
|
294
300
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
301
|
+
const hasQueries = Array.isArray(qrules) && qrules.length;
|
|
302
|
+
|
|
303
|
+
// Default (non-streaming):
|
|
304
|
+
// - without log:query: derive everything first, then print only newly derived facts
|
|
305
|
+
// - with log:query: derive everything first, then print only unique instantiated
|
|
306
|
+
// conclusion triples from the log:query directives.
|
|
307
|
+
let derived = [];
|
|
308
|
+
let outTriples = [];
|
|
309
|
+
let outDerived = [];
|
|
310
|
+
|
|
311
|
+
if (hasQueries) {
|
|
312
|
+
const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules);
|
|
313
|
+
derived = res.derived;
|
|
314
|
+
outTriples = res.queryTriples;
|
|
315
|
+
outDerived = res.queryDerived;
|
|
316
|
+
} else {
|
|
317
|
+
derived = engine.forwardChain(facts, frules, brules);
|
|
318
|
+
outDerived = derived;
|
|
319
|
+
outTriples = derived.map((df) => df.fact);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const usedPrefixes = prefixes.prefixesUsedForOutput(outTriples);
|
|
299
323
|
|
|
300
324
|
for (const [pfx, base] of usedPrefixes) {
|
|
301
325
|
if (pfx === '') console.log(`@prefix : <${base}> .`);
|
|
302
326
|
else console.log(`@prefix ${pfx}: <${base}> .`);
|
|
303
327
|
}
|
|
304
|
-
if (
|
|
328
|
+
if (outTriples.length && usedPrefixes.length) console.log();
|
|
305
329
|
|
|
306
|
-
for (const df of
|
|
330
|
+
for (const df of outDerived) {
|
|
307
331
|
if (engine.getProofCommentsEnabled()) {
|
|
308
332
|
engine.printExplanation(df, prefixes);
|
|
309
333
|
console.log(engine.tripleToN3(df.fact, prefixes));
|
package/lib/engine.js
CHANGED
|
@@ -2378,6 +2378,159 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
2378
2378
|
}
|
|
2379
2379
|
}
|
|
2380
2380
|
|
|
2381
|
+
// ---------------------------------------------------------------------------
|
|
2382
|
+
// log:query output selection
|
|
2383
|
+
// ---------------------------------------------------------------------------
|
|
2384
|
+
// A top-level directive of the form:
|
|
2385
|
+
// { premise } log:query { conclusion }.
|
|
2386
|
+
// does not add facts to the closure. Instead, when one or more such directives
|
|
2387
|
+
// are present in the input, eyeling outputs only the **unique instantiated**
|
|
2388
|
+
// conclusion triples for each solution of the premise (similar to a forward
|
|
2389
|
+
// rule head projection).
|
|
2390
|
+
|
|
2391
|
+
function __tripleKeyForOutput(tr) {
|
|
2392
|
+
// Use a canonical structural encoding (covers lists and quoted graphs).
|
|
2393
|
+
// Note: this is used only for de-duplication of output triples.
|
|
2394
|
+
return skolemKeyFromTerm(tr.s) + '\t' + skolemKeyFromTerm(tr.p) + '\t' + skolemKeyFromTerm(tr.o);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
function __withScopedSnapshotForQueries(facts, fn) {
|
|
2398
|
+
// Some scoped log:* builtins "delay" unless a frozen snapshot exists.
|
|
2399
|
+
// After forwardChain completes, we create a snapshot of the saturated
|
|
2400
|
+
// closure so query premises can use scoped builtins reliably.
|
|
2401
|
+
const oldSnap = hasOwn.call(facts, '__scopedSnapshot') ? facts.__scopedSnapshot : undefined;
|
|
2402
|
+
const oldLvl = hasOwn.call(facts, '__scopedClosureLevel') ? facts.__scopedClosureLevel : undefined;
|
|
2403
|
+
|
|
2404
|
+
// Create a frozen snapshot of the saturated closure.
|
|
2405
|
+
const snap = facts.slice();
|
|
2406
|
+
ensureFactIndexes(snap);
|
|
2407
|
+
Object.defineProperty(snap, '__scopedSnapshot', {
|
|
2408
|
+
value: snap,
|
|
2409
|
+
enumerable: false,
|
|
2410
|
+
writable: true,
|
|
2411
|
+
configurable: true,
|
|
2412
|
+
});
|
|
2413
|
+
Object.defineProperty(snap, '__scopedClosureLevel', {
|
|
2414
|
+
value: Number.MAX_SAFE_INTEGER,
|
|
2415
|
+
enumerable: false,
|
|
2416
|
+
writable: true,
|
|
2417
|
+
configurable: true,
|
|
2418
|
+
});
|
|
2419
|
+
|
|
2420
|
+
// Ensure the live facts array exposes the snapshot/level for builtins.
|
|
2421
|
+
if (!hasOwn.call(facts, '__scopedSnapshot')) {
|
|
2422
|
+
Object.defineProperty(facts, '__scopedSnapshot', {
|
|
2423
|
+
value: null,
|
|
2424
|
+
enumerable: false,
|
|
2425
|
+
writable: true,
|
|
2426
|
+
configurable: true,
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
if (!hasOwn.call(facts, '__scopedClosureLevel')) {
|
|
2430
|
+
Object.defineProperty(facts, '__scopedClosureLevel', {
|
|
2431
|
+
value: 0,
|
|
2432
|
+
enumerable: false,
|
|
2433
|
+
writable: true,
|
|
2434
|
+
configurable: true,
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
facts.__scopedSnapshot = snap;
|
|
2439
|
+
facts.__scopedClosureLevel = Number.MAX_SAFE_INTEGER;
|
|
2440
|
+
|
|
2441
|
+
try {
|
|
2442
|
+
return fn();
|
|
2443
|
+
} finally {
|
|
2444
|
+
facts.__scopedSnapshot = oldSnap === undefined ? null : oldSnap;
|
|
2445
|
+
facts.__scopedClosureLevel = oldLvl === undefined ? 0 : oldLvl;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
function collectLogQueryConclusions(logQueryRules, facts, backRules) {
|
|
2450
|
+
const queryTriples = [];
|
|
2451
|
+
const queryDerived = [];
|
|
2452
|
+
const seen = new Set();
|
|
2453
|
+
|
|
2454
|
+
if (!Array.isArray(logQueryRules) || logQueryRules.length === 0) {
|
|
2455
|
+
return { queryTriples, queryDerived };
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
ensureFactIndexes(facts);
|
|
2459
|
+
ensureBackRuleIndexes(backRules);
|
|
2460
|
+
|
|
2461
|
+
// Shared state across all query firings (mirrors forwardChain()).
|
|
2462
|
+
const varGen = [0];
|
|
2463
|
+
const skCounter = [0];
|
|
2464
|
+
const headSkolemCache = new Map();
|
|
2465
|
+
|
|
2466
|
+
return __withScopedSnapshotForQueries(facts, () => {
|
|
2467
|
+
for (let qi = 0; qi < logQueryRules.length; qi++) {
|
|
2468
|
+
const r = logQueryRules[qi];
|
|
2469
|
+
if (!r || !Array.isArray(r.premise) || !Array.isArray(r.conclusion)) continue;
|
|
2470
|
+
|
|
2471
|
+
const sols = proveGoals(r.premise, null, facts, backRules, 0, null, varGen, undefined, {
|
|
2472
|
+
deferBuiltins: true,
|
|
2473
|
+
});
|
|
2474
|
+
|
|
2475
|
+
for (const s of sols) {
|
|
2476
|
+
const skMap = {};
|
|
2477
|
+
const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
|
|
2478
|
+
const fireKey = __firingKey(1000000 + qi, instantiatedPremises);
|
|
2479
|
+
|
|
2480
|
+
// Support dynamic heads (same semantics as forwardChain).
|
|
2481
|
+
let dynamicHeadTriples = null;
|
|
2482
|
+
let headBlankLabelsHere = r.headBlankLabels;
|
|
2483
|
+
if (r.__dynamicConclusionTerm) {
|
|
2484
|
+
const dynTerm = applySubstTerm(r.__dynamicConclusionTerm, s);
|
|
2485
|
+
const dynTriples = __graphTriplesOrTrue(dynTerm);
|
|
2486
|
+
dynamicHeadTriples = dynTriples !== null ? dynTriples : [];
|
|
2487
|
+
const dynHeadBlankLabels =
|
|
2488
|
+
dynamicHeadTriples && dynamicHeadTriples.length ? collectBlankLabelsInTriples(dynamicHeadTriples) : null;
|
|
2489
|
+
if (dynHeadBlankLabels && dynHeadBlankLabels.size) {
|
|
2490
|
+
headBlankLabelsHere = new Set([...headBlankLabelsHere, ...dynHeadBlankLabels]);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
const headPatterns =
|
|
2495
|
+
dynamicHeadTriples && dynamicHeadTriples.length ? r.conclusion.concat(dynamicHeadTriples) : r.conclusion;
|
|
2496
|
+
|
|
2497
|
+
for (const cpat of headPatterns) {
|
|
2498
|
+
const instantiated = applySubstTriple(cpat, s);
|
|
2499
|
+
const inst = skolemizeTripleForHeadBlanks(
|
|
2500
|
+
instantiated,
|
|
2501
|
+
headBlankLabelsHere,
|
|
2502
|
+
skMap,
|
|
2503
|
+
skCounter,
|
|
2504
|
+
fireKey,
|
|
2505
|
+
headSkolemCache,
|
|
2506
|
+
);
|
|
2507
|
+
if (!isGroundTriple(inst)) continue;
|
|
2508
|
+
const k = __tripleKeyForOutput(inst);
|
|
2509
|
+
if (seen.has(k)) continue;
|
|
2510
|
+
seen.add(k);
|
|
2511
|
+
queryTriples.push(inst);
|
|
2512
|
+
queryDerived.push(new DerivedFact(inst, r, instantiatedPremises.slice(), { ...s }));
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
return { queryTriples, queryDerived };
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function forwardChainAndCollectLogQueryConclusions(facts, forwardRules, backRules, logQueryRules, onDerived) {
|
|
2522
|
+
__enterReasoningRun();
|
|
2523
|
+
try {
|
|
2524
|
+
// Forward chain first (saturates `facts`).
|
|
2525
|
+
const derived = forwardChain(facts, forwardRules, backRules, onDerived);
|
|
2526
|
+
// Then collect query conclusions against the saturated closure.
|
|
2527
|
+
const { queryTriples, queryDerived } = collectLogQueryConclusions(logQueryRules, facts, backRules);
|
|
2528
|
+
return { derived, queryTriples, queryDerived };
|
|
2529
|
+
} finally {
|
|
2530
|
+
__exitReasoningRun();
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2381
2534
|
// (proof printing + log:outputString moved to lib/explain.js)
|
|
2382
2535
|
|
|
2383
2536
|
function reasonStream(n3Text, opts = {}) {
|
|
@@ -2397,32 +2550,62 @@ function reasonStream(n3Text, opts = {}) {
|
|
|
2397
2550
|
const parser = new Parser(toks);
|
|
2398
2551
|
if (baseIri) parser.prefixes.setBase(baseIri);
|
|
2399
2552
|
|
|
2400
|
-
const [prefixes, triples, frules, brules] = parser.parseDocument();
|
|
2553
|
+
const [prefixes, triples, frules, brules, logQueryRules] = parser.parseDocument();
|
|
2401
2554
|
// Make the parsed prefixes available to log:trace output
|
|
2402
2555
|
trace.setTracePrefixes(prefixes);
|
|
2403
2556
|
|
|
2404
2557
|
// Materialize anonymous rdf:first/rdf:rest collections into list terms.
|
|
2405
2558
|
// Named list nodes keep identity; list:* builtins can traverse them.
|
|
2406
|
-
materializeRdfLists(triples, frules, brules);
|
|
2559
|
+
materializeRdfLists(triples, frules.concat(logQueryRules || []), brules);
|
|
2407
2560
|
|
|
2408
2561
|
// facts becomes the saturated closure because pushFactIndexed(...) appends into it
|
|
2409
2562
|
const facts = triples.filter((tr) => isGroundTriple(tr));
|
|
2410
2563
|
|
|
2411
|
-
|
|
2564
|
+
let derived = [];
|
|
2565
|
+
let queryTriples = [];
|
|
2566
|
+
let queryDerived = [];
|
|
2567
|
+
|
|
2568
|
+
if (Array.isArray(logQueryRules) && logQueryRules.length) {
|
|
2569
|
+
// Query-selection mode: derive full closure, then output only the unique
|
|
2570
|
+
// instantiated conclusion triples of the log:query directives.
|
|
2571
|
+
const res = forwardChainAndCollectLogQueryConclusions(facts, frules, brules, logQueryRules);
|
|
2572
|
+
derived = res.derived;
|
|
2573
|
+
queryTriples = res.queryTriples;
|
|
2574
|
+
queryDerived = res.queryDerived;
|
|
2575
|
+
|
|
2576
|
+
// For compatibility with the streaming callback signature, we emit the
|
|
2577
|
+
// query-selected triples (not all derived facts).
|
|
2412
2578
|
if (typeof onDerived === 'function') {
|
|
2413
|
-
|
|
2414
|
-
triple: tripleToN3(
|
|
2415
|
-
|
|
2416
|
-
});
|
|
2579
|
+
for (const qdf of queryDerived) {
|
|
2580
|
+
onDerived({ triple: tripleToN3(qdf.fact, prefixes), df: qdf });
|
|
2581
|
+
}
|
|
2417
2582
|
}
|
|
2418
|
-
}
|
|
2583
|
+
} else {
|
|
2584
|
+
// Default mode: output only newly derived forward facts.
|
|
2585
|
+
derived = forwardChain(facts, frules, brules, (df) => {
|
|
2586
|
+
if (typeof onDerived === 'function') {
|
|
2587
|
+
onDerived({
|
|
2588
|
+
triple: tripleToN3(df.fact, prefixes),
|
|
2589
|
+
df,
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2419
2594
|
|
|
2420
|
-
const closureTriples =
|
|
2595
|
+
const closureTriples =
|
|
2596
|
+
Array.isArray(logQueryRules) && logQueryRules.length
|
|
2597
|
+
? queryTriples
|
|
2598
|
+
: includeInputFactsInClosure
|
|
2599
|
+
? facts
|
|
2600
|
+
: derived.map((d) => d.fact);
|
|
2421
2601
|
|
|
2422
2602
|
const __out = {
|
|
2423
2603
|
prefixes,
|
|
2424
2604
|
facts, // saturated closure (Triple[])
|
|
2425
2605
|
derived, // DerivedFact[]
|
|
2606
|
+
queryMode: Array.isArray(logQueryRules) && logQueryRules.length ? true : false,
|
|
2607
|
+
queryTriples,
|
|
2608
|
+
queryDerived,
|
|
2426
2609
|
closureN3: closureTriples.map((t) => tripleToN3(t, prefixes)).join('\n'),
|
|
2427
2610
|
};
|
|
2428
2611
|
deref.setEnforceHttpsEnabled(__oldEnforceHttps);
|
|
@@ -2475,6 +2658,8 @@ function setTracePrefixes(v) {
|
|
|
2475
2658
|
|
|
2476
2659
|
module.exports = {
|
|
2477
2660
|
reasonStream,
|
|
2661
|
+
collectLogQueryConclusions,
|
|
2662
|
+
forwardChainAndCollectLogQueryConclusions,
|
|
2478
2663
|
collectOutputStringsFromFacts,
|
|
2479
2664
|
main,
|
|
2480
2665
|
version,
|
package/lib/entry.js
CHANGED
|
@@ -23,6 +23,8 @@ module.exports = {
|
|
|
23
23
|
lex: engine.lex,
|
|
24
24
|
Parser: engine.Parser,
|
|
25
25
|
forwardChain: engine.forwardChain,
|
|
26
|
+
collectLogQueryConclusions: engine.collectLogQueryConclusions,
|
|
27
|
+
forwardChainAndCollectLogQueryConclusions: engine.forwardChainAndCollectLogQueryConclusions,
|
|
26
28
|
materializeRdfLists: engine.materializeRdfLists,
|
|
27
29
|
isGroundTriple: engine.isGroundTriple,
|
|
28
30
|
printExplanation: engine.printExplanation,
|
package/lib/parser.js
CHANGED
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
collectBlankLabelsInTriples,
|
|
26
26
|
isLogImplies,
|
|
27
27
|
isLogImpliedBy,
|
|
28
|
+
isLogQuery,
|
|
28
29
|
} = require('./prelude');
|
|
29
30
|
|
|
30
31
|
const { N3SyntaxError } = require('./lexer');
|
|
@@ -73,6 +74,7 @@ class Parser {
|
|
|
73
74
|
const triples = [];
|
|
74
75
|
const forwardRules = [];
|
|
75
76
|
const backwardRules = [];
|
|
77
|
+
const logQueries = [];
|
|
76
78
|
|
|
77
79
|
while (this.peek().typ !== 'EOF') {
|
|
78
80
|
if (this.peek().typ === 'AtPrefix') {
|
|
@@ -147,6 +149,11 @@ class Parser {
|
|
|
147
149
|
forwardRules.push(this.makeRule(tr.s, tr.o, true));
|
|
148
150
|
} else if (isLogImpliedBy(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
149
151
|
backwardRules.push(this.makeRule(tr.s, tr.o, false));
|
|
152
|
+
} else if (isLogQuery(tr.p) && tr.s instanceof GraphTerm && tr.o instanceof GraphTerm) {
|
|
153
|
+
// Output-selection directive: { premise } log:query { conclusion }.
|
|
154
|
+
// When present at top-level, eyeling prints only the instantiated conclusion
|
|
155
|
+
// triples (unique) instead of all newly derived facts.
|
|
156
|
+
logQueries.push(this.makeRule(tr.s, tr.o, true));
|
|
150
157
|
} else {
|
|
151
158
|
triples.push(tr);
|
|
152
159
|
}
|
|
@@ -155,7 +162,7 @@ class Parser {
|
|
|
155
162
|
}
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
return [this.prefixes, triples, forwardRules, backwardRules];
|
|
165
|
+
return [this.prefixes, triples, forwardRules, backwardRules, logQueries];
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
parsePrefixDirective() {
|
package/lib/prelude.js
CHANGED
|
@@ -317,6 +317,10 @@ function isLogImpliedBy(p) {
|
|
|
317
317
|
return p instanceof Iri && p.value === LOG_NS + 'impliedBy';
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
function isLogQuery(p) {
|
|
321
|
+
return p instanceof Iri && p.value === LOG_NS + 'query';
|
|
322
|
+
}
|
|
323
|
+
|
|
320
324
|
// ===========================================================================
|
|
321
325
|
// PREFIX ENVIRONMENT
|
|
322
326
|
// ===========================================================================
|
|
@@ -528,6 +532,7 @@ module.exports = {
|
|
|
528
532
|
isOwlSameAsPred,
|
|
529
533
|
isLogImplies,
|
|
530
534
|
isLogImpliedBy,
|
|
535
|
+
isLogQuery,
|
|
531
536
|
PrefixEnv,
|
|
532
537
|
collectIrisInTerm,
|
|
533
538
|
varsInRule,
|
package/package.json
CHANGED
package/tools/n3gen.js
CHANGED
|
@@ -101,7 +101,6 @@ const log = {
|
|
|
101
101
|
nameOf: `${LOG_NS}nameOf`,
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
|
|
105
104
|
// ---------------------------------------------------------------------------
|
|
106
105
|
// Minimal Turtle/N3 model + lexer + parser
|
|
107
106
|
// ---------------------------------------------------------------------------
|
|
@@ -2001,7 +2000,10 @@ function writeN3LogNameOf({ datasetQuads, prefixes }) {
|
|
|
2001
2000
|
const prunedPrefixes = pruneUnusedPrefixes(prefixes, pseudoTriplesForUse);
|
|
2002
2001
|
const skolemMap = buildSkolemMapForBnodesThatCrossScopes(pseudoTriplesForUse);
|
|
2003
2002
|
const outPrefixes = ensureRdfPrefixIfUsed(
|
|
2004
|
-
ensureXsdPrefixIfUsed(
|
|
2003
|
+
ensureXsdPrefixIfUsed(
|
|
2004
|
+
ensureLogPrefixIfUsed(ensureSkolemPrefix(prunedPrefixes, skolemMap), pseudoTriplesForUse),
|
|
2005
|
+
pseudoTriplesForUse,
|
|
2006
|
+
),
|
|
2005
2007
|
pseudoTriplesForUse,
|
|
2006
2008
|
);
|
|
2007
2009
|
const pro = renderPrefixPrologue(outPrefixes).trim();
|
|
@@ -2059,7 +2061,10 @@ function writeN3Triples({ triples, prefixes }) {
|
|
|
2059
2061
|
const prunedPrefixes = pruneUnusedPrefixes(prefixes, foldedTriples);
|
|
2060
2062
|
const skolemMap = buildSkolemMapForBnodesThatCrossScopes(foldedTriples);
|
|
2061
2063
|
const outPrefixes = ensureRdfPrefixIfUsed(
|
|
2062
|
-
ensureXsdPrefixIfUsed(
|
|
2064
|
+
ensureXsdPrefixIfUsed(
|
|
2065
|
+
ensureLogPrefixIfUsed(ensureSkolemPrefix(prunedPrefixes, skolemMap), foldedTriples),
|
|
2066
|
+
foldedTriples,
|
|
2067
|
+
),
|
|
2063
2068
|
foldedTriples,
|
|
2064
2069
|
);
|
|
2065
2070
|
const blocks = [];
|