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.
Files changed (4) hide show
  1. package/README.md +8 -19
  2. package/eyeling.js +196 -294
  3. package/index.js +13 -6
  4. 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
- ### Run a single file
106
+ ### Usage
107
107
 
108
- From the repo root:
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
- # Option 2: explicit node
115
- node eyeling.js examples/socrates.n3
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 = true;
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 unifyTerm(b, a, subst);
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
- // String-literal match (RDF 1.1): treat plain strings and xsd:string as equal (but not @lang)
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 match: treat untyped true/false tokens and xsd:boolean as equal.
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 for literals, BUT ONLY when datatypes agree (or infer to agree)
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
- // Formulas:
2129
- // 1) If they are alpha-equivalent, succeed without leaking internal bindings.
2130
- // 2) Otherwise fall back to full unification (may bind vars).
2131
- if (a instanceof FormulaTerm && b instanceof FormulaTerm) {
2132
- if (alphaEqFormulaTriples(a.triples, b.triples)) return { ...subst };
2133
- return unifyFormulaTriples(a.triples, b.triples, subst);
2134
- }
2135
- return null;
2136
- }
2137
-
2138
- function unifyTermListAppend(a, b, subst) {
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 = unifyTermListAppend(a.prefix[i], b.prefix[i], 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 = unifyTermListAppend(a.elems[i], b.elems[i], 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 absSecs = Math.abs(secs);
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
- if (!(g.s instanceof ListTerm)) return [];
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
- // Closed list: (a b c) -> (b c)
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
- while (true) {
5072
- let changed = false;
5073
-
5074
- for (let i = 0; i < forwardRules.length; i++) {
5075
- const r = forwardRules[i];
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
- const sols = proveGoals(r.premise.slice(), empty, facts, backRules, 0, visited, varGen);
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
- // Inference fuse
5082
- if (r.isFuse && sols.length) {
5083
- console.log('# Inference fuse triggered: a { ... } => false. rule fired.');
5084
- process.exit(2);
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
- for (const s of sols) {
5088
- // IMPORTANT: one skolem map per *rule firing* so head blank nodes
5089
- // (e.g., from [ :p ... ; :q ... ]) stay connected across all head triples.
5090
- const skMap = {};
5091
- const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
5092
- const fireKey = firingKey(i, instantiatedPremises);
5093
-
5094
- for (const cpat of r.conclusion) {
5095
- const instantiated = applySubstTriple(cpat, s);
5096
-
5097
- const isFwRuleTriple =
5098
- isLogImplies(instantiated.p) &&
5099
- ((instantiated.s instanceof FormulaTerm && instantiated.o instanceof FormulaTerm) ||
5100
- (instantiated.s instanceof Literal && instantiated.s.value === 'true' && instantiated.o instanceof FormulaTerm) ||
5101
- (instantiated.s instanceof FormulaTerm && instantiated.o instanceof Literal && instantiated.o.value === 'true'));
5102
-
5103
- const isBwRuleTriple =
5104
- isLogImpliedBy(instantiated.p) &&
5105
- ((instantiated.s instanceof FormulaTerm && instantiated.o instanceof FormulaTerm) ||
5106
- (instantiated.s instanceof FormulaTerm && instantiated.o instanceof Literal && instantiated.o.value === 'true') ||
5107
- (instantiated.s instanceof Literal && instantiated.s.value === 'true' && instantiated.o instanceof FormulaTerm));
5108
-
5109
- if (isFwRuleTriple || isBwRuleTriple) {
5110
- if (!hasFactIndexed(facts, instantiated)) {
5111
- factList.push(instantiated);
5112
- pushFactIndexed(facts, instantiated);
5113
- derivedForward.push(
5114
- new DerivedFact(instantiated, r, instantiatedPremises.slice(), {
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
- changed = true;
5119
- }
5120
-
5121
- // Promote rule-producing triples to live rules, treating literal true as {}.
5122
- const left =
5123
- instantiated.s instanceof FormulaTerm
5124
- ? instantiated.s.triples
5125
- : instantiated.s instanceof Literal && instantiated.s.value === 'true'
5126
- ? []
5127
- : null;
5128
-
5129
- const right =
5130
- instantiated.o instanceof FormulaTerm
5131
- ? instantiated.o.triples
5132
- : instantiated.o instanceof Literal && instantiated.o.value === 'true'
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
- // Only skolemize blank nodes that occur explicitly in the rule head
5176
- const inst = skolemizeTripleForHeadBlanks(instantiated, r.headBlankLabels, skMap, skCounter, fireKey, headSkolemCache);
5052
+ continue; // skip normal fact handling
5053
+ }
5177
5054
 
5178
- if (!isGroundTriple(inst)) continue;
5179
- if (hasFactIndexed(facts, inst)) continue;
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
- factList.push(inst);
5182
- pushFactIndexed(facts, inst);
5058
+ if (!isGroundTriple(inst)) continue;
5059
+ if (hasFactIndexed(facts, inst)) continue;
5183
5060
 
5184
- derivedForward.push(new DerivedFact(inst, r, instantiatedPremises.slice(), { ...s }));
5185
- changed = true;
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
- // --no-proof-comments / -n: disable proof explanations
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('Usage: eyeling.js [--version|-v] [--no-proof-comments|-n] <file.n3>');
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) input triples
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
- if (!proofComments) args.push('--no-proof-comments'); // CLI already supports this :contentReference[oaicite:1]{index=1}
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.6.14",
3
+ "version": "1.6.16",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [