eyeling 1.6.14 → 1.6.16
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/README.md +8 -19
- package/eyeling.js +196 -294
- package/index.js +13 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,16 +103,16 @@ npm run test:packlist
|
|
|
103
103
|
- `test:package` does a “real consumer” smoke test: `npm pack` → install tarball into a temp project → run API + CLI + examples.
|
|
104
104
|
- `test:packlist` sanity-checks what will be published in the npm tarball (and the CLI shebang/bin wiring).
|
|
105
105
|
|
|
106
|
-
###
|
|
106
|
+
### Usage
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
# Option 1: use the shebang (Unix-like)
|
|
112
|
-
./eyeling.js examples/socrates.n3
|
|
108
|
+
```
|
|
109
|
+
Usage: eyeling.js [options] <file.n3>
|
|
113
110
|
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
Options:
|
|
112
|
+
-h, --help Show this help and exit.
|
|
113
|
+
-v, --version Print version and exit.
|
|
114
|
+
-p, --proof-comments Enable proof explanations.
|
|
115
|
+
-n, --no-proof-comments Disable proof explanations (default).
|
|
116
116
|
```
|
|
117
117
|
|
|
118
118
|
By default, `eyeling`:
|
|
@@ -122,17 +122,6 @@ By default, `eyeling`:
|
|
|
122
122
|
3. prints only **newly derived forward facts** (not the original input facts)
|
|
123
123
|
4. prints a compact per-triple explanation as `#` comments (can be disabled)
|
|
124
124
|
|
|
125
|
-
### Options
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
node eyeling.js --version
|
|
129
|
-
node eyeling.js -v
|
|
130
|
-
|
|
131
|
-
# Disable proof comments (print only derived triples)
|
|
132
|
-
node eyeling.js --no-proof-comments examples/socrates.n3
|
|
133
|
-
node eyeling.js -n examples/socrates.n3
|
|
134
|
-
```
|
|
135
|
-
|
|
136
125
|
## What output do I get?
|
|
137
126
|
|
|
138
127
|
For each newly derived triple, `eyeling` prints:
|
package/eyeling.js
CHANGED
|
@@ -75,7 +75,7 @@ const skolemCache = new Map();
|
|
|
75
75
|
const jsonPointerCache = new Map();
|
|
76
76
|
|
|
77
77
|
// Controls whether human-readable proof comments are printed.
|
|
78
|
-
let proofCommentsEnabled =
|
|
78
|
+
let proofCommentsEnabled = false;
|
|
79
79
|
|
|
80
80
|
// ----------------------------------------------------------------------------
|
|
81
81
|
// Deterministic time support
|
|
@@ -2041,6 +2041,16 @@ function unifyFormulaTriples(xs, ys, subst) {
|
|
|
2041
2041
|
}
|
|
2042
2042
|
|
|
2043
2043
|
function unifyTerm(a, b, subst) {
|
|
2044
|
+
return unifyTermWithOptions(a, b, subst, { boolValueEq: true, intDecimalEq: false });
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function unifyTermListAppend(a, b, subst) {
|
|
2048
|
+
// Keep list:append behavior: allow integer<->decimal exact equality,
|
|
2049
|
+
// but do NOT add boolean-value equivalence (preserves current semantics).
|
|
2050
|
+
return unifyTermWithOptions(a, b, subst, { boolValueEq: false, intDecimalEq: true });
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
function unifyTermWithOptions(a, b, subst, opts) {
|
|
2044
2054
|
a = applySubstTerm(a, subst);
|
|
2045
2055
|
b = applySubstTerm(b, subst);
|
|
2046
2056
|
|
|
@@ -2054,9 +2064,8 @@ function unifyTerm(a, b, subst) {
|
|
|
2054
2064
|
s2[v] = t;
|
|
2055
2065
|
return s2;
|
|
2056
2066
|
}
|
|
2057
|
-
|
|
2058
2067
|
if (b instanceof Var) {
|
|
2059
|
-
return
|
|
2068
|
+
return unifyTermWithOptions(b, a, subst, opts);
|
|
2060
2069
|
}
|
|
2061
2070
|
|
|
2062
2071
|
// Exact matches
|
|
@@ -2064,25 +2073,25 @@ function unifyTerm(a, b, subst) {
|
|
|
2064
2073
|
if (a instanceof Literal && b instanceof Literal && a.value === b.value) return { ...subst };
|
|
2065
2074
|
if (a instanceof Blank && b instanceof Blank && a.label === b.label) return { ...subst };
|
|
2066
2075
|
|
|
2067
|
-
//
|
|
2076
|
+
// Plain string vs xsd:string equivalence
|
|
2068
2077
|
if (a instanceof Literal && b instanceof Literal) {
|
|
2069
2078
|
if (literalsEquivalentAsXsdString(a.value, b.value)) return { ...subst };
|
|
2070
2079
|
}
|
|
2071
2080
|
|
|
2072
|
-
// Boolean-value
|
|
2073
|
-
if (a instanceof Literal && b instanceof Literal) {
|
|
2081
|
+
// Boolean-value equivalence (ONLY for normal unifyTerm)
|
|
2082
|
+
if (opts.boolValueEq && a instanceof Literal && b instanceof Literal) {
|
|
2074
2083
|
const ai = parseBooleanLiteralInfo(a);
|
|
2075
2084
|
const bi = parseBooleanLiteralInfo(b);
|
|
2076
2085
|
if (ai && bi && ai.value === bi.value) return { ...subst };
|
|
2077
2086
|
}
|
|
2078
2087
|
|
|
2079
|
-
// Numeric-value match
|
|
2088
|
+
// Numeric-value match:
|
|
2089
|
+
// - always allow equality when datatype matches (existing behavior)
|
|
2090
|
+
// - optionally allow integer<->decimal exact equality (list:append only)
|
|
2080
2091
|
if (a instanceof Literal && b instanceof Literal) {
|
|
2081
2092
|
const ai = parseNumericLiteralInfo(a);
|
|
2082
2093
|
const bi = parseNumericLiteralInfo(b);
|
|
2083
|
-
|
|
2084
2094
|
if (ai && bi) {
|
|
2085
|
-
// same datatype: keep existing behavior
|
|
2086
2095
|
if (ai.dt === bi.dt) {
|
|
2087
2096
|
if (ai.kind === 'bigint' && bi.kind === 'bigint') {
|
|
2088
2097
|
if (ai.value === bi.value) return { ...subst };
|
|
@@ -2092,101 +2101,18 @@ function unifyTerm(a, b, subst) {
|
|
|
2092
2101
|
if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return { ...subst };
|
|
2093
2102
|
}
|
|
2094
2103
|
}
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
// Open list vs concrete list
|
|
2099
|
-
if (a instanceof OpenListTerm && b instanceof ListTerm) {
|
|
2100
|
-
return unifyOpenWithList(a.prefix, a.tailVar, b.elems, subst);
|
|
2101
|
-
}
|
|
2102
|
-
if (a instanceof ListTerm && b instanceof OpenListTerm) {
|
|
2103
|
-
return unifyOpenWithList(b.prefix, b.tailVar, a.elems, subst);
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
// Open list vs open list (same tail var)
|
|
2107
|
-
if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
|
|
2108
|
-
if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length) return null;
|
|
2109
|
-
let s2 = { ...subst };
|
|
2110
|
-
for (let i = 0; i < a.prefix.length; i++) {
|
|
2111
|
-
s2 = unifyTerm(a.prefix[i], b.prefix[i], s2);
|
|
2112
|
-
if (s2 === null) return null;
|
|
2113
|
-
}
|
|
2114
|
-
return s2;
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
// List terms
|
|
2118
|
-
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
2119
|
-
if (a.elems.length !== b.elems.length) return null;
|
|
2120
|
-
let s2 = { ...subst };
|
|
2121
|
-
for (let i = 0; i < a.elems.length; i++) {
|
|
2122
|
-
s2 = unifyTerm(a.elems[i], b.elems[i], s2);
|
|
2123
|
-
if (s2 === null) return null;
|
|
2124
|
-
}
|
|
2125
|
-
return s2;
|
|
2126
|
-
}
|
|
2127
2104
|
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
a = applySubstTerm(a, subst);
|
|
2140
|
-
b = applySubstTerm(b, subst);
|
|
2141
|
-
|
|
2142
|
-
// Variable binding (same as unifyTerm)
|
|
2143
|
-
if (a instanceof Var) {
|
|
2144
|
-
const v = a.name;
|
|
2145
|
-
const t = b;
|
|
2146
|
-
if (t instanceof Var && t.name === v) return { ...subst };
|
|
2147
|
-
if (containsVarTerm(t, v)) return null;
|
|
2148
|
-
const s2 = { ...subst };
|
|
2149
|
-
s2[v] = t;
|
|
2150
|
-
return s2;
|
|
2151
|
-
}
|
|
2152
|
-
if (b instanceof Var) return unifyTermListAppend(b, a, subst);
|
|
2153
|
-
|
|
2154
|
-
// Exact matches
|
|
2155
|
-
if (a instanceof Iri && b instanceof Iri && a.value === b.value) return { ...subst };
|
|
2156
|
-
if (a instanceof Literal && b instanceof Literal && a.value === b.value) return { ...subst };
|
|
2157
|
-
if (a instanceof Blank && b instanceof Blank && a.label === b.label) return { ...subst };
|
|
2158
|
-
|
|
2159
|
-
// Plain string vs xsd:string equivalence
|
|
2160
|
-
if (a instanceof Literal && b instanceof Literal) {
|
|
2161
|
-
if (literalsEquivalentAsXsdString(a.value, b.value)) return { ...subst };
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
// Numeric match: same-dt OR integer<->decimal exact equality (for list:append only)
|
|
2165
|
-
if (a instanceof Literal && b instanceof Literal) {
|
|
2166
|
-
const ai = parseNumericLiteralInfo(a);
|
|
2167
|
-
const bi = parseNumericLiteralInfo(b);
|
|
2168
|
-
if (ai && bi) {
|
|
2169
|
-
// same datatype
|
|
2170
|
-
if (ai.dt === bi.dt) {
|
|
2171
|
-
if (ai.kind === 'bigint' && bi.kind === 'bigint') {
|
|
2172
|
-
if (ai.value === bi.value) return { ...subst };
|
|
2173
|
-
} else {
|
|
2174
|
-
const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
|
|
2175
|
-
const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
|
|
2176
|
-
if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return { ...subst };
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
// integer <-> decimal exact equality
|
|
2181
|
-
const intDt = XSD_NS + 'integer';
|
|
2182
|
-
const decDt = XSD_NS + 'decimal';
|
|
2183
|
-
if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
|
|
2184
|
-
const intInfo = ai.dt === intDt ? ai : bi;
|
|
2185
|
-
const decInfo = ai.dt === decDt ? ai : bi;
|
|
2186
|
-
const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
|
|
2187
|
-
if (dec) {
|
|
2188
|
-
const scaledInt = intInfo.value * pow10n(dec.scale);
|
|
2189
|
-
if (scaledInt === dec.num) return { ...subst };
|
|
2105
|
+
if (opts.intDecimalEq) {
|
|
2106
|
+
const intDt = XSD_NS + 'integer';
|
|
2107
|
+
const decDt = XSD_NS + 'decimal';
|
|
2108
|
+
if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
|
|
2109
|
+
const intInfo = ai.dt === intDt ? ai : bi; // bigint
|
|
2110
|
+
const decInfo = ai.dt === decDt ? ai : bi; // number + lexStr
|
|
2111
|
+
const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
|
|
2112
|
+
if (dec) {
|
|
2113
|
+
const scaledInt = intInfo.value * pow10n(dec.scale);
|
|
2114
|
+
if (scaledInt === dec.num) return { ...subst };
|
|
2115
|
+
}
|
|
2190
2116
|
}
|
|
2191
2117
|
}
|
|
2192
2118
|
}
|
|
@@ -2205,7 +2131,7 @@ function unifyTermListAppend(a, b, subst) {
|
|
|
2205
2131
|
if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length) return null;
|
|
2206
2132
|
let s2 = { ...subst };
|
|
2207
2133
|
for (let i = 0; i < a.prefix.length; i++) {
|
|
2208
|
-
s2 =
|
|
2134
|
+
s2 = unifyTermWithOptions(a.prefix[i], b.prefix[i], s2, opts);
|
|
2209
2135
|
if (s2 === null) return null;
|
|
2210
2136
|
}
|
|
2211
2137
|
return s2;
|
|
@@ -2216,7 +2142,7 @@ function unifyTermListAppend(a, b, subst) {
|
|
|
2216
2142
|
if (a.elems.length !== b.elems.length) return null;
|
|
2217
2143
|
let s2 = { ...subst };
|
|
2218
2144
|
for (let i = 0; i < a.elems.length; i++) {
|
|
2219
|
-
s2 =
|
|
2145
|
+
s2 = unifyTermWithOptions(a.elems[i], b.elems[i], s2, opts);
|
|
2220
2146
|
if (s2 === null) return null;
|
|
2221
2147
|
}
|
|
2222
2148
|
return s2;
|
|
@@ -2886,14 +2812,7 @@ function parseNumOrDuration(t) {
|
|
|
2886
2812
|
|
|
2887
2813
|
function formatDurationLiteralFromSeconds(secs) {
|
|
2888
2814
|
const neg = secs < 0;
|
|
2889
|
-
const
|
|
2890
|
-
const days = Math.round(absSecs / 86400.0);
|
|
2891
|
-
const lex = neg ? `" -P${days}D"` : `"P${days}D"`;
|
|
2892
|
-
const cleanLex = neg ? `" -P${days}D"` : `"P${days}D"`; // minor detail; we just follow shape
|
|
2893
|
-
const lex2 = neg ? `" -P${days}D"` : `"P${days}D"`;
|
|
2894
|
-
const actualLex = neg ? `" -P${days}D"` : `"P${days}D"`;
|
|
2895
|
-
// keep simpler, no spaces:
|
|
2896
|
-
const finalLex = neg ? `" -P${days}D"` : `"P${days}D"`;
|
|
2815
|
+
const days = Math.round(Math.abs(secs) / 86400.0);
|
|
2897
2816
|
const literalLex = neg ? `"-P${days}D"` : `"P${days}D"`;
|
|
2898
2817
|
return new Literal(`${literalLex}^^<${XSD_NS}duration>`);
|
|
2899
2818
|
}
|
|
@@ -3091,6 +3010,38 @@ function evalUnaryMathRel(g, subst, forwardFn, inverseFn /* may be null */) {
|
|
|
3091
3010
|
return [];
|
|
3092
3011
|
}
|
|
3093
3012
|
|
|
3013
|
+
function evalListFirstLikeBuiltin(sTerm, oTerm, subst) {
|
|
3014
|
+
if (!(sTerm instanceof ListTerm)) return [];
|
|
3015
|
+
if (!sTerm.elems.length) return [];
|
|
3016
|
+
const first = sTerm.elems[0];
|
|
3017
|
+
const s2 = unifyTerm(oTerm, first, subst);
|
|
3018
|
+
return s2 !== null ? [s2] : [];
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
|
|
3022
|
+
// Closed list: (a b c) -> (b c)
|
|
3023
|
+
if (sTerm instanceof ListTerm) {
|
|
3024
|
+
if (!sTerm.elems.length) return [];
|
|
3025
|
+
const rest = new ListTerm(sTerm.elems.slice(1));
|
|
3026
|
+
const s2 = unifyTerm(oTerm, rest, subst);
|
|
3027
|
+
return s2 !== null ? [s2] : [];
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// Open list: (a b ... ?T) -> (b ... ?T)
|
|
3031
|
+
if (sTerm instanceof OpenListTerm) {
|
|
3032
|
+
if (!sTerm.prefix.length) return [];
|
|
3033
|
+
if (sTerm.prefix.length === 1) {
|
|
3034
|
+
const s2 = unifyTerm(oTerm, new Var(sTerm.tailVar), subst);
|
|
3035
|
+
return s2 !== null ? [s2] : [];
|
|
3036
|
+
}
|
|
3037
|
+
const rest = new OpenListTerm(sTerm.prefix.slice(1), sTerm.tailVar);
|
|
3038
|
+
const s2 = unifyTerm(oTerm, rest, subst);
|
|
3039
|
+
return s2 !== null ? [s2] : [];
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
return [];
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3094
3045
|
// ============================================================================
|
|
3095
3046
|
// Backward proof & builtins mutual recursion — declarations first
|
|
3096
3047
|
// ============================================================================
|
|
@@ -3785,79 +3736,18 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
|
|
|
3785
3736
|
return [];
|
|
3786
3737
|
}
|
|
3787
3738
|
|
|
3788
|
-
// list:first
|
|
3739
|
+
// list:first and rdf:first
|
|
3789
3740
|
// true iff $s is a list and $o is the first member of that list.
|
|
3790
3741
|
// Schema: $s+ list:first $o-
|
|
3791
|
-
if (g.p instanceof Iri && g.p.value === LIST_NS + 'first') {
|
|
3792
|
-
|
|
3793
|
-
if (!g.s.elems.length) return [];
|
|
3794
|
-
const first = g.s.elems[0];
|
|
3795
|
-
const s2 = unifyTerm(g.o, first, subst);
|
|
3796
|
-
return s2 !== null ? [s2] : [];
|
|
3742
|
+
if (g.p instanceof Iri && (g.p.value === LIST_NS + 'first' || g.p.value === RDF_NS + 'first')) {
|
|
3743
|
+
return evalListFirstLikeBuiltin(g.s, g.o, subst);
|
|
3797
3744
|
}
|
|
3798
3745
|
|
|
3799
|
-
// list:rest
|
|
3746
|
+
// list:rest and rdf:rest
|
|
3800
3747
|
// true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
|
|
3801
3748
|
// Schema: $s+ list:rest $o-
|
|
3802
|
-
if (g.p instanceof Iri && g.p.value === LIST_NS + 'rest') {
|
|
3803
|
-
|
|
3804
|
-
if (g.s instanceof ListTerm) {
|
|
3805
|
-
if (!g.s.elems.length) return [];
|
|
3806
|
-
const rest = new ListTerm(g.s.elems.slice(1));
|
|
3807
|
-
const s2 = unifyTerm(g.o, rest, subst);
|
|
3808
|
-
return s2 !== null ? [s2] : [];
|
|
3809
|
-
}
|
|
3810
|
-
|
|
3811
|
-
// Open list: (a b ... ?T) -> (b ... ?T)
|
|
3812
|
-
if (g.s instanceof OpenListTerm) {
|
|
3813
|
-
if (!g.s.prefix.length) return []; // can't compute rest without a known head
|
|
3814
|
-
|
|
3815
|
-
if (g.s.prefix.length === 1) {
|
|
3816
|
-
// (a ... ?T) rest is exactly ?T
|
|
3817
|
-
const s2 = unifyTerm(g.o, new Var(g.s.tailVar), subst);
|
|
3818
|
-
return s2 !== null ? [s2] : [];
|
|
3819
|
-
}
|
|
3820
|
-
|
|
3821
|
-
const rest = new OpenListTerm(g.s.prefix.slice(1), g.s.tailVar);
|
|
3822
|
-
const s2 = unifyTerm(g.o, rest, subst);
|
|
3823
|
-
return s2 !== null ? [s2] : [];
|
|
3824
|
-
}
|
|
3825
|
-
|
|
3826
|
-
return [];
|
|
3827
|
-
}
|
|
3828
|
-
|
|
3829
|
-
// rdf:first (alias of list:first)
|
|
3830
|
-
// Schema: $s+ rdf:first $o-
|
|
3831
|
-
if (g.p instanceof Iri && g.p.value === RDF_NS + 'first') {
|
|
3832
|
-
if (!(g.s instanceof ListTerm)) return [];
|
|
3833
|
-
if (!g.s.elems.length) return [];
|
|
3834
|
-
const first = g.s.elems[0];
|
|
3835
|
-
const s2 = unifyTerm(g.o, first, subst);
|
|
3836
|
-
return s2 !== null ? [s2] : [];
|
|
3837
|
-
}
|
|
3838
|
-
|
|
3839
|
-
// rdf:rest (alias of list:rest)
|
|
3840
|
-
// Schema: $s+ rdf:rest $o-
|
|
3841
|
-
if (g.p instanceof Iri && g.p.value === RDF_NS + 'rest') {
|
|
3842
|
-
// Closed list: (a b c) -> (b c)
|
|
3843
|
-
if (g.s instanceof ListTerm) {
|
|
3844
|
-
if (!g.s.elems.length) return [];
|
|
3845
|
-
const rest = new ListTerm(g.s.elems.slice(1));
|
|
3846
|
-
const s2 = unifyTerm(g.o, rest, subst);
|
|
3847
|
-
return s2 !== null ? [s2] : [];
|
|
3848
|
-
}
|
|
3849
|
-
// Open list: (a b ... ?T) -> (b ... ?T)
|
|
3850
|
-
if (g.s instanceof OpenListTerm) {
|
|
3851
|
-
if (!g.s.prefix.length) return [];
|
|
3852
|
-
if (g.s.prefix.length === 1) {
|
|
3853
|
-
const s2 = unifyTerm(g.o, new Var(g.s.tailVar), subst);
|
|
3854
|
-
return s2 !== null ? [s2] : [];
|
|
3855
|
-
}
|
|
3856
|
-
const rest = new OpenListTerm(g.s.prefix.slice(1), g.s.tailVar);
|
|
3857
|
-
const s2 = unifyTerm(g.o, rest, subst);
|
|
3858
|
-
return s2 !== null ? [s2] : [];
|
|
3859
|
-
}
|
|
3860
|
-
return [];
|
|
3749
|
+
if (g.p instanceof Iri && (g.p.value === LIST_NS + 'rest' || g.p.value === RDF_NS + 'rest')) {
|
|
3750
|
+
return evalListRestLikeBuiltin(g.s, g.o, subst);
|
|
3861
3751
|
}
|
|
3862
3752
|
|
|
3863
3753
|
// list:iterate
|
|
@@ -5065,134 +4955,121 @@ function forwardChain(facts, forwardRules, backRules) {
|
|
|
5065
4955
|
|
|
5066
4956
|
function runFixpoint() {
|
|
5067
4957
|
let anyChange = false;
|
|
4958
|
+
|
|
5068
4959
|
while (true) {
|
|
5069
4960
|
let changed = false;
|
|
5070
4961
|
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
const empty = {};
|
|
5077
|
-
const visited = [];
|
|
4962
|
+
for (let i = 0; i < forwardRules.length; i++) {
|
|
4963
|
+
const r = forwardRules[i];
|
|
4964
|
+
const empty = {};
|
|
4965
|
+
const visited = [];
|
|
4966
|
+
const sols = proveGoals(r.premise.slice(), empty, facts, backRules, 0, visited, varGen);
|
|
5078
4967
|
|
|
5079
|
-
|
|
4968
|
+
// Inference fuse
|
|
4969
|
+
if (r.isFuse && sols.length) {
|
|
4970
|
+
console.log('# Inference fuse triggered: a { ... } => false. rule fired.');
|
|
4971
|
+
process.exit(2);
|
|
4972
|
+
}
|
|
5080
4973
|
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
4974
|
+
for (const s of sols) {
|
|
4975
|
+
// IMPORTANT: one skolem map per *rule firing*
|
|
4976
|
+
const skMap = {};
|
|
4977
|
+
const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
|
|
4978
|
+
const fireKey = firingKey(i, instantiatedPremises);
|
|
4979
|
+
|
|
4980
|
+
for (const cpat of r.conclusion) {
|
|
4981
|
+
const instantiated = applySubstTriple(cpat, s);
|
|
4982
|
+
|
|
4983
|
+
const isFwRuleTriple =
|
|
4984
|
+
isLogImplies(instantiated.p) &&
|
|
4985
|
+
((instantiated.s instanceof FormulaTerm && instantiated.o instanceof FormulaTerm) ||
|
|
4986
|
+
(instantiated.s instanceof Literal && instantiated.s.value === 'true' && instantiated.o instanceof FormulaTerm) ||
|
|
4987
|
+
(instantiated.s instanceof FormulaTerm && instantiated.o instanceof Literal && instantiated.o.value === 'true'));
|
|
4988
|
+
|
|
4989
|
+
const isBwRuleTriple =
|
|
4990
|
+
isLogImpliedBy(instantiated.p) &&
|
|
4991
|
+
((instantiated.s instanceof FormulaTerm && instantiated.o instanceof FormulaTerm) ||
|
|
4992
|
+
(instantiated.s instanceof FormulaTerm && instantiated.o instanceof Literal && instantiated.o.value === 'true') ||
|
|
4993
|
+
(instantiated.s instanceof Literal && instantiated.s.value === 'true' && instantiated.o instanceof FormulaTerm));
|
|
4994
|
+
|
|
4995
|
+
if (isFwRuleTriple || isBwRuleTriple) {
|
|
4996
|
+
if (!hasFactIndexed(facts, instantiated)) {
|
|
4997
|
+
factList.push(instantiated);
|
|
4998
|
+
pushFactIndexed(facts, instantiated);
|
|
4999
|
+
derivedForward.push(new DerivedFact(instantiated, r, instantiatedPremises.slice(), { ...s }));
|
|
5000
|
+
changed = true;
|
|
5001
|
+
}
|
|
5086
5002
|
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
const
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
...s,
|
|
5116
|
-
}),
|
|
5003
|
+
// Promote rule-producing triples to live rules, treating literal true as {}.
|
|
5004
|
+
const left =
|
|
5005
|
+
instantiated.s instanceof FormulaTerm
|
|
5006
|
+
? instantiated.s.triples
|
|
5007
|
+
: instantiated.s instanceof Literal && instantiated.s.value === 'true'
|
|
5008
|
+
? []
|
|
5009
|
+
: null;
|
|
5010
|
+
|
|
5011
|
+
const right =
|
|
5012
|
+
instantiated.o instanceof FormulaTerm
|
|
5013
|
+
? instantiated.o.triples
|
|
5014
|
+
: instantiated.o instanceof Literal && instantiated.o.value === 'true'
|
|
5015
|
+
? []
|
|
5016
|
+
: null;
|
|
5017
|
+
|
|
5018
|
+
if (left !== null && right !== null) {
|
|
5019
|
+
if (isFwRuleTriple) {
|
|
5020
|
+
const [premise0, conclusion] = liftBlankRuleVars(left, right);
|
|
5021
|
+
const premise = reorderPremiseForConstraints(premise0);
|
|
5022
|
+
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
5023
|
+
const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
|
|
5024
|
+
|
|
5025
|
+
const already = forwardRules.some(
|
|
5026
|
+
(rr) =>
|
|
5027
|
+
rr.isForward === newRule.isForward &&
|
|
5028
|
+
rr.isFuse === newRule.isFuse &&
|
|
5029
|
+
triplesListEqual(rr.premise, newRule.premise) &&
|
|
5030
|
+
triplesListEqual(rr.conclusion, newRule.conclusion),
|
|
5117
5031
|
);
|
|
5118
|
-
|
|
5119
|
-
}
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
: null;
|
|
5135
|
-
|
|
5136
|
-
if (left !== null && right !== null) {
|
|
5137
|
-
if (isFwRuleTriple) {
|
|
5138
|
-
const [premise0, conclusion] = liftBlankRuleVars(left, right);
|
|
5139
|
-
const premise = reorderPremiseForConstraints(premise0);
|
|
5140
|
-
|
|
5141
|
-
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
5142
|
-
const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
|
|
5143
|
-
|
|
5144
|
-
const already = forwardRules.some(
|
|
5145
|
-
(rr) =>
|
|
5146
|
-
rr.isForward === newRule.isForward &&
|
|
5147
|
-
rr.isFuse === newRule.isFuse &&
|
|
5148
|
-
triplesListEqual(rr.premise, newRule.premise) &&
|
|
5149
|
-
triplesListEqual(rr.conclusion, newRule.conclusion),
|
|
5150
|
-
);
|
|
5151
|
-
if (!already) forwardRules.push(newRule);
|
|
5152
|
-
} else if (isBwRuleTriple) {
|
|
5153
|
-
const [premise, conclusion] = liftBlankRuleVars(right, left);
|
|
5154
|
-
|
|
5155
|
-
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
5156
|
-
const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
|
|
5157
|
-
|
|
5158
|
-
const already = backRules.some(
|
|
5159
|
-
(rr) =>
|
|
5160
|
-
rr.isForward === newRule.isForward &&
|
|
5161
|
-
rr.isFuse === newRule.isFuse &&
|
|
5162
|
-
triplesListEqual(rr.premise, newRule.premise) &&
|
|
5163
|
-
triplesListEqual(rr.conclusion, newRule.conclusion),
|
|
5164
|
-
);
|
|
5165
|
-
if (!already) {
|
|
5166
|
-
backRules.push(newRule);
|
|
5167
|
-
indexBackRule(backRules, newRule);
|
|
5168
|
-
}
|
|
5032
|
+
if (!already) forwardRules.push(newRule);
|
|
5033
|
+
} else if (isBwRuleTriple) {
|
|
5034
|
+
const [premise, conclusion] = liftBlankRuleVars(right, left);
|
|
5035
|
+
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
5036
|
+
const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
|
|
5037
|
+
|
|
5038
|
+
const already = backRules.some(
|
|
5039
|
+
(rr) =>
|
|
5040
|
+
rr.isForward === newRule.isForward &&
|
|
5041
|
+
rr.isFuse === newRule.isFuse &&
|
|
5042
|
+
triplesListEqual(rr.premise, newRule.premise) &&
|
|
5043
|
+
triplesListEqual(rr.conclusion, newRule.conclusion),
|
|
5044
|
+
);
|
|
5045
|
+
if (!already) {
|
|
5046
|
+
backRules.push(newRule);
|
|
5047
|
+
indexBackRule(backRules, newRule);
|
|
5169
5048
|
}
|
|
5170
5049
|
}
|
|
5171
|
-
|
|
5172
|
-
continue; // skip normal fact handling
|
|
5173
5050
|
}
|
|
5174
5051
|
|
|
5175
|
-
//
|
|
5176
|
-
|
|
5052
|
+
continue; // skip normal fact handling
|
|
5053
|
+
}
|
|
5177
5054
|
|
|
5178
|
-
|
|
5179
|
-
|
|
5055
|
+
// Only skolemize blank nodes that occur explicitly in the rule head
|
|
5056
|
+
const inst = skolemizeTripleForHeadBlanks(instantiated, r.headBlankLabels, skMap, skCounter, fireKey, headSkolemCache);
|
|
5180
5057
|
|
|
5181
|
-
|
|
5182
|
-
|
|
5058
|
+
if (!isGroundTriple(inst)) continue;
|
|
5059
|
+
if (hasFactIndexed(facts, inst)) continue;
|
|
5183
5060
|
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
}
|
|
5061
|
+
factList.push(inst);
|
|
5062
|
+
pushFactIndexed(facts, inst);
|
|
5063
|
+
derivedForward.push(new DerivedFact(inst, r, instantiatedPremises.slice(), { ...s }));
|
|
5064
|
+
changed = true;
|
|
5187
5065
|
}
|
|
5188
5066
|
}
|
|
5189
|
-
|
|
5190
|
-
if (!changed) break;
|
|
5191
5067
|
}
|
|
5192
5068
|
|
|
5193
|
-
if (changed) anyChange = true;
|
|
5194
5069
|
if (!changed) break;
|
|
5070
|
+
anyChange = true;
|
|
5195
5071
|
}
|
|
5072
|
+
|
|
5196
5073
|
return anyChange;
|
|
5197
5074
|
}
|
|
5198
5075
|
|
|
@@ -5558,7 +5435,6 @@ function localIsoDateTimeString(d) {
|
|
|
5558
5435
|
// ============================================================================
|
|
5559
5436
|
// CLI entry point
|
|
5560
5437
|
// ============================================================================
|
|
5561
|
-
|
|
5562
5438
|
function main() {
|
|
5563
5439
|
// Drop "node" and script name; keep only user-provided args
|
|
5564
5440
|
const argv = process.argv.slice(2);
|
|
@@ -5566,6 +5442,19 @@ function main() {
|
|
|
5566
5442
|
// --------------------------------------------------------------------------
|
|
5567
5443
|
// Global options
|
|
5568
5444
|
// --------------------------------------------------------------------------
|
|
5445
|
+
// --help / -h: print help and exit
|
|
5446
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
5447
|
+
console.log(
|
|
5448
|
+
'Usage: eyeling.js [options] <file.n3>\n' +
|
|
5449
|
+
'\n' +
|
|
5450
|
+
'Options:\n' +
|
|
5451
|
+
' -h, --help Show this help and exit.\n' +
|
|
5452
|
+
' -v, --version Print version and exit.\n' +
|
|
5453
|
+
' -p, --proof-comments Enable proof explanations.\n' +
|
|
5454
|
+
' -n, --no-proof-comments Disable proof explanations (default).\n',
|
|
5455
|
+
);
|
|
5456
|
+
process.exit(0);
|
|
5457
|
+
}
|
|
5569
5458
|
|
|
5570
5459
|
// --version / -v: print version and exit
|
|
5571
5460
|
if (argv.includes('--version') || argv.includes('-v')) {
|
|
@@ -5573,7 +5462,13 @@ function main() {
|
|
|
5573
5462
|
process.exit(0);
|
|
5574
5463
|
}
|
|
5575
5464
|
|
|
5576
|
-
// --
|
|
5465
|
+
// --proof-comments / -p: enable proof explanations
|
|
5466
|
+
if (argv.includes('--proof-comments') || argv.includes('-p')) {
|
|
5467
|
+
proofCommentsEnabled = true;
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
// --no-proof-comments / -n: disable proof explanations (default)
|
|
5471
|
+
// Keep this after --proof-comments so -n wins if both are present.
|
|
5577
5472
|
if (argv.includes('--no-proof-comments') || argv.includes('-n')) {
|
|
5578
5473
|
proofCommentsEnabled = false;
|
|
5579
5474
|
}
|
|
@@ -5582,9 +5477,16 @@ function main() {
|
|
|
5582
5477
|
// Positional args (the N3 file)
|
|
5583
5478
|
// --------------------------------------------------------------------------
|
|
5584
5479
|
const positional = argv.filter((a) => !a.startsWith('-'));
|
|
5585
|
-
|
|
5586
5480
|
if (positional.length !== 1) {
|
|
5587
|
-
console.error(
|
|
5481
|
+
console.error(
|
|
5482
|
+
'Usage: eyeling.js [options] <file.n3>\n' +
|
|
5483
|
+
'\n' +
|
|
5484
|
+
'Options:\n' +
|
|
5485
|
+
' -h, --help Show this help and exit.\n' +
|
|
5486
|
+
' -v, --version Print version and exit.\n' +
|
|
5487
|
+
' -p, --proof-comments Enable proof explanations.\n' +
|
|
5488
|
+
' -n, --no-proof-comments Disable proof explanations (default).\n',
|
|
5489
|
+
);
|
|
5588
5490
|
process.exit(1);
|
|
5589
5491
|
}
|
|
5590
5492
|
|
|
@@ -5603,12 +5505,12 @@ function main() {
|
|
|
5603
5505
|
const [prefixes, triples, frules, brules] = parser.parseDocument();
|
|
5604
5506
|
// console.log(JSON.stringify([prefixes, triples, frules, brules], null, 2));
|
|
5605
5507
|
|
|
5606
|
-
// Build internal ListTerm values from rdf:first/rdf:rest (+ rdf:nil)
|
|
5508
|
+
// Build internal ListTerm values from rdf:first/rdf:rest (+ rdf:nil)
|
|
5509
|
+
// input triples
|
|
5607
5510
|
materializeRdfLists(triples, frules, brules);
|
|
5608
5511
|
|
|
5609
5512
|
const facts = triples.filter((tr) => isGroundTriple(tr));
|
|
5610
5513
|
const derived = forwardChain(facts, frules, brules);
|
|
5611
|
-
|
|
5612
5514
|
const derivedTriples = derived.map((df) => df.fact);
|
|
5613
5515
|
const usedPrefixes = prefixes.prefixesUsedForOutput(derivedTriples);
|
|
5614
5516
|
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
'use strict';
|
|
1
|
+
+'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const os = require('node:os');
|
|
@@ -13,15 +13,23 @@ function reason(opt = {}, n3_input = '') {
|
|
|
13
13
|
|
|
14
14
|
// allow passing an args array directly
|
|
15
15
|
if (Array.isArray(opt)) opt = { args: opt };
|
|
16
|
+
if (opt == null || typeof opt !== 'object') opt = {};
|
|
16
17
|
|
|
17
18
|
const args = [];
|
|
18
19
|
|
|
19
20
|
// default: proof comments OFF for API output (machine-friendly)
|
|
20
21
|
// set { proofComments: true } to keep them
|
|
22
|
+
const proofCommentsSpecified = typeof opt.proofComments === 'boolean' || typeof opt.noProofComments === 'boolean';
|
|
23
|
+
|
|
21
24
|
const proofComments =
|
|
22
25
|
typeof opt.proofComments === 'boolean' ? opt.proofComments : typeof opt.noProofComments === 'boolean' ? !opt.noProofComments : false;
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
// Only pass a flag when the caller explicitly asked.
|
|
28
|
+
// (CLI default is now: no proof comments.)
|
|
29
|
+
if (proofCommentsSpecified) {
|
|
30
|
+
if (proofComments) args.push('--proof-comments');
|
|
31
|
+
else args.push('--no-proof-comments');
|
|
32
|
+
}
|
|
25
33
|
|
|
26
34
|
if (Array.isArray(opt.args)) args.push(...opt.args);
|
|
27
35
|
|
|
@@ -34,10 +42,7 @@ function reason(opt = {}, n3_input = '') {
|
|
|
34
42
|
fs.writeFileSync(inputFile, n3_input, 'utf8');
|
|
35
43
|
|
|
36
44
|
const eyelingPath = path.join(__dirname, 'eyeling.js');
|
|
37
|
-
const res = cp.spawnSync(process.execPath, [eyelingPath, ...args, inputFile], {
|
|
38
|
-
encoding: 'utf8',
|
|
39
|
-
maxBuffer,
|
|
40
|
-
});
|
|
45
|
+
const res = cp.spawnSync(process.execPath, [eyelingPath, ...args, inputFile], { encoding: 'utf8', maxBuffer });
|
|
41
46
|
|
|
42
47
|
if (res.error) throw res.error;
|
|
43
48
|
if (res.status !== 0) {
|
|
@@ -47,6 +52,7 @@ function reason(opt = {}, n3_input = '') {
|
|
|
47
52
|
err.stderr = res.stderr;
|
|
48
53
|
throw err;
|
|
49
54
|
}
|
|
55
|
+
|
|
50
56
|
return res.stdout;
|
|
51
57
|
} finally {
|
|
52
58
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -54,5 +60,6 @@ function reason(opt = {}, n3_input = '') {
|
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
module.exports = { reason };
|
|
63
|
+
|
|
57
64
|
// small interop nicety for ESM default import
|
|
58
65
|
module.exports.default = module.exports;
|