eyeling 1.26.4 → 1.26.6

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.
@@ -0,0 +1,55 @@
1
+ @prefix : <http://example.org/#> .
2
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
3
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
4
+ @prefix pe: <https://eyereasoner.github.io/pe#> .
5
+
6
+ :x :y 1 .
7
+ {
8
+ :x :y 1 .
9
+ } => {
10
+ :a :b :c .
11
+ } .
12
+ :a :b :c .
13
+
14
+ { :x :y 1 . } pe:why {
15
+ { :x :y 1 . }
16
+ pe:by [ pe:rule "log-not-includes.n3"; pe:line 10 ];
17
+ pe:uses { 1 math:equalTo 1 . }.
18
+
19
+ { 1 math:equalTo 1 . }
20
+ pe:by [ pe:builtin math:equalTo ].
21
+ }.
22
+
23
+ {
24
+ {
25
+ :x :y 1 .
26
+ } => {
27
+ :a :b :c .
28
+ } .
29
+ } pe:why {
30
+ {
31
+ {
32
+ :x :y 1 .
33
+ } => {
34
+ :a :b :c .
35
+ } .
36
+ }
37
+ pe:by [ pe:rule "log-not-includes.n3"; pe:line 10 ];
38
+ pe:uses { 1 math:equalTo 1 . }.
39
+
40
+ { 1 math:equalTo 1 . }
41
+ pe:by [ pe:builtin math:equalTo ].
42
+ }.
43
+
44
+ { :a :b :c . } pe:why {
45
+ { :a :b :c . }
46
+ pe:by [ pe:rule "log-not-includes.n3"; pe:line 10 ];
47
+ pe:uses { :x :y 1 . }.
48
+
49
+ { :x :y 1 . }
50
+ pe:by [ pe:rule "log-not-includes.n3"; pe:line 10 ];
51
+ pe:uses { 1 math:equalTo 1 . }.
52
+
53
+ { 1 math:equalTo 1 . }
54
+ pe:by [ pe:builtin math:equalTo ].
55
+ }.
@@ -0,0 +1,34 @@
1
+ @prefix : <http://example.org/#> .
2
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
3
+ @prefix pe: <https://eyereasoner.github.io/pe#> .
4
+
5
+ :uriString :is "https://www.w3.org" .
6
+ :uriResource :is <https://www.w3.org> .
7
+
8
+ { :uriString :is "https://www.w3.org" . } pe:why {
9
+ { :uriString :is "https://www.w3.org" . }
10
+ pe:by [ pe:rule "log-uri.n3"; pe:line 8 ];
11
+ pe:binding
12
+ [ pe:var "r"; pe:value <https://www.w3.org> ],
13
+ [ pe:var "s"; pe:value "https://www.w3.org" ];
14
+ pe:uses
15
+ { <https://www.w3.org> log:uri "https://www.w3.org" . },
16
+ { <https://www.w3.org> log:uri "https://www.w3.org" . }.
17
+
18
+ { <https://www.w3.org> log:uri "https://www.w3.org" . }
19
+ pe:by [ pe:builtin log:uri ].
20
+ }.
21
+
22
+ { :uriResource :is <https://www.w3.org> . } pe:why {
23
+ { :uriResource :is <https://www.w3.org> . }
24
+ pe:by [ pe:rule "log-uri.n3"; pe:line 8 ];
25
+ pe:binding
26
+ [ pe:var "r"; pe:value <https://www.w3.org> ],
27
+ [ pe:var "s"; pe:value "https://www.w3.org" ];
28
+ pe:uses
29
+ { <https://www.w3.org> log:uri "https://www.w3.org" . },
30
+ { <https://www.w3.org> log:uri "https://www.w3.org" . }.
31
+
32
+ { <https://www.w3.org> log:uri "https://www.w3.org" . }
33
+ pe:by [ pe:builtin log:uri ].
34
+ }.
@@ -0,0 +1,34 @@
1
+ @prefix : <https://eyereasoner.github.io/ns#> .
2
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
3
+ @prefix pe: <https://eyereasoner.github.io/pe#> .
4
+
5
+ :result :is true .
6
+
7
+ { :result :is true . } pe:why {
8
+ { :result :is true . }
9
+ pe:by [ pe:rule "rule-matching.n3"; pe:line 8 ];
10
+ pe:binding
11
+ [ pe:var "A"; pe:value {
12
+ ?A__0 => ?B__1 .
13
+ } ],
14
+ [ pe:var "B"; pe:value {
15
+ :result :is true .
16
+ } ];
17
+ pe:uses
18
+ {
19
+ {
20
+ ?A__0 => ?B__1 .
21
+ } => {
22
+ :result :is true .
23
+ } .
24
+ }.
25
+
26
+ {
27
+ {
28
+ ?A__0 => ?B__1 .
29
+ } => {
30
+ :result :is true .
31
+ } .
32
+ }
33
+ pe:by [ pe:builtin log:implies ].
34
+ }.
package/eyeling.js CHANGED
@@ -6212,16 +6212,16 @@ function __computeConclusionFromFormula(formula) {
6212
6212
  for (const tr of formula.triples) {
6213
6213
  // Treat {A} => {B} as a forward rule.
6214
6214
  if (isLogImplies(tr.p)) {
6215
- fw.push(__makeRuleFromTerms(tr.s, tr.o, true));
6215
+ fw.push(__copyProofSource(tr, __makeRuleFromTerms(tr.s, tr.o, true)));
6216
6216
  continue;
6217
6217
  }
6218
6218
 
6219
6219
  // Treat {A} <= {B} as the same rule in the other direction, i.e., {B} => {A},
6220
6220
  // so it participates in deductive closure even if only <= is used.
6221
6221
  if (isLogImpliedBy(tr.p)) {
6222
- fw.push(__makeRuleFromTerms(tr.o, tr.s, true));
6222
+ fw.push(__copyProofSource(tr, __makeRuleFromTerms(tr.o, tr.s, true)));
6223
6223
  // Also index it as a backward rule for completeness (helps proveGoals in some cases).
6224
- bw.push(__makeRuleFromTerms(tr.s, tr.o, false));
6224
+ bw.push(__copyProofSource(tr, __makeRuleFromTerms(tr.s, tr.o, false)));
6225
6225
  continue;
6226
6226
  }
6227
6227
  }
@@ -8735,7 +8735,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8735
8735
  // Promote rule-producing triples to live rules, treating literal true as {}
8736
8736
  // and literal false as a fuse head.
8737
8737
  if (isFwRuleTriple) {
8738
- const newRule = __makeRuleFromTerms(subj, obj, true);
8738
+ const newRule = __copyProofSource(r, __makeRuleFromTerms(subj, obj, true));
8739
8739
  __prepareForwardRule(newRule);
8740
8740
 
8741
8741
  const key = __ruleKey(
@@ -8752,7 +8752,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8752
8752
  rulesChanged = true;
8753
8753
  }
8754
8754
  } else if (isBwRuleTriple) {
8755
- const newRule = __makeRuleFromTerms(subj, obj, false);
8755
+ const newRule = __copyProofSource(r, __makeRuleFromTerms(subj, obj, false));
8756
8756
 
8757
8757
  const key = __ruleKey(
8758
8758
  newRule.isForward,
package/lib/engine.js CHANGED
@@ -717,16 +717,16 @@ function __computeConclusionFromFormula(formula) {
717
717
  for (const tr of formula.triples) {
718
718
  // Treat {A} => {B} as a forward rule.
719
719
  if (isLogImplies(tr.p)) {
720
- fw.push(__makeRuleFromTerms(tr.s, tr.o, true));
720
+ fw.push(__copyProofSource(tr, __makeRuleFromTerms(tr.s, tr.o, true)));
721
721
  continue;
722
722
  }
723
723
 
724
724
  // Treat {A} <= {B} as the same rule in the other direction, i.e., {B} => {A},
725
725
  // so it participates in deductive closure even if only <= is used.
726
726
  if (isLogImpliedBy(tr.p)) {
727
- fw.push(__makeRuleFromTerms(tr.o, tr.s, true));
727
+ fw.push(__copyProofSource(tr, __makeRuleFromTerms(tr.o, tr.s, true)));
728
728
  // Also index it as a backward rule for completeness (helps proveGoals in some cases).
729
- bw.push(__makeRuleFromTerms(tr.s, tr.o, false));
729
+ bw.push(__copyProofSource(tr, __makeRuleFromTerms(tr.s, tr.o, false)));
730
730
  continue;
731
731
  }
732
732
  }
@@ -3240,7 +3240,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3240
3240
  // Promote rule-producing triples to live rules, treating literal true as {}
3241
3241
  // and literal false as a fuse head.
3242
3242
  if (isFwRuleTriple) {
3243
- const newRule = __makeRuleFromTerms(subj, obj, true);
3243
+ const newRule = __copyProofSource(r, __makeRuleFromTerms(subj, obj, true));
3244
3244
  __prepareForwardRule(newRule);
3245
3245
 
3246
3246
  const key = __ruleKey(
@@ -3257,7 +3257,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3257
3257
  rulesChanged = true;
3258
3258
  }
3259
3259
  } else if (isBwRuleTriple) {
3260
- const newRule = __makeRuleFromTerms(subj, obj, false);
3260
+ const newRule = __copyProofSource(r, __makeRuleFromTerms(subj, obj, false));
3261
3261
 
3262
3262
  const key = __ruleKey(
3263
3263
  newRule.isForward,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.26.4",
3
+ "version": "1.26.6",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -52,20 +52,53 @@ function normalizeMarkdownForCompare(text) {
52
52
  // Normalize N3 output for comparison.
53
53
  // Eyeling (and other N3 tools) may emit the same closure with different
54
54
  // triple ordering. Examples tests should verify content, not presentation.
55
- function normalizeN3ForCompare(n3Text) {
56
- return stripTrailingWhitespace(n3Text)
55
+ function normalizeN3ForCompare(n3Text, sourceText = '', expectedPath = '') {
56
+ let value = stripTrailingWhitespace(n3Text);
57
+
58
+ // get-uuid.n3 intentionally uses log:skolem and relative IRIs, so its
59
+ // output depends on the checkout path and generated skolem seed. Compare the
60
+ // shape of the generated triples rather than those environment-specific bits.
61
+ if (/\blog:skolem\b/.test(sourceText) || path.basename(expectedPath) === 'get-uuid.n3') {
62
+ value = value
63
+ .replace(/<urn:uuid:[^>]+>/g, '<urn:uuid:__UUID__>')
64
+ .replace(/<file:\/\/[^>\s]*\/examples\/([^/>]+)>/g, '<file://__EXAMPLES__/$1>');
65
+ }
66
+
67
+ return value
57
68
  .split('\n')
58
69
  .filter((l) => l.length > 0)
59
70
  .sort((a, b) => a.localeCompare(b))
60
71
  .join('\n');
61
72
  }
62
73
 
63
- function normalizeForCompare(text, expectedPath) {
74
+ function normalizeForCompare(text, expectedPath, sourceText = '') {
64
75
  const ext = path.extname(expectedPath);
65
76
  const value = text;
66
77
  if (ext === '.md') return normalizeMarkdownForCompare(value);
67
78
  if (ext === '.txt') return normalizeTextForCompare(value);
68
- return normalizeN3ForCompare(value);
79
+ return normalizeN3ForCompare(value, sourceText, expectedPath);
80
+ }
81
+
82
+ function normalizeProofForCompare(text, expectedPath, sourceText) {
83
+ let value = normalizeForCompare(text, expectedPath, sourceText);
84
+
85
+ // Some proof goldens intentionally cover volatile builtins such as
86
+ // time:localTime. Keep those examples useful by comparing proof structure
87
+ // while masking only the volatile literal values.
88
+ if (/\btime:localTime\b/.test(sourceText)) {
89
+ value = value
90
+ .replace(/"[^"\n]*"\^\^xsd:dateTime/g, '"__DATETIME__"^^xsd:dateTime')
91
+ .replace(/"PT[0-9]+(?:\.[0-9]+)?S"\^\^xsd:duration/g, '"__DURATION__"^^xsd:duration');
92
+ }
93
+
94
+ return value;
95
+ }
96
+
97
+ function compareGeneratedOutput({ expectedPath, generatedPath, sourceText, proof }) {
98
+ const expectedText = fs.readFileSync(expectedPath, 'utf8');
99
+ const generatedText = fs.readFileSync(generatedPath, 'utf8');
100
+ const normalize = proof ? normalizeProofForCompare : normalizeForCompare;
101
+ return normalize(expectedText, expectedPath, sourceText) === normalize(generatedText, expectedPath, sourceText);
69
102
  }
70
103
 
71
104
  // Expectation logic (robust, long-term):
@@ -141,6 +174,42 @@ function resolveExampleBuiltinPath(root, inputFile) {
141
174
  return { abs, rel };
142
175
  }
143
176
 
177
+ function runExampleToFile({ root, examplesDir, eyelingJsPath, nodePath, file, generatedPath, proof = false }) {
178
+ const builtin = resolveExampleBuiltinPath(root, file);
179
+ const trigInput = resolveExampleTrigInput(root, file);
180
+ const rdfMode = !!trigInput;
181
+ const modeFlag = proof ? '-p' : '-d';
182
+ const outFd = fs.openSync(generatedPath, 'w');
183
+
184
+ try {
185
+ if (builtin) {
186
+ const args = [eyelingJsPath, modeFlag];
187
+ if (rdfMode) args.push('-r');
188
+ args.push('--builtin', builtin.rel, path.join('examples', file));
189
+ if (trigInput) args.push(path.join('examples', trigInput.rel));
190
+ return cp.spawnSync(nodePath, args, {
191
+ cwd: root,
192
+ stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
193
+ maxBuffer: 200 * 1024 * 1024,
194
+ encoding: 'utf8',
195
+ });
196
+ }
197
+
198
+ const args = [eyelingJsPath, modeFlag];
199
+ if (rdfMode) args.push('-r');
200
+ args.push(file);
201
+ if (trigInput) args.push(trigInput.rel);
202
+ return cp.spawnSync(nodePath, args, {
203
+ cwd: examplesDir,
204
+ stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
205
+ maxBuffer: 200 * 1024 * 1024,
206
+ encoding: 'utf8',
207
+ });
208
+ } finally {
209
+ fs.closeSync(outFd);
210
+ }
211
+ }
212
+
144
213
  function main() {
145
214
  const suiteStart = Date.now();
146
215
 
@@ -148,6 +217,7 @@ function main() {
148
217
  const root = path.resolve(__dirname, '..');
149
218
  const examplesDir = path.join(root, 'examples');
150
219
  const outputDir = path.join(examplesDir, 'output');
220
+ const proofDir = path.join(examplesDir, 'proof');
151
221
  const eyelingJsPath = path.join(root, 'eyeling.js');
152
222
  const nodePath = process.execPath;
153
223
 
@@ -164,8 +234,15 @@ function main() {
164
234
  .readdirSync(examplesDir)
165
235
  .filter((f) => f.endsWith('.n3'))
166
236
  .sort((a, b) => a.localeCompare(b));
167
-
168
- info(`Running ${files.length} examples tests`);
237
+ const proofFiles = fs.existsSync(proofDir)
238
+ ? fs
239
+ .readdirSync(proofDir)
240
+ .filter((f) => f.endsWith('.n3') && fs.existsSync(path.join(examplesDir, f)))
241
+ .sort((a, b) => a.localeCompare(b))
242
+ : [];
243
+ const totalTests = files.length + proofFiles.length;
244
+
245
+ info(`Running ${files.length} examples tests and ${proofFiles.length} proof golden tests`);
169
246
  console.log(`${C.dim}${getEyelingVersion(nodePath, eyelingJsPath, root)}; node ${process.version}${C.n}`);
170
247
 
171
248
  if (files.length === 0) {
@@ -178,6 +255,7 @@ function main() {
178
255
 
179
256
  // Pretty, stable numbering (e.g., 001..100 when running 100 tests)
180
257
  const idxWidth = String(files.length).length;
258
+ const proofIdxWidth = String(Math.max(proofFiles.length, 1)).length;
181
259
 
182
260
  for (let i = 0; i < files.length; i++) {
183
261
  const idx = String(i + 1).padStart(idxWidth, '0');
@@ -221,38 +299,7 @@ function main() {
221
299
  // node eyeling.js --builtin examples/builtin/foo.js examples/foo.n3
222
300
  // A matching examples/input/<stem>.trig sidecar is external RDF/TriG
223
301
  // evidence for this example, so include it and run in -r mode automatically.
224
- const builtin = resolveExampleBuiltinPath(root, file);
225
- const trigInput = resolveExampleTrigInput(root, file);
226
- const rdfMode = !!trigInput;
227
- const outFd = fs.openSync(generatedPath, 'w');
228
- let r;
229
- try {
230
- if (builtin) {
231
- const args = [eyelingJsPath, '-d'];
232
- if (rdfMode) args.push('-r');
233
- args.push('--builtin', builtin.rel, path.join('examples', file));
234
- if (trigInput) args.push(path.join('examples', trigInput.rel));
235
- r = cp.spawnSync(nodePath, args, {
236
- cwd: root,
237
- stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
238
- maxBuffer: 200 * 1024 * 1024,
239
- encoding: 'utf8',
240
- });
241
- } else {
242
- const args = [eyelingJsPath, '-d'];
243
- if (rdfMode) args.push('-r');
244
- args.push(file);
245
- if (trigInput) args.push(trigInput.rel);
246
- r = cp.spawnSync(nodePath, args, {
247
- cwd: examplesDir,
248
- stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
249
- maxBuffer: 200 * 1024 * 1024,
250
- encoding: 'utf8',
251
- });
252
- }
253
- } finally {
254
- fs.closeSync(outFd);
255
- }
302
+ const r = runExampleToFile({ root, examplesDir, eyelingJsPath, nodePath, file, generatedPath });
256
303
 
257
304
  const rc = r.status == null ? 1 : r.status;
258
305
 
@@ -261,12 +308,7 @@ function main() {
261
308
  // Compare output. N3 outputs are order-insensitive; Markdown outputs are order-sensitive.
262
309
  let diffOk = false;
263
310
  try {
264
- const expectedText = fs.readFileSync(expectedPath, 'utf8');
265
- const generatedText = fs.readFileSync(generatedPath, 'utf8');
266
- if (expectedText == null) throw new Error('missing expected output');
267
- diffOk =
268
- normalizeForCompare(expectedText, expectedPath) ===
269
- normalizeForCompare(generatedText, expectedPath);
311
+ diffOk = compareGeneratedOutput({ expectedPath, generatedPath, sourceText: n3Text, proof: false });
270
312
  } catch {
271
313
  diffOk = false;
272
314
  }
@@ -302,15 +344,76 @@ function main() {
302
344
  if (tmpDir) rmrf(tmpDir);
303
345
  }
304
346
 
347
+
348
+ for (let i = 0; i < proofFiles.length; i++) {
349
+ const idx = String(i + 1).padStart(proofIdxWidth, '0');
350
+ const file = proofFiles[i];
351
+ const start = Date.now();
352
+ const filePath = path.join(examplesDir, file);
353
+ const expectedPath = path.join(proofDir, file);
354
+
355
+ let n3Text;
356
+ try {
357
+ n3Text = fs.readFileSync(filePath, 'utf8');
358
+ } catch (e) {
359
+ const ms = Date.now() - start;
360
+ fail(`proof ${idx} ${file} ${msTag(ms)}`);
361
+ fail(`Cannot read proof input: ${e.message}`);
362
+ failed++;
363
+ continue;
364
+ }
365
+
366
+ const expectedRc = expectedExitCode(n3Text);
367
+ const tmpDir = mkTmpDir();
368
+ const generatedPath = path.join(tmpDir, 'generated.n3');
369
+ const r = runExampleToFile({ root, examplesDir, eyelingJsPath, nodePath, file, generatedPath, proof: true });
370
+ const rc = r.status == null ? 1 : r.status;
371
+ const ms = Date.now() - start;
372
+
373
+ let diffOk = false;
374
+ try {
375
+ diffOk = compareGeneratedOutput({ expectedPath, generatedPath, sourceText: n3Text, proof: true });
376
+ } catch {
377
+ diffOk = false;
378
+ }
379
+
380
+ const rcOk = rc === expectedRc;
381
+
382
+ if (diffOk && rcOk) {
383
+ if (expectedRc === 0) {
384
+ ok(`proof ${idx} ${file} ${msTag(ms)}`);
385
+ } else {
386
+ ok(`proof ${idx} ${file} (expected exit ${expectedRc}) ${msTag(ms)}`);
387
+ }
388
+ passed++;
389
+ } else {
390
+ fail(`proof ${idx} ${file} ${msTag(ms)}`);
391
+ if (!rcOk) {
392
+ fail(`Exit code ${rc}, expected ${expectedRc}`);
393
+ }
394
+ if (!diffOk) {
395
+ fail('Proof output differs');
396
+ }
397
+ showDiff({
398
+ examplesDir,
399
+ expectedPath,
400
+ generatedPath,
401
+ });
402
+ failed++;
403
+ }
404
+
405
+ if (tmpDir) rmrf(tmpDir);
406
+ }
407
+
305
408
  console.log('');
306
409
  const suiteMs = Date.now() - suiteStart;
307
410
  info(`Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
308
411
 
309
412
  if (failed === 0) {
310
- ok(`All examples tests passed (${passed}/${files.length})`);
413
+ ok(`All examples tests passed (${passed}/${totalTests})`);
311
414
  process.exit(0);
312
415
  } else {
313
- fail(`Some examples tests failed (${passed}/${files.length})`);
416
+ fail(`Some examples tests failed (${passed}/${totalTests})`);
314
417
  // keep exit code 2 (matches historical behavior of examples/test)
315
418
  process.exit(2);
316
419
  }