eyeling 1.5.24 → 1.5.25

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 (2) hide show
  1. package/package.json +1 -1
  2. package/test/api.test.js +241 -165
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.5.24",
3
+ "version": "1.5.25",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -8,9 +8,15 @@ const C = TTY
8
8
  ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
9
9
  : { g: '', r: '', y: '', dim: '', n: '' };
10
10
 
11
- function ok(msg) { console.log(`${C.g}OK${C.n} ${msg}`); }
12
- function info(msg) { console.log(`${C.y}==${C.n} ${msg}`); }
13
- function fail(msg) { console.error(`${C.r}FAIL${C.n} ${msg}`); }
11
+ function ok(msg) {
12
+ console.log(`${C.g}OK${C.n} ${msg}`);
13
+ }
14
+ function info(msg) {
15
+ console.log(`${C.y}==${C.n} ${msg}`);
16
+ }
17
+ function fail(msg) {
18
+ console.error(`${C.r}FAIL${C.n} ${msg}`);
19
+ }
14
20
 
15
21
  function msNow() {
16
22
  return Date.now();
@@ -24,8 +30,21 @@ function mustNotMatch(output, re, label) {
24
30
  assert.ok(!re.test(output), label || `Expected output NOT to match ${re}`);
25
31
  }
26
32
 
27
- const EX = 'http://example.org/';
33
+ function countMatches(output, re) {
34
+ // ensure global counting without mutating caller regex
35
+ const flags = re.flags.includes('g') ? re.flags : re.flags + 'g';
36
+ const rg = new RegExp(re.source, flags);
37
+ let c = 0;
38
+ while (rg.exec(output)) c++;
39
+ return c;
40
+ }
41
+
42
+ function mustOccurExactly(output, re, n, label) {
43
+ const c = countMatches(output, re);
44
+ assert.equal(c, n, label || `Expected ${n} matches of ${re}, got ${c}`);
45
+ }
28
46
 
47
+ const EX = 'http://example.org/';
29
48
  // Helper to build a URI quickly
30
49
  const U = (path) => `<${EX}${path}>`;
31
50
 
@@ -43,7 +62,6 @@ function parentChainN3(n) {
43
62
  }
44
63
 
45
64
  function subclassChainN3(n) {
46
- // C0 sub C1 ... Cn sub C(n+1)
47
65
  let s = '';
48
66
  for (let i = 0; i <= n; i++) {
49
67
  s += `${U(`C${i}`)} ${U('sub')} ${U(`C${i + 1}`)}.\n`;
@@ -54,7 +72,6 @@ function subclassChainN3(n) {
54
72
  }
55
73
 
56
74
  function ruleChainN3(n) {
57
- // p0 -> p1 -> ... -> pn, starting from (s p0 o)
58
75
  let s = '';
59
76
  for (let i = 0; i < n; i++) {
60
77
  s += `{ ${U('s')} ${U(`p${i}`)} ${U('o')}. } => { ${U('s')} ${U(`p${i + 1}`)} ${U('o')}. }.\n`;
@@ -64,9 +81,7 @@ function ruleChainN3(n) {
64
81
  }
65
82
 
66
83
  function binaryTreeParentN3(depth) {
67
- // breadth-first numbering: node i has children 2i+1 and 2i+2
68
- // depth=0 -> 1 node; depth=1 -> 3 nodes; depth=4 -> 31 nodes
69
- const maxNode = (1 << (depth + 1)) - 2; // last index at given depth
84
+ const maxNode = (1 << (depth + 1)) - 2;
70
85
  let s = '';
71
86
 
72
87
  for (let i = 0; i <= maxNode; i++) {
@@ -90,12 +105,10 @@ function transitiveClosureN3(pred) {
90
105
  }
91
106
 
92
107
  function reachabilityGraphN3(n) {
93
- // chain plus a few extra edges for branching
94
108
  let s = '';
95
109
  for (let i = 0; i < n; i++) {
96
110
  s += `${U(`g${i}`)} ${U('edge')} ${U(`g${i + 1}`)}.\n`;
97
111
  }
98
- // add some shortcuts/branches
99
112
  if (n >= 6) {
100
113
  s += `${U('g0')} ${U('edge')} ${U('g3')}.\n`;
101
114
  s += `${U('g2')} ${U('edge')} ${U('g5')}.\n`;
@@ -121,7 +134,6 @@ ${U('x')} ${U('type')} ${U('A')}.
121
134
  }
122
135
 
123
136
  function join3HopN3(k) {
124
- // a0 --p--> a1 --p--> a2 --p--> ... ; rule derives hop3 edges
125
137
  let s = '';
126
138
  for (let i = 0; i < k; i++) {
127
139
  s += `${U(`j${i}`)} ${U('p')} ${U(`j${i + 1}`)}.\n`;
@@ -162,7 +174,6 @@ function bigFactsN3(n) {
162
174
  }
163
175
 
164
176
  function negativeEntailmentBatchN3(n) {
165
- // if any forbidden fact exists, derive false
166
177
  let s = '';
167
178
  for (let i = 0; i < n; i++) {
168
179
  s += `${U('x')} ${U('ok')} ${U(`v${i}`)}.\n`;
@@ -173,7 +184,6 @@ function negativeEntailmentBatchN3(n) {
173
184
  }
174
185
 
175
186
  function symmetricTransitiveN3() {
176
- // friend is symmetric; reachFriend is transitive closure over friend edges
177
187
  return `
178
188
  ${U('a')} ${U('friend')} ${U('b')}.
179
189
  ${U('b')} ${U('friend')} ${U('c')}.
@@ -185,62 +195,6 @@ ${U('c')} ${U('friend')} ${U('d')}.
185
195
  `;
186
196
  }
187
197
 
188
- function mkChainRewriteCase(i, steps) {
189
- const input = ruleChainN3(steps); // already defined earlier
190
- return {
191
- name: `${String(i).padStart(2, '0')} chain rewrite: ${steps} steps`,
192
- opt: { proofComments: false },
193
- input,
194
- expect: [new RegExp(`${EX}s>\\s+<${EX}p${steps}>\\s+<${EX}o>\\s*\\.`)],
195
- };
196
- }
197
-
198
- function mkSubclassChainCase(i, steps) {
199
- const input = subclassChainN3(steps); // already defined earlier
200
- return {
201
- name: `${String(i).padStart(2, '0')} subclass chain: ${steps} steps`,
202
- opt: { proofComments: false },
203
- input,
204
- expect: [new RegExp(`${EX}x>\\s+<${EX}type>\\s+<${EX}C${steps + 1}>\\s*\\.`)],
205
- };
206
- }
207
-
208
- function mkParentChainCase(i, links) {
209
- const input = parentChainN3(links); // already defined earlier
210
- return {
211
- name: `${String(i).padStart(2, '0')} ancestor chain: ${links} links`,
212
- opt: { proofComments: false },
213
- input,
214
- expect: [new RegExp(`${EX}n0>\\s+<${EX}ancestor>\\s+<${EX}n${links}>\\s*\\.`)],
215
- };
216
- }
217
-
218
- function mkJoinCase(i, len) {
219
- const input = join3HopN3(len); // already defined earlier
220
- // Check a couple of hop-3 inferences that always exist for len>=6
221
- return {
222
- name: `${String(i).padStart(2, '0')} 3-hop join over chain len ${len}`,
223
- opt: { proofComments: false },
224
- input,
225
- expect: [
226
- new RegExp(`${EX}j0>\\s+<${EX}p3>\\s+<${EX}j3>\\s*\\.`),
227
- new RegExp(`${EX}j2>\\s+<${EX}p3>\\s+<${EX}j5>\\s*\\.`),
228
- ],
229
- };
230
- }
231
-
232
- function mkBranchReachCase(i, n) {
233
- const input = reachabilityGraphN3(n); // already defined earlier
234
- return {
235
- name: `${String(i).padStart(2, '0')} reachability: n=${n}`,
236
- opt: { proofComments: false },
237
- input,
238
- expect: [
239
- new RegExp(`${EX}g0>\\s+<${EX}reach>\\s+<${EX}g${n}>\\s*\\.`),
240
- ],
241
- };
242
- }
243
-
244
198
  const cases = [
245
199
  {
246
200
  name: '01 forward rule: p -> q',
@@ -249,11 +203,8 @@ const cases = [
249
203
  { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }.
250
204
  ${U('s')} ${U('p')} ${U('o')}.
251
205
  `,
252
- expect: [
253
- new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`),
254
- ],
206
+ expect: [new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`)],
255
207
  },
256
-
257
208
  {
258
209
  name: '02 two-step: p -> q -> r',
259
210
  opt: { proofComments: false },
@@ -262,11 +213,8 @@ ${U('s')} ${U('p')} ${U('o')}.
262
213
  { ${U('s')} ${U('q')} ${U('o')}. } => { ${U('s')} ${U('r')} ${U('o')}. }.
263
214
  ${U('s')} ${U('p')} ${U('o')}.
264
215
  `,
265
- expect: [
266
- new RegExp(`${EX}s>\\s+<${EX}r>\\s+<${EX}o>\\s*\\.`),
267
- ],
216
+ expect: [new RegExp(`${EX}s>\\s+<${EX}r>\\s+<${EX}o>\\s*\\.`)],
268
217
  },
269
-
270
218
  {
271
219
  name: '03 join antecedents: (x p y & y p z) -> (x p2 z)',
272
220
  opt: { proofComments: false },
@@ -275,11 +223,8 @@ ${U('s')} ${U('p')} ${U('o')}.
275
223
  ${U('a')} ${U('p')} ${U('b')}.
276
224
  ${U('b')} ${U('p')} ${U('c')}.
277
225
  `,
278
- expect: [
279
- new RegExp(`${EX}a>\\s+<${EX}p2>\\s+<${EX}c>\\s*\\.`),
280
- ],
226
+ expect: [new RegExp(`${EX}a>\\s+<${EX}p2>\\s+<${EX}c>\\s*\\.`)],
281
227
  },
282
-
283
228
  {
284
229
  name: '04 inverse relation: (x p y) -> (y invp x)',
285
230
  opt: { proofComments: false },
@@ -287,11 +232,8 @@ ${U('b')} ${U('p')} ${U('c')}.
287
232
  { ?x ${U('p')} ?y. } => { ?y ${U('invp')} ?x. }.
288
233
  ${U('alice')} ${U('p')} ${U('bob')}.
289
234
  `,
290
- expect: [
291
- new RegExp(`${EX}bob>\\s+<${EX}invp>\\s+<${EX}alice>\\s*\\.`),
292
- ],
235
+ expect: [new RegExp(`${EX}bob>\\s+<${EX}invp>\\s+<${EX}alice>\\s*\\.`)],
293
236
  },
294
-
295
237
  {
296
238
  name: '05 subclass rule: type + sub -> inferred type (two-level chain)',
297
239
  opt: { proofComments: false },
@@ -307,7 +249,6 @@ ${U('Socrates')} ${U('type')} ${U('Human')}.
307
249
  new RegExp(`${EX}Socrates>\\s+<${EX}type>\\s+<${EX}Being>\\s*\\.`),
308
250
  ],
309
251
  },
310
-
311
252
  {
312
253
  name: '06 transitive closure: sub is transitive',
313
254
  opt: { proofComments: false },
@@ -317,11 +258,8 @@ ${U('B')} ${U('sub')} ${U('C')}.
317
258
 
318
259
  { ?a ${U('sub')} ?b. ?b ${U('sub')} ?c } => { ?a ${U('sub')} ?c }.
319
260
  `,
320
- expect: [
321
- new RegExp(`${EX}A>\\s+<${EX}sub>\\s+<${EX}C>\\s*\\.`),
322
- ],
261
+ expect: [new RegExp(`${EX}A>\\s+<${EX}sub>\\s+<${EX}C>\\s*\\.`)],
323
262
  },
324
-
325
263
  {
326
264
  name: '07 symmetric: knows is symmetric',
327
265
  opt: { proofComments: false },
@@ -329,11 +267,8 @@ ${U('B')} ${U('sub')} ${U('C')}.
329
267
  { ?x ${U('knows')} ?y } => { ?y ${U('knows')} ?x }.
330
268
  ${U('a')} ${U('knows')} ${U('b')}.
331
269
  `,
332
- expect: [
333
- new RegExp(`${EX}b>\\s+<${EX}knows>\\s+<${EX}a>\\s*\\.`),
334
- ],
270
+ expect: [new RegExp(`${EX}b>\\s+<${EX}knows>\\s+<${EX}a>\\s*\\.`)],
335
271
  },
336
-
337
272
  {
338
273
  name: '08 recursion: ancestor from parent (2 steps)',
339
274
  opt: { proofComments: false },
@@ -344,11 +279,8 @@ ${U('b')} ${U('parent')} ${U('c')}.
344
279
  { ?x ${U('parent')} ?y } => { ?x ${U('ancestor')} ?y }.
345
280
  { ?x ${U('parent')} ?y. ?y ${U('ancestor')} ?z } => { ?x ${U('ancestor')} ?z }.
346
281
  `,
347
- expect: [
348
- new RegExp(`${EX}a>\\s+<${EX}ancestor>\\s+<${EX}c>\\s*\\.`),
349
- ],
282
+ expect: [new RegExp(`${EX}a>\\s+<${EX}ancestor>\\s+<${EX}c>\\s*\\.`)],
350
283
  },
351
-
352
284
  {
353
285
  name: '09 literals preserved: age -> hasAge',
354
286
  opt: { proofComments: false },
@@ -356,11 +288,8 @@ ${U('b')} ${U('parent')} ${U('c')}.
356
288
  { ?s ${U('age')} ?n } => { ?s ${U('hasAge')} ?n }.
357
289
  ${U('x')} ${U('age')} "42".
358
290
  `,
359
- expect: [
360
- new RegExp(`${EX}x>\\s+<${EX}hasAge>\\s+"42"\\s*\\.`),
361
- ],
291
+ expect: [new RegExp(`${EX}x>\\s+<${EX}hasAge>\\s+"42"\\s*\\.`)],
362
292
  },
363
-
364
293
  {
365
294
  name: '10 API option: opt can be an args array',
366
295
  opt: ['--no-proof-comments'],
@@ -368,11 +297,9 @@ ${U('x')} ${U('age')} "42".
368
297
  { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }.
369
298
  ${U('s')} ${U('p')} ${U('o')}.
370
299
  `,
371
- expect: [
372
- new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`),
373
- ],
300
+ expect: [new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`)],
301
+ notExpect: [/^#/m],
374
302
  },
375
-
376
303
  {
377
304
  name: '11 negative entailment: rule derives false (expect exit 2 => throws)',
378
305
  opt: { proofComments: false },
@@ -382,12 +309,11 @@ ${U('a')} ${U('p')} ${U('b')}.
382
309
  `,
383
310
  expectErrorCode: 2,
384
311
  },
385
-
386
312
  {
387
313
  name: '12 invalid syntax should throw (non-zero exit)',
388
314
  opt: { proofComments: false },
389
315
  input: `
390
- @prefix : <http://example.org/> # missing dot on purpose
316
+ @prefix : # missing dot on purpose
391
317
  : s :p :o .
392
318
  `,
393
319
  expectError: true,
@@ -401,34 +327,27 @@ ${U('a')} ${U('p')} ${U('b')}.
401
327
  new RegExp(`${EX}n3>\\s+<${EX}ancestor>\\s+<${EX}n12>\\s*\\.`),
402
328
  ],
403
329
  },
404
-
405
330
  {
406
331
  name: '14 heavier taxonomy: 60-step subclass chain',
407
332
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
408
333
  input: subclassChainN3(60),
409
- expect: [
410
- new RegExp(`${EX}x>\\s+<${EX}type>\\s+<${EX}C61>\\s*\\.`),
411
- ],
334
+ expect: [new RegExp(`${EX}x>\\s+<${EX}type>\\s+<${EX}C61>\\s*\\.`)],
412
335
  },
413
-
414
336
  {
415
337
  name: '15 heavier chaining: 40-step predicate rewrite chain',
416
338
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
417
339
  input: ruleChainN3(40),
418
- expect: [
419
- new RegExp(`${EX}s>\\s+<${EX}p40>\\s+<${EX}o>\\s*\\.`),
420
- ],
340
+ expect: [new RegExp(`${EX}s>\\s+<${EX}p40>\\s+<${EX}o>\\s*\\.`)],
421
341
  },
422
342
  {
423
343
  name: '16 heavier recursion: binary tree ancestor closure (depth 4)',
424
344
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
425
- input: binaryTreeParentN3(4), // 31 nodes
345
+ input: binaryTreeParentN3(4),
426
346
  expect: [
427
- new RegExp(`${EX}t0>\\s+<${EX}ancestor>\\s+<${EX}t30>\\s*\\.`), // root reaches last node
347
+ new RegExp(`${EX}t0>\\s+<${EX}ancestor>\\s+<${EX}t30>\\s*\\.`),
428
348
  new RegExp(`${EX}t1>\\s+<${EX}ancestor>\\s+<${EX}t22>\\s*\\.`),
429
349
  ],
430
350
  },
431
-
432
351
  {
433
352
  name: '17 heavier reachability: branching graph reach closure',
434
353
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
@@ -438,16 +357,12 @@ ${U('a')} ${U('p')} ${U('b')}.
438
357
  new RegExp(`${EX}g2>\\s+<${EX}reach>\\s+<${EX}g10>\\s*\\.`),
439
358
  ],
440
359
  },
441
-
442
360
  {
443
361
  name: '18 heavier taxonomy: diamond subclass inference',
444
362
  opt: { proofComments: false },
445
363
  input: diamondSubclassN3(),
446
- expect: [
447
- new RegExp(`${EX}x>\\s+<${EX}type>\\s+<${EX}D>\\s*\\.`),
448
- ],
364
+ expect: [new RegExp(`${EX}x>\\s+<${EX}type>\\s+<${EX}D>\\s*\\.`)],
449
365
  },
450
-
451
366
  {
452
367
  name: '19 heavier join: 3-hop path rule over a chain of 25 edges',
453
368
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
@@ -458,26 +373,21 @@ ${U('a')} ${U('p')} ${U('b')}.
458
373
  new RegExp(`${EX}j20>\\s+<${EX}p3>\\s+<${EX}j23>\\s*\\.`),
459
374
  ],
460
375
  },
461
-
462
376
  {
463
377
  name: '20 heavier branching: p produces q and r, then q+r produces qr',
464
378
  opt: { proofComments: false },
465
379
  input: ruleBranchJoinN3(),
466
- expect: [
467
- new RegExp(`${EX}s>\\s+<${EX}qr>\\s+<${EX}o>\\s*\\.`),
468
- ],
380
+ expect: [new RegExp(`${EX}s>\\s+<${EX}qr>\\s+<${EX}o>\\s*\\.`)],
469
381
  },
470
-
471
382
  {
472
383
  name: '21 heavier equivalence: sameAs propagation (with symmetric sameAs)',
473
384
  opt: { proofComments: false },
474
385
  input: sameAsN3(),
475
386
  expect: [
476
- new RegExp(`${EX}b>\\s+<${EX}p>\\s+<${EX}o>\\s*\\.`), // b inherits a's triple
477
- new RegExp(`${EX}b>\\s+<${EX}sameAs>\\s+<${EX}a>\\s*\\.`), // symmetric sameAs inferred
387
+ new RegExp(`${EX}b>\\s+<${EX}p>\\s+<${EX}o>\\s*\\.`),
388
+ new RegExp(`${EX}b>\\s+<${EX}sameAs>\\s+<${EX}a>\\s*\\.`),
478
389
  ],
479
390
  },
480
-
481
391
  {
482
392
  name: '22 heavier closure: transitive property via generic rule',
483
393
  opt: { proofComments: false },
@@ -493,17 +403,15 @@ ${transitiveClosureN3('sub')}
493
403
  new RegExp(`${EX}b>\\s+<${EX}sub>\\s+<${EX}d>\\s*\\.`),
494
404
  ],
495
405
  },
496
-
497
406
  {
498
407
  name: '23 heavier social: symmetric + reachFriend closure',
499
408
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
500
409
  input: symmetricTransitiveN3(),
501
410
  expect: [
502
411
  new RegExp(`${EX}a>\\s+<${EX}reachFriend>\\s+<${EX}d>\\s*\\.`),
503
- new RegExp(`${EX}d>\\s+<${EX}reachFriend>\\s+<${EX}a>\\s*\\.`), // due to symmetry + closure
412
+ new RegExp(`${EX}d>\\s+<${EX}reachFriend>\\s+<${EX}a>\\s*\\.`),
504
413
  ],
505
414
  },
506
-
507
415
  {
508
416
  name: '24 heavier volume: 400 facts, simple rewrite rule p -> q',
509
417
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
@@ -513,44 +421,207 @@ ${transitiveClosureN3('sub')}
513
421
  new RegExp(`${EX}x>\\s+<${EX}q>\\s+<${EX}o399>\\s*\\.`),
514
422
  ],
515
423
  },
516
-
517
424
  {
518
425
  name: '25 heavier negative entailment: batch + forbidden => false (expect exit 2)',
519
426
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
520
427
  input: negativeEntailmentBatchN3(200),
521
428
  expectErrorCode: 2,
522
429
  },
523
- ];
430
+ {
431
+ name: '26 sanity: no rules => no newly derived facts',
432
+ opt: { proofComments: false },
433
+ input: `
434
+ ${U('a')} ${U('p')} ${U('b')}.
435
+ `,
436
+ expect: [/^\s*$/],
437
+ },
438
+ {
439
+ name: '27 regression: backward rule (<=) can satisfy a forward rule premise',
440
+ opt: { proofComments: false },
441
+ input: `
442
+ ${U('a')} ${U('p')} ${U('b')}.
524
443
 
525
- // ---- Generated “many more” tests (keep them fast & deterministic) ----
526
- const generated = [];
444
+ { ${U('a')} ${U('q')} ${U('b')}. } <= { ${U('a')} ${U('p')} ${U('b')}. }.
445
+ { ${U('a')} ${U('q')} ${U('b')}. } => { ${U('a')} ${U('r')} ${U('b')}. }.
446
+ `,
447
+ expect: [new RegExp(`${EX}a>\\s+<${EX}r>\\s+<${EX}b>\\s*\\.`)],
448
+ },
449
+ {
450
+ name: '28 regression: top-level log:implies behaves like a forward rule',
451
+ opt: { proofComments: false },
452
+ input: `
453
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
527
454
 
528
- // 10 rewrite-chain tests
529
- for (const steps of [1, 2, 3, 5, 8, 10, 12, 15, 20, 25]) {
530
- generated.push(mkChainRewriteCase(100 + generated.length + 1, steps));
531
- }
455
+ { ${U('a')} ${U('p')} ${U('b')}. } log:implies { ${U('a')} ${U('q')} ${U('b')}. }.
456
+ ${U('a')} ${U('p')} ${U('b')}.
457
+ `,
458
+ expect: [new RegExp(`${EX}a>\\s+<${EX}q>\\s+<${EX}b>\\s*\\.`)],
459
+ },
460
+ {
461
+ name: '29 regression: derived log:implies becomes a live rule during reasoning',
462
+ opt: { proofComments: false },
463
+ input: `
464
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
532
465
 
533
- // 8 subclass-chain tests
534
- for (const steps of [1, 2, 3, 5, 8, 10, 15, 25]) {
535
- generated.push(mkSubclassChainCase(100 + generated.length + 1, steps));
536
- }
466
+ { ${U('a')} ${U('trigger')} ${U('go')}. }
467
+ =>
468
+ { { ${U('a')} ${U('p')} ${U('b')}. } log:implies { ${U('a')} ${U('q2')} ${U('b')}. }. }.
537
469
 
538
- // 6 ancestor-chain tests
539
- for (const links of [2, 3, 5, 8, 10, 15]) {
540
- generated.push(mkParentChainCase(100 + generated.length + 1, links));
541
- }
470
+ ${U('a')} ${U('trigger')} ${U('go')}.
471
+ ${U('a')} ${U('p')} ${U('b')}.
472
+ `,
473
+ expect: [new RegExp(`${EX}a>\\s+<${EX}q2>\\s+<${EX}b>\\s*\\.`)],
474
+ },
475
+ {
476
+ name: '30 sanity: proofComments:true enables proof comments',
477
+ opt: { proofComments: true },
478
+ input: `
479
+ { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }.
480
+ ${U('s')} ${U('p')} ${U('o')}.
481
+ `,
482
+ expect: [/^#/m, new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`)],
483
+ },
484
+ {
485
+ name: '31 sanity: -n suppresses proof comments',
486
+ opt: ['-n'],
487
+ input: `
488
+ { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }.
489
+ ${U('s')} ${U('p')} ${U('o')}.
490
+ `,
491
+ expect: [new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`)],
492
+ notExpect: [/^#/m],
493
+ },
542
494
 
543
- // 3 join tests
544
- for (const len of [8, 12, 20]) {
545
- generated.push(mkJoinCase(100 + generated.length + 1, len));
546
- }
495
+ // -------------------------
496
+ // Added sanity/regression tests
497
+ // -------------------------
547
498
 
548
- // 3 reachability tests
549
- for (const n of [6, 10, 14]) {
550
- generated.push(mkBranchReachCase(100 + generated.length + 1, n));
551
- }
499
+ {
500
+ name: '32 sanity: variable rule fires for multiple matching facts',
501
+ opt: { proofComments: false },
502
+ input: `
503
+ ${U('a')} ${U('p')} ${U('b')}.
504
+ ${U('c')} ${U('p')} ${U('d')}.
552
505
 
553
- cases.push(...generated);
506
+ { ?s ${U('p')} ?o. } => { ?s ${U('q')} ?o. }.
507
+ `,
508
+ expect: [
509
+ new RegExp(`${EX}a>\\s+<${EX}q>\\s+<${EX}b>\\s*\\.`),
510
+ new RegExp(`${EX}c>\\s+<${EX}q>\\s+<${EX}d>\\s*\\.`),
511
+ ],
512
+ },
513
+
514
+ {
515
+ name: '33 regression: mutual cycle does not echo already-known facts',
516
+ opt: { proofComments: false },
517
+ input: `
518
+ ${U('s')} ${U('p')} ${U('o')}.
519
+
520
+ { ?x ${U('p')} ?y. } => { ?x ${U('q')} ?y. }.
521
+ { ?x ${U('q')} ?y. } => { ?x ${U('p')} ?y. }.
522
+ `,
523
+ expect: [new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`)],
524
+ notExpect: [new RegExp(`${EX}s>\\s+<${EX}p>\\s+<${EX}o>\\s*\\.`)],
525
+ },
526
+
527
+ {
528
+ name: '34 sanity: rule that reproduces same triple produces no output',
529
+ opt: { proofComments: false },
530
+ input: `
531
+ ${U('s')} ${U('p')} ${U('o')}.
532
+ { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('p')} ${U('o')}. }.
533
+ `,
534
+ expect: [/^\s*$/],
535
+ },
536
+
537
+ {
538
+ name: '35 regression: fuse from derived fact',
539
+ opt: { proofComments: false },
540
+ input: `
541
+ ${U('a')} ${U('p')} ${U('b')}.
542
+
543
+ { ${U('a')} ${U('p')} ${U('b')}. } => { ${U('a')} ${U('q')} ${U('b')}. }.
544
+ { ${U('a')} ${U('q')} ${U('b')}. } => false.
545
+ `,
546
+ expectErrorCode: 2,
547
+ },
548
+
549
+ {
550
+ name: '36 sanity: multiple consequents in one rule',
551
+ opt: { proofComments: false },
552
+ input: `
553
+ ${U('s')} ${U('p')} ${U('o')}.
554
+
555
+ { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. ${U('s')} ${U('r')} ${U('o')}. }.
556
+ `,
557
+ expect: [
558
+ new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`),
559
+ new RegExp(`${EX}s>\\s+<${EX}r>\\s+<${EX}o>\\s*\\.`),
560
+ ],
561
+ },
562
+
563
+ {
564
+ name: '37 regression: backward chaining can chain (<= then <= then =>)',
565
+ opt: { proofComments: false },
566
+ input: `
567
+ ${U('a')} ${U('p')} ${U('b')}.
568
+
569
+ { ${U('a')} ${U('q')} ${U('b')}. } <= { ${U('a')} ${U('p')} ${U('b')}. }.
570
+ { ${U('a')} ${U('r')} ${U('b')}. } <= { ${U('a')} ${U('q')} ${U('b')}. }.
571
+ { ${U('a')} ${U('r')} ${U('b')}. } => { ${U('a')} ${U('s')} ${U('b')}. }.
572
+ `,
573
+ expect: [new RegExp(`${EX}a>\\s+<${EX}s>\\s+<${EX}b>\\s*\\.`)],
574
+ },
575
+
576
+ {
577
+ name: '38 regression: backward rule body can require multiple facts',
578
+ opt: { proofComments: false },
579
+ input: `
580
+ ${U('a')} ${U('p')} ${U('b')}.
581
+ ${U('a')} ${U('p2')} ${U('b')}.
582
+
583
+ { ${U('a')} ${U('q')} ${U('b')}. } <= { ${U('a')} ${U('p')} ${U('b')}. ${U('a')} ${U('p2')} ${U('b')}. }.
584
+ { ${U('a')} ${U('q')} ${U('b')}. } => { ${U('a')} ${U('r')} ${U('b')}. }.
585
+ `,
586
+ expect: [new RegExp(`${EX}a>\\s+<${EX}r>\\s+<${EX}b>\\s*\\.`)],
587
+ },
588
+
589
+ {
590
+ name: '39 sanity: backward rule fails when a required fact is missing',
591
+ opt: { proofComments: false },
592
+ input: `
593
+ ${U('a')} ${U('p')} ${U('b')}.
594
+
595
+ { ${U('a')} ${U('q')} ${U('b')}. } <= { ${U('a')} ${U('p')} ${U('b')}. ${U('a')} ${U('p2')} ${U('b')}. }.
596
+ { ${U('a')} ${U('q')} ${U('b')}. } => { ${U('a')} ${U('r')} ${U('b')}. }.
597
+ `,
598
+ expect: [/^\s*$/],
599
+ },
600
+
601
+ {
602
+ name: '40 sanity: comments and whitespace are tolerated',
603
+ opt: { proofComments: false },
604
+ input: `
605
+ # leading comment
606
+ { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }. # trailing comment
607
+
608
+ ${U('s')} ${U('p')} ${U('o')}. # another trailing comment
609
+ `,
610
+ expect: [new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`)],
611
+ },
612
+
613
+ {
614
+ name: '41 stability: diamond subclass derives D only once',
615
+ opt: { proofComments: false },
616
+ input: diamondSubclassN3(),
617
+ expect: [new RegExp(`${EX}x>\\s+<${EX}type>\\s+<${EX}D>\\s*\\.`)],
618
+ // and ensure it doesn't print the same derived triple twice via the two paths
619
+ check(out) {
620
+ const reD = new RegExp(`${EX}x>\\s+<${EX}type>\\s+<${EX}D>\\s*\\.`, 'm');
621
+ mustOccurExactly(out, reD, 1, 'diamond subclass should not duplicate x type D');
622
+ },
623
+ },
624
+ ];
554
625
 
555
626
  let passed = 0;
556
627
  let failed = 0;
@@ -568,8 +639,10 @@ let failed = 0;
568
639
  throw new Error(`Expected an error, but reason() returned output:\n${out}`);
569
640
  }
570
641
 
571
- for (const re of (tc.expect || [])) mustMatch(out, re, `${tc.name}: missing expected pattern ${re}`);
572
- for (const re of (tc.notExpect || [])) mustNotMatch(out, re, `${tc.name}: unexpected pattern ${re}`);
642
+ for (const re of tc.expect || []) mustMatch(out, re, `${tc.name}: missing expected pattern ${re}`);
643
+ for (const re of tc.notExpect || []) mustNotMatch(out, re, `${tc.name}: unexpected pattern ${re}`);
644
+
645
+ if (typeof tc.check === 'function') tc.check(out);
573
646
 
574
647
  const dur = msNow() - start;
575
648
  ok(`${tc.name} ${C.dim}(${dur} ms)${C.n}`);
@@ -577,7 +650,6 @@ let failed = 0;
577
650
  } catch (e) {
578
651
  const dur = msNow() - start;
579
652
 
580
- // Expected error handling
581
653
  if (tc.expectErrorCode != null) {
582
654
  if (e && typeof e === 'object' && 'code' in e && e.code === tc.expectErrorCode) {
583
655
  ok(`${tc.name} ${C.dim}(expected exit ${tc.expectErrorCode}, ${dur} ms)${C.n}`);
@@ -585,7 +657,11 @@ let failed = 0;
585
657
  continue;
586
658
  }
587
659
  fail(`${tc.name} ${C.dim}(${dur} ms)${C.n}`);
588
- fail(`Expected exit code ${tc.expectErrorCode}, got: ${e && e.code != null ? e.code : 'unknown'}\n${e && e.stderr ? e.stderr : (e && e.stack ? e.stack : String(e))}`);
660
+ fail(
661
+ `Expected exit code ${tc.expectErrorCode}, got: ${e && e.code != null ? e.code : 'unknown'}\n${
662
+ e && e.stderr ? e.stderr : e && e.stack ? e.stack : String(e)
663
+ }`
664
+ );
589
665
  failed++;
590
666
  continue;
591
667
  }