eyeling 1.22.6 → 1.22.7

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 (38) hide show
  1. package/HANDBOOK.md +245 -0
  2. package/dist/browser/eyeling.browser.js +128 -11
  3. package/examples/act-alarm-bit-interoperability.n3 +180 -0
  4. package/examples/act-barley-seed-lineage.n3 +565 -0
  5. package/examples/act-docking-abort.n3 +285 -0
  6. package/examples/act-gravity-mediator-witness.n3 +235 -0
  7. package/examples/act-isolation-breach.n3 +354 -0
  8. package/examples/act-photosynthetic-exciton-transfer.n3 +245 -0
  9. package/examples/act-sensor-memory-reset.n3 +190 -0
  10. package/examples/act-tunnel-junction-wake-switch.n3 +225 -0
  11. package/examples/act-yeast-self-reproduction.n3 +248 -0
  12. package/examples/complex-matrix-stability.n3 +288 -0
  13. package/examples/deck/act-barley-seed-lineage.md +593 -0
  14. package/examples/fundamental-theorem-arithmetic.n3 +244 -0
  15. package/examples/harborsmr.n3 +233 -0
  16. package/examples/meta-rule-audit.n3 +135 -0
  17. package/examples/output/act-alarm-bit-interoperability.txt +20 -0
  18. package/examples/output/act-barley-seed-lineage.txt +25 -0
  19. package/examples/output/act-docking-abort.txt +22 -0
  20. package/examples/output/act-gravity-mediator-witness.txt +24 -0
  21. package/examples/output/act-isolation-breach.txt +27 -0
  22. package/examples/output/act-photosynthetic-exciton-transfer.txt +20 -0
  23. package/examples/output/act-sensor-memory-reset.txt +20 -0
  24. package/examples/output/act-tunnel-junction-wake-switch.txt +21 -0
  25. package/examples/output/act-yeast-self-reproduction.txt +23 -0
  26. package/examples/output/complex-matrix-stability.txt +14 -0
  27. package/examples/output/fundamental-theorem-arithmetic.txt +15 -0
  28. package/examples/output/get-uuid.n3 +2 -2
  29. package/examples/output/harborsmr.txt +20 -0
  30. package/examples/output/meta-rule-audit.n3 +44 -0
  31. package/examples/output/theory-diff.n3 +22 -0
  32. package/examples/theory-diff.n3 +125 -0
  33. package/eyeling.js +128 -11
  34. package/lib/builtins.js +18 -1
  35. package/lib/cli.js +31 -5
  36. package/lib/engine.js +79 -5
  37. package/package.json +1 -1
  38. package/test/api.test.js +69 -0
package/lib/cli.js CHANGED
@@ -7,7 +7,11 @@
7
7
 
8
8
  'use strict';
9
9
 
10
+ const path = require('node:path');
11
+ const { pathToFileURL } = require('node:url');
12
+
10
13
  const engine = require('./engine');
14
+ const deref = require('./deref');
11
15
  const { PrefixEnv } = require('./prelude');
12
16
 
13
17
  function offsetToLineCol(text, offset) {
@@ -50,6 +54,29 @@ function readTextFromStdinSync() {
50
54
  return fs.readFileSync(0, { encoding: 'utf8' });
51
55
  }
52
56
 
57
+ function __isNetworkOrFileIri(s) {
58
+ return typeof s === 'string' && /^(https?:|file:\/\/)/i.test(s);
59
+ }
60
+
61
+ function __sourceLabelToBaseIri(sourceLabel) {
62
+ if (!sourceLabel || sourceLabel === '<stdin>') return '';
63
+ if (__isNetworkOrFileIri(sourceLabel)) return deref.stripFragment(sourceLabel);
64
+ return pathToFileURL(path.resolve(sourceLabel)).toString();
65
+ }
66
+
67
+ function __readInputSourceSync(sourceLabel) {
68
+ if (sourceLabel === '<stdin>') return readTextFromStdinSync();
69
+
70
+ if (__isNetworkOrFileIri(sourceLabel)) {
71
+ const txt = deref.derefTextSync(sourceLabel);
72
+ if (typeof txt !== 'string') throw new Error(`Failed to dereference ${sourceLabel}`);
73
+ return txt;
74
+ }
75
+
76
+ const fs = require('node:fs');
77
+ return fs.readFileSync(sourceLabel, { encoding: 'utf8' });
78
+ }
79
+
53
80
  function main() {
54
81
  // Drop "node" and script name; keep only user-provided args
55
82
  // Expand combined short options: -pt == -p -t
@@ -163,13 +190,11 @@ function main() {
163
190
  }
164
191
 
165
192
  const sourceLabel = useImplicitStdin || positional[0] === '-' ? '<stdin>' : positional[0];
193
+ const baseIri = __sourceLabelToBaseIri(sourceLabel);
194
+
166
195
  let text;
167
196
  try {
168
- if (sourceLabel === '<stdin>') text = readTextFromStdinSync();
169
- else {
170
- const fs = require('node:fs');
171
- text = fs.readFileSync(sourceLabel, { encoding: 'utf8' });
172
- }
197
+ text = __readInputSourceSync(sourceLabel);
173
198
  } catch (e) {
174
199
  if (sourceLabel === '<stdin>') console.error(`Error reading stdin: ${e.message}`);
175
200
  else console.error(`Error reading file ${JSON.stringify(sourceLabel)}: ${e.message}`);
@@ -181,6 +206,7 @@ function main() {
181
206
  try {
182
207
  toks = engine.lex(text);
183
208
  const parser = new engine.Parser(toks);
209
+ if (baseIri) parser.prefixes.setBase(baseIri);
184
210
  [prefixes, triples, frules, brules, qrules] = parser.parseDocument();
185
211
  // Make the parsed prefixes available to log:trace output (CLI path)
186
212
  engine.setTracePrefixes(prefixes);
package/lib/engine.js CHANGED
@@ -199,21 +199,80 @@ function __isStrictGroundTriple(tr) {
199
199
  // -----------------------------------------------------------------------------
200
200
  // Used to maintain O(1) membership sets for dynamically promoted rules, and to
201
201
  // memoize per-firing head-blank skolemization.
202
+ //
203
+ // Important: variables and blank nodes *inside quoted formulas* are local to the
204
+ // formula. Canonicalize those labels by first occurrence so alpha-equivalent
205
+ // formulas (for example the repeated results of log:semantics after
206
+ // standardize-apart) get the same identity key. Keep top-level blank labels
207
+ // untouched so distinct existential witnesses in the global fact set do not
208
+ // collapse together.
209
+ function __keyFromTermForRuleIdentity(term) {
210
+ const ctx = { quotedVar: new Map(), quotedBlank: new Map() };
211
+
212
+ function canonQuotedVar(name) {
213
+ let out = ctx.quotedVar.get(name);
214
+ if (!out) {
215
+ out = `v${ctx.quotedVar.size}`;
216
+ ctx.quotedVar.set(name, out);
217
+ }
218
+ return out;
219
+ }
220
+
221
+ function canonQuotedBlank(label) {
222
+ let out = ctx.quotedBlank.get(label);
223
+ if (!out) {
224
+ out = `b${ctx.quotedBlank.size}`;
225
+ ctx.quotedBlank.set(label, out);
226
+ }
227
+ return out;
228
+ }
229
+
230
+ function enc(u, inQuotedFormula) {
231
+ if (u instanceof Iri) return ['I', u.value];
232
+ if (u instanceof Literal) return ['L', u.value];
233
+ if (u instanceof Blank) return inQuotedFormula ? ['BQ', canonQuotedBlank(u.label)] : ['B', u.label];
234
+ if (u instanceof Var) return inQuotedFormula ? ['VQ', canonQuotedVar(u.name)] : ['V', u.name];
235
+ if (u instanceof ListTerm) return ['List', u.elems.map((e) => enc(e, inQuotedFormula))];
236
+ if (u instanceof OpenListTerm) {
237
+ return [
238
+ 'OpenList',
239
+ u.prefix.map((e) => enc(e, inQuotedFormula)),
240
+ inQuotedFormula ? ['TailVQ', canonQuotedVar(u.tailVar)] : ['TailV', u.tailVar],
241
+ ];
242
+ }
243
+ if (u instanceof GraphTerm)
244
+ return ['Graph', u.triples.map((tr) => [enc(tr.s, true), enc(tr.p, true), enc(tr.o, true)])];
245
+ return ['Other', String(u)];
246
+ }
247
+
248
+ return JSON.stringify(enc(term, false));
249
+ }
250
+
202
251
  function __ruleKey(isForward, isFuse, premise, conclusion, dynamicConclusionTerm /* optional */) {
203
252
  let out = (isForward ? 'F' : 'B') + (isFuse ? '!' : '') + '|P|';
204
253
  for (let i = 0; i < premise.length; i++) {
205
254
  const tr = premise[i];
206
255
  if (i) out += '\n';
207
- out += skolemKeyFromTerm(tr.s) + '\t' + skolemKeyFromTerm(tr.p) + '\t' + skolemKeyFromTerm(tr.o);
256
+ out +=
257
+ __keyFromTermForRuleIdentity(tr.s) +
258
+ '\t' +
259
+ __keyFromTermForRuleIdentity(tr.p) +
260
+ '\t' +
261
+ __keyFromTermForRuleIdentity(tr.o);
208
262
  }
209
263
  out += '|C|';
210
264
  for (let i = 0; i < conclusion.length; i++) {
211
265
  const tr = conclusion[i];
212
266
  if (i) out += '\n';
213
- out += skolemKeyFromTerm(tr.s) + '\t' + skolemKeyFromTerm(tr.p) + '\t' + skolemKeyFromTerm(tr.o);
267
+ out +=
268
+ __keyFromTermForRuleIdentity(tr.s) +
269
+ '\t' +
270
+ __keyFromTermForRuleIdentity(tr.p) +
271
+ '\t' +
272
+ __keyFromTermForRuleIdentity(tr.o);
214
273
  }
215
274
  if (dynamicConclusionTerm) {
216
- out += '|T|' + skolemKeyFromTerm(dynamicConclusionTerm);
275
+ out += '|T|' + __keyFromTermForRuleIdentity(dynamicConclusionTerm);
217
276
  }
218
277
  return out;
219
278
  }
@@ -224,7 +283,12 @@ function __firingKey(ruleIndex, instantiatedPremises) {
224
283
  for (let i = 0; i < instantiatedPremises.length; i++) {
225
284
  const tr = instantiatedPremises[i];
226
285
  if (i) out += '\n';
227
- out += skolemKeyFromTerm(tr.s) + '\t' + skolemKeyFromTerm(tr.p) + '\t' + skolemKeyFromTerm(tr.o);
286
+ out +=
287
+ __keyFromTermForRuleIdentity(tr.s) +
288
+ '\t' +
289
+ __keyFromTermForRuleIdentity(tr.p) +
290
+ '\t' +
291
+ __keyFromTermForRuleIdentity(tr.o);
228
292
  }
229
293
  return out;
230
294
  }
@@ -2464,7 +2528,17 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
2464
2528
  if (remaining <= 0) continue;
2465
2529
  const builtinMax = Number.isFinite(remaining) && !restGoals.length ? remaining : undefined;
2466
2530
 
2467
- let deltas = evalBuiltin(goal0, {}, facts, backRules, frame.curDepth, varGen, builtinMax);
2531
+ const builtinGoalForEval = goalPredicateIri === LOG_NS + 'rawType' ? rawGoal : goal0;
2532
+ const builtinSubstForEval = goalPredicateIri === LOG_NS + 'rawType' ? substMut : {};
2533
+ let deltas = evalBuiltin(
2534
+ builtinGoalForEval,
2535
+ builtinSubstForEval,
2536
+ facts,
2537
+ backRules,
2538
+ frame.curDepth,
2539
+ varGen,
2540
+ builtinMax,
2541
+ );
2468
2542
 
2469
2543
  const dc = typeof frame.deferCount === 'number' ? frame.deferCount : 0;
2470
2544
  const builtinDeltasAreVacuous = deltas.length > 0 && deltas.every((d) => Object.keys(d).length === 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.22.6",
3
+ "version": "1.22.7",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
4
5
  const path = require('node:path');
5
6
  const { spawnSync } = require('node:child_process');
6
7
  const ROOT = path.resolve(__dirname, '..');
@@ -2236,6 +2237,74 @@ _:x :hates { _:foo :making :mess }.
2236
2237
  `,
2237
2238
  expect: [/^:result\s+:is\s+\{[\s\S]*\?A\s+a\s+\?B\s*\.[\s\S]*\?B\s+rdfs:subClassOf\s+\?C\s*\.[\s\S]*\}\s*\./m],
2238
2239
  },
2240
+ {
2241
+ name: 'regression: log:rawType accepts quoted variables found via log:includes',
2242
+ opt: { proofComments: false },
2243
+ input: `@prefix log: <http://www.w3.org/2000/10/swap/log#> .
2244
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
2245
+ @prefix : <http://example.org/ns#> .
2246
+
2247
+ { ?X :likes ?Y } <= { ?X :loves ?Y }.
2248
+
2249
+ {
2250
+ ?X log:impliedBy ?Y .
2251
+ ?X log:includes { ?Z1 :likes ?Z2 }.
2252
+ ?Z1 log:rawType ?T.
2253
+ }
2254
+ =>
2255
+ {
2256
+ :test :is true .
2257
+ }.
2258
+ `,
2259
+ expect: [/^:test\s+:is\s+true\s*\./m],
2260
+ },
2261
+ {
2262
+ name: 'regression: log:semantics body alpha-renaming does not refire blank-head rule forever',
2263
+ async run() {
2264
+ const os = require('node:os');
2265
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeling-alpha-fire-'));
2266
+ const mainPath = path.join(tmp, 'main.n3');
2267
+ const examplePath = path.join(tmp, 'example.n3');
2268
+
2269
+ fs.writeFileSync(
2270
+ mainPath,
2271
+ `@prefix log: <http://www.w3.org/2000/10/swap/log#> .
2272
+ @prefix : <urn:test#>.
2273
+
2274
+ <> :facts <./example.n3> .
2275
+
2276
+ { ?X :load ?S } <= { <> :facts ?F . ?F log:semantics ?S . }.
2277
+
2278
+ { ?This :load ?S . } => { [] a :Test . }.
2279
+ `,
2280
+ 'utf8',
2281
+ );
2282
+
2283
+ fs.writeFileSync(
2284
+ examplePath,
2285
+ `@prefix : <urn:test#>.
2286
+ { ?A :p ?B } <= { ?A :q ?B }.
2287
+ `,
2288
+ 'utf8',
2289
+ );
2290
+
2291
+ try {
2292
+ const r = spawnSync(process.execPath, [path.join(ROOT, 'bin', 'eyeling.cjs'), mainPath], {
2293
+ cwd: ROOT,
2294
+ encoding: 'utf8',
2295
+ maxBuffer: DEFAULT_MAX_BUFFER,
2296
+ timeout: 5000,
2297
+ });
2298
+
2299
+ if (r.error) throw r.error;
2300
+ assert.equal(r.status, 0, r.stderr || `unexpected exit ${r.status}`);
2301
+ mustOccurExactly(r.stdout, /^_:sk_\d+\s+a\s+:Test\s*\.$/gm, 1, 'expected exactly one derived :Test witness');
2302
+ return r.stdout;
2303
+ } finally {
2304
+ fs.rmSync(tmp, { recursive: true, force: true });
2305
+ }
2306
+ },
2307
+ },
2239
2308
  ];
2240
2309
 
2241
2310
  let passed = 0;