eyeling 1.5.13 → 1.5.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.5.13",
3
+ "version": "1.5.15",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -29,8 +29,10 @@
29
29
  "node": ">=18"
30
30
  },
31
31
  "scripts": {
32
+ "test:packlist": "node test/packlist.test.js",
32
33
  "test:api": "node test/api.test.js",
33
34
  "test:examples": "bash -lc 'cd examples && ./test'",
34
- "test": "npm run test:api && npm run test:examples"
35
+ "test:package": "bash test/package-smoke.sh",
36
+ "test": "npm run test:packlist && npm run test:api && npm run test:examples"
35
37
  }
36
38
  }
package/test/api.test.js CHANGED
@@ -1,22 +1,613 @@
1
1
  'use strict';
2
2
 
3
3
  const assert = require('node:assert/strict');
4
- const { reason } = require('..'); // gebruikt package main: ./index.js
4
+ const { reason } = require('..');
5
5
 
6
- const input = `
7
- { <http://example.org/s> <http://example.org/p> <http://example.org/o>. }
8
- => { <http://example.org/s> <http://example.org/q> <http://example.org/o>. }.
6
+ const TTY = process.stdout.isTTY;
7
+ const C = TTY
8
+ ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
9
+ : { g: '', r: '', y: '', dim: '', n: '' };
9
10
 
10
- <http://example.org/s> <http://example.org/p> <http://example.org/o>.
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}`); }
14
+
15
+ function msNow() {
16
+ return Date.now();
17
+ }
18
+
19
+ function mustMatch(output, re, label) {
20
+ assert.match(output, re, label || `Expected output to match ${re}`);
21
+ }
22
+
23
+ function mustNotMatch(output, re, label) {
24
+ assert.ok(!re.test(output), label || `Expected output NOT to match ${re}`);
25
+ }
26
+
27
+ const EX = 'http://example.org/';
28
+
29
+ // Helper to build a URI quickly
30
+ const U = (path) => `<${EX}${path}>`;
31
+
32
+ function parentChainN3(n) {
33
+ // n links => n+1 nodes: n0->n1->...->nN
34
+ let s = '';
35
+ for (let i = 0; i < n; i++) {
36
+ s += `${U(`n${i}`)} ${U('parent')} ${U(`n${i + 1}`)}.\n`;
37
+ }
38
+ s += `
39
+ { ?x ${U('parent')} ?y } => { ?x ${U('ancestor')} ?y }.
40
+ { ?x ${U('parent')} ?y. ?y ${U('ancestor')} ?z } => { ?x ${U('ancestor')} ?z }.
41
+ `;
42
+ return s;
43
+ }
44
+
45
+ function subclassChainN3(n) {
46
+ // C0 sub C1 ... Cn sub C(n+1)
47
+ let s = '';
48
+ for (let i = 0; i <= n; i++) {
49
+ s += `${U(`C${i}`)} ${U('sub')} ${U(`C${i + 1}`)}.\n`;
50
+ }
51
+ s += `${U('x')} ${U('type')} ${U('C0')}.\n`;
52
+ s += `{ ?s ${U('type')} ?a. ?a ${U('sub')} ?b } => { ?s ${U('type')} ?b }.\n`;
53
+ return s;
54
+ }
55
+
56
+ function ruleChainN3(n) {
57
+ // p0 -> p1 -> ... -> pn, starting from (s p0 o)
58
+ let s = '';
59
+ for (let i = 0; i < n; i++) {
60
+ s += `{ ${U('s')} ${U(`p${i}`)} ${U('o')}. } => { ${U('s')} ${U(`p${i + 1}`)} ${U('o')}. }.\n`;
61
+ }
62
+ s += `${U('s')} ${U('p0')} ${U('o')}.\n`;
63
+ return s;
64
+ }
65
+
66
+ 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
70
+ let s = '';
71
+
72
+ for (let i = 0; i <= maxNode; i++) {
73
+ const left = 2 * i + 1;
74
+ const right = 2 * i + 2;
75
+ if (left <= maxNode) s += `${U(`t${i}`)} ${U('parent')} ${U(`t${left}`)}.\n`;
76
+ if (right <= maxNode) s += `${U(`t${i}`)} ${U('parent')} ${U(`t${right}`)}.\n`;
77
+ }
78
+
79
+ s += `
80
+ { ?x ${U('parent')} ?y } => { ?x ${U('ancestor')} ?y }.
81
+ { ?x ${U('parent')} ?y. ?y ${U('ancestor')} ?z } => { ?x ${U('ancestor')} ?z }.
82
+ `;
83
+ return s;
84
+ }
85
+
86
+ function transitiveClosureN3(pred) {
87
+ return `
88
+ { ?a ${U(pred)} ?b. ?b ${U(pred)} ?c } => { ?a ${U(pred)} ?c }.
89
+ `;
90
+ }
91
+
92
+ function reachabilityGraphN3(n) {
93
+ // chain plus a few extra edges for branching
94
+ let s = '';
95
+ for (let i = 0; i < n; i++) {
96
+ s += `${U(`g${i}`)} ${U('edge')} ${U(`g${i + 1}`)}.\n`;
97
+ }
98
+ // add some shortcuts/branches
99
+ if (n >= 6) {
100
+ s += `${U('g0')} ${U('edge')} ${U('g3')}.\n`;
101
+ s += `${U('g2')} ${U('edge')} ${U('g5')}.\n`;
102
+ s += `${U('g1')} ${U('edge')} ${U('g4')}.\n`;
103
+ }
104
+ s += `
105
+ { ?a ${U('edge')} ?b } => { ?a ${U('reach')} ?b }.
106
+ { ?a ${U('edge')} ?b. ?b ${U('reach')} ?c } => { ?a ${U('reach')} ?c }.
11
107
  `;
108
+ return s;
109
+ }
110
+
111
+ function diamondSubclassN3() {
112
+ return `
113
+ ${U('A')} ${U('sub')} ${U('B')}.
114
+ ${U('A')} ${U('sub')} ${U('C')}.
115
+ ${U('B')} ${U('sub')} ${U('D')}.
116
+ ${U('C')} ${U('sub')} ${U('D')}.
117
+ ${U('x')} ${U('type')} ${U('A')}.
118
+
119
+ { ?s ${U('type')} ?a. ?a ${U('sub')} ?b } => { ?s ${U('type')} ?b }.
120
+ `;
121
+ }
122
+
123
+ function join3HopN3(k) {
124
+ // a0 --p--> a1 --p--> a2 --p--> ... ; rule derives hop3 edges
125
+ let s = '';
126
+ for (let i = 0; i < k; i++) {
127
+ s += `${U(`j${i}`)} ${U('p')} ${U(`j${i + 1}`)}.\n`;
128
+ }
129
+ s += `
130
+ { ?x ${U('p')} ?y. ?y ${U('p')} ?z. ?z ${U('p')} ?w } => { ?x ${U('p3')} ?w }.
131
+ `;
132
+ return s;
133
+ }
134
+
135
+ function sameAsN3() {
136
+ return `
137
+ ${U('a')} ${U('sameAs')} ${U('b')}.
138
+ ${U('a')} ${U('p')} ${U('o')}.
139
+
140
+ { ?x ${U('sameAs')} ?y } => { ?y ${U('sameAs')} ?x }.
141
+ { ?x ${U('sameAs')} ?y. ?x ?p ?o } => { ?y ?p ?o }.
142
+ `;
143
+ }
144
+
145
+ function ruleBranchJoinN3() {
146
+ return `
147
+ ${U('s')} ${U('p')} ${U('o')}.
148
+
149
+ { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }.
150
+ { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('r')} ${U('o')}. }.
151
+ { ${U('s')} ${U('q')} ${U('o')}. ${U('s')} ${U('r')} ${U('o')}. } => { ${U('s')} ${U('qr')} ${U('o')}. }.
152
+ `;
153
+ }
154
+
155
+ function bigFactsN3(n) {
156
+ let s = '';
157
+ for (let i = 0; i < n; i++) {
158
+ s += `${U('x')} ${U('p')} ${U(`o${i}`)}.\n`;
159
+ }
160
+ s += `{ ?s ${U('p')} ?o } => { ?s ${U('q')} ?o }.\n`;
161
+ return s;
162
+ }
163
+
164
+ function negativeEntailmentBatchN3(n) {
165
+ // if any forbidden fact exists, derive false
166
+ let s = '';
167
+ for (let i = 0; i < n; i++) {
168
+ s += `${U('x')} ${U('ok')} ${U(`v${i}`)}.\n`;
169
+ }
170
+ s += `${U('x')} ${U('forbidden')} ${U('boom')}.\n`;
171
+ s += `{ ?s ${U('forbidden')} ?o. } => false.\n`;
172
+ return s;
173
+ }
174
+
175
+ function symmetricTransitiveN3() {
176
+ // friend is symmetric; reachFriend is transitive closure over friend edges
177
+ return `
178
+ ${U('a')} ${U('friend')} ${U('b')}.
179
+ ${U('b')} ${U('friend')} ${U('c')}.
180
+ ${U('c')} ${U('friend')} ${U('d')}.
181
+
182
+ { ?x ${U('friend')} ?y } => { ?y ${U('friend')} ?x }.
183
+ { ?a ${U('friend')} ?b } => { ?a ${U('reachFriend')} ?b }.
184
+ { ?a ${U('friend')} ?b. ?b ${U('reachFriend')} ?c } => { ?a ${U('reachFriend')} ?c }.
185
+ `;
186
+ }
187
+
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
+ const cases = [
245
+ {
246
+ name: '01 forward rule: p -> q',
247
+ opt: { proofComments: false },
248
+ input: `
249
+ { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }.
250
+ ${U('s')} ${U('p')} ${U('o')}.
251
+ `,
252
+ expect: [
253
+ new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`),
254
+ ],
255
+ },
256
+
257
+ {
258
+ name: '02 two-step: p -> q -> r',
259
+ opt: { proofComments: false },
260
+ input: `
261
+ { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }.
262
+ { ${U('s')} ${U('q')} ${U('o')}. } => { ${U('s')} ${U('r')} ${U('o')}. }.
263
+ ${U('s')} ${U('p')} ${U('o')}.
264
+ `,
265
+ expect: [
266
+ new RegExp(`${EX}s>\\s+<${EX}r>\\s+<${EX}o>\\s*\\.`),
267
+ ],
268
+ },
269
+
270
+ {
271
+ name: '03 join antecedents: (x p y & y p z) -> (x p2 z)',
272
+ opt: { proofComments: false },
273
+ input: `
274
+ { ?x ${U('p')} ?y. ?y ${U('p')} ?z. } => { ?x ${U('p2')} ?z. }.
275
+ ${U('a')} ${U('p')} ${U('b')}.
276
+ ${U('b')} ${U('p')} ${U('c')}.
277
+ `,
278
+ expect: [
279
+ new RegExp(`${EX}a>\\s+<${EX}p2>\\s+<${EX}c>\\s*\\.`),
280
+ ],
281
+ },
282
+
283
+ {
284
+ name: '04 inverse relation: (x p y) -> (y invp x)',
285
+ opt: { proofComments: false },
286
+ input: `
287
+ { ?x ${U('p')} ?y. } => { ?y ${U('invp')} ?x. }.
288
+ ${U('alice')} ${U('p')} ${U('bob')}.
289
+ `,
290
+ expect: [
291
+ new RegExp(`${EX}bob>\\s+<${EX}invp>\\s+<${EX}alice>\\s*\\.`),
292
+ ],
293
+ },
294
+
295
+ {
296
+ name: '05 subclass rule: type + sub -> inferred type (two-level chain)',
297
+ opt: { proofComments: false },
298
+ input: `
299
+ ${U('Human')} ${U('sub')} ${U('Mortal')}.
300
+ ${U('Mortal')} ${U('sub')} ${U('Being')}.
301
+ ${U('Socrates')} ${U('type')} ${U('Human')}.
302
+
303
+ { ?s ${U('type')} ?a. ?a ${U('sub')} ?b } => { ?s ${U('type')} ?b }.
304
+ `,
305
+ expect: [
306
+ new RegExp(`${EX}Socrates>\\s+<${EX}type>\\s+<${EX}Mortal>\\s*\\.`),
307
+ new RegExp(`${EX}Socrates>\\s+<${EX}type>\\s+<${EX}Being>\\s*\\.`),
308
+ ],
309
+ },
310
+
311
+ {
312
+ name: '06 transitive closure: sub is transitive',
313
+ opt: { proofComments: false },
314
+ input: `
315
+ ${U('A')} ${U('sub')} ${U('B')}.
316
+ ${U('B')} ${U('sub')} ${U('C')}.
317
+
318
+ { ?a ${U('sub')} ?b. ?b ${U('sub')} ?c } => { ?a ${U('sub')} ?c }.
319
+ `,
320
+ expect: [
321
+ new RegExp(`${EX}A>\\s+<${EX}sub>\\s+<${EX}C>\\s*\\.`),
322
+ ],
323
+ },
324
+
325
+ {
326
+ name: '07 symmetric: knows is symmetric',
327
+ opt: { proofComments: false },
328
+ input: `
329
+ { ?x ${U('knows')} ?y } => { ?y ${U('knows')} ?x }.
330
+ ${U('a')} ${U('knows')} ${U('b')}.
331
+ `,
332
+ expect: [
333
+ new RegExp(`${EX}b>\\s+<${EX}knows>\\s+<${EX}a>\\s*\\.`),
334
+ ],
335
+ },
336
+
337
+ {
338
+ name: '08 recursion: ancestor from parent (2 steps)',
339
+ opt: { proofComments: false },
340
+ input: `
341
+ ${U('a')} ${U('parent')} ${U('b')}.
342
+ ${U('b')} ${U('parent')} ${U('c')}.
343
+
344
+ { ?x ${U('parent')} ?y } => { ?x ${U('ancestor')} ?y }.
345
+ { ?x ${U('parent')} ?y. ?y ${U('ancestor')} ?z } => { ?x ${U('ancestor')} ?z }.
346
+ `,
347
+ expect: [
348
+ new RegExp(`${EX}a>\\s+<${EX}ancestor>\\s+<${EX}c>\\s*\\.`),
349
+ ],
350
+ },
351
+
352
+ {
353
+ name: '09 literals preserved: age -> hasAge',
354
+ opt: { proofComments: false },
355
+ input: `
356
+ { ?s ${U('age')} ?n } => { ?s ${U('hasAge')} ?n }.
357
+ ${U('x')} ${U('age')} "42".
358
+ `,
359
+ expect: [
360
+ new RegExp(`${EX}x>\\s+<${EX}hasAge>\\s+"42"\\s*\\.`),
361
+ ],
362
+ },
363
+
364
+ {
365
+ name: '10 API option: opt can be an args array',
366
+ opt: ['--no-proof-comments'],
367
+ input: `
368
+ { ${U('s')} ${U('p')} ${U('o')}. } => { ${U('s')} ${U('q')} ${U('o')}. }.
369
+ ${U('s')} ${U('p')} ${U('o')}.
370
+ `,
371
+ expect: [
372
+ new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}o>\\s*\\.`),
373
+ ],
374
+ },
375
+
376
+ {
377
+ name: '11 negative entailment: rule derives false (expect exit 2 => throws)',
378
+ opt: { proofComments: false },
379
+ input: `
380
+ { ${U('a')} ${U('p')} ${U('b')}. } => false.
381
+ ${U('a')} ${U('p')} ${U('b')}.
382
+ `,
383
+ expectErrorCode: 2,
384
+ },
385
+
386
+ {
387
+ name: '12 invalid syntax should throw (non-zero exit)',
388
+ opt: { proofComments: false },
389
+ input: `
390
+ @prefix : <http://example.org/> # missing dot on purpose
391
+ : s :p :o .
392
+ `,
393
+ expectError: true,
394
+ },
395
+ {
396
+ name: '13 heavier recursion: ancestor closure over 15 links',
397
+ opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
398
+ input: parentChainN3(15),
399
+ expect: [
400
+ new RegExp(`${EX}n0>\\s+<${EX}ancestor>\\s+<${EX}n15>\\s*\\.`),
401
+ new RegExp(`${EX}n3>\\s+<${EX}ancestor>\\s+<${EX}n12>\\s*\\.`),
402
+ ],
403
+ },
404
+
405
+ {
406
+ name: '14 heavier taxonomy: 60-step subclass chain',
407
+ opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
408
+ input: subclassChainN3(60),
409
+ expect: [
410
+ new RegExp(`${EX}x>\\s+<${EX}type>\\s+<${EX}C61>\\s*\\.`),
411
+ ],
412
+ },
413
+
414
+ {
415
+ name: '15 heavier chaining: 40-step predicate rewrite chain',
416
+ opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
417
+ input: ruleChainN3(40),
418
+ expect: [
419
+ new RegExp(`${EX}s>\\s+<${EX}p40>\\s+<${EX}o>\\s*\\.`),
420
+ ],
421
+ },
422
+ {
423
+ name: '16 heavier recursion: binary tree ancestor closure (depth 4)',
424
+ opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
425
+ input: binaryTreeParentN3(4), // 31 nodes
426
+ expect: [
427
+ new RegExp(`${EX}t0>\\s+<${EX}ancestor>\\s+<${EX}t30>\\s*\\.`), // root reaches last node
428
+ new RegExp(`${EX}t1>\\s+<${EX}ancestor>\\s+<${EX}t22>\\s*\\.`),
429
+ ],
430
+ },
431
+
432
+ {
433
+ name: '17 heavier reachability: branching graph reach closure',
434
+ opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
435
+ input: reachabilityGraphN3(12),
436
+ expect: [
437
+ new RegExp(`${EX}g0>\\s+<${EX}reach>\\s+<${EX}g12>\\s*\\.`),
438
+ new RegExp(`${EX}g2>\\s+<${EX}reach>\\s+<${EX}g10>\\s*\\.`),
439
+ ],
440
+ },
441
+
442
+ {
443
+ name: '18 heavier taxonomy: diamond subclass inference',
444
+ opt: { proofComments: false },
445
+ input: diamondSubclassN3(),
446
+ expect: [
447
+ new RegExp(`${EX}x>\\s+<${EX}type>\\s+<${EX}D>\\s*\\.`),
448
+ ],
449
+ },
450
+
451
+ {
452
+ name: '19 heavier join: 3-hop path rule over a chain of 25 edges',
453
+ opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
454
+ input: join3HopN3(25),
455
+ expect: [
456
+ new RegExp(`${EX}j0>\\s+<${EX}p3>\\s+<${EX}j3>\\s*\\.`),
457
+ new RegExp(`${EX}j10>\\s+<${EX}p3>\\s+<${EX}j13>\\s*\\.`),
458
+ new RegExp(`${EX}j20>\\s+<${EX}p3>\\s+<${EX}j23>\\s*\\.`),
459
+ ],
460
+ },
461
+
462
+ {
463
+ name: '20 heavier branching: p produces q and r, then q+r produces qr',
464
+ opt: { proofComments: false },
465
+ input: ruleBranchJoinN3(),
466
+ expect: [
467
+ new RegExp(`${EX}s>\\s+<${EX}qr>\\s+<${EX}o>\\s*\\.`),
468
+ ],
469
+ },
470
+
471
+ {
472
+ name: '21 heavier equivalence: sameAs propagation (with symmetric sameAs)',
473
+ opt: { proofComments: false },
474
+ input: sameAsN3(),
475
+ 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
478
+ ],
479
+ },
480
+
481
+ {
482
+ name: '22 heavier closure: transitive property via generic rule',
483
+ opt: { proofComments: false },
484
+ input: `
485
+ ${U('a')} ${U('sub')} ${U('b')}.
486
+ ${U('b')} ${U('sub')} ${U('c')}.
487
+ ${U('c')} ${U('sub')} ${U('d')}.
488
+ ${U('d')} ${U('sub')} ${U('e')}.
489
+ ${transitiveClosureN3('sub')}
490
+ `,
491
+ expect: [
492
+ new RegExp(`${EX}a>\\s+<${EX}sub>\\s+<${EX}e>\\s*\\.`),
493
+ new RegExp(`${EX}b>\\s+<${EX}sub>\\s+<${EX}d>\\s*\\.`),
494
+ ],
495
+ },
496
+
497
+ {
498
+ name: '23 heavier social: symmetric + reachFriend closure',
499
+ opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
500
+ input: symmetricTransitiveN3(),
501
+ expect: [
502
+ 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
504
+ ],
505
+ },
506
+
507
+ {
508
+ name: '24 heavier volume: 400 facts, simple rewrite rule p -> q',
509
+ opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
510
+ input: bigFactsN3(400),
511
+ expect: [
512
+ new RegExp(`${EX}x>\\s+<${EX}q>\\s+<${EX}o0>\\s*\\.`),
513
+ new RegExp(`${EX}x>\\s+<${EX}q>\\s+<${EX}o399>\\s*\\.`),
514
+ ],
515
+ },
516
+
517
+ {
518
+ name: '25 heavier negative entailment: batch + forbidden => false (expect exit 2)',
519
+ opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
520
+ input: negativeEntailmentBatchN3(200),
521
+ expectErrorCode: 2,
522
+ },
523
+ ];
524
+
525
+ // ---- Generated “many more” tests (keep them fast & deterministic) ----
526
+ const generated = [];
527
+
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
+ }
532
+
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
+ }
537
+
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
+ }
542
+
543
+ // 3 join tests
544
+ for (const len of [8, 12, 20]) {
545
+ generated.push(mkJoinCase(100 + generated.length + 1, len));
546
+ }
547
+
548
+ // 3 reachability tests
549
+ for (const n of [6, 10, 14]) {
550
+ generated.push(mkBranchReachCase(100 + generated.length + 1, n));
551
+ }
552
+
553
+ cases.push(...generated);
554
+
555
+ let passed = 0;
556
+ let failed = 0;
557
+
558
+ (async function main() {
559
+ info(`Running ${cases.length} API tests (independent of examples/)`);
560
+
561
+ for (const tc of cases) {
562
+ const start = msNow();
563
+ try {
564
+ const out = reason(tc.opt, tc.input);
565
+
566
+ if (tc.expectErrorCode != null || tc.expectError) {
567
+ throw new Error(`Expected an error, but reason() returned output:\n${out}`);
568
+ }
569
+
570
+ for (const re of (tc.expect || [])) mustMatch(out, re, `${tc.name}: missing expected pattern ${re}`);
571
+ for (const re of (tc.notExpect || [])) mustNotMatch(out, re, `${tc.name}: unexpected pattern ${re}`);
572
+
573
+ const dur = msNow() - start;
574
+ ok(`${tc.name} ${C.dim}(${dur} ms)${C.n}`);
575
+ passed++;
576
+ } catch (e) {
577
+ const dur = msNow() - start;
578
+
579
+ // Expected error handling
580
+ if (tc.expectErrorCode != null) {
581
+ if (e && typeof e === 'object' && 'code' in e && e.code === tc.expectErrorCode) {
582
+ ok(`${tc.name} ${C.dim}(expected exit ${tc.expectErrorCode}, ${dur} ms)${C.n}`);
583
+ passed++;
584
+ continue;
585
+ }
586
+ fail(`${tc.name} ${C.dim}(${dur} ms)${C.n}`);
587
+ 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))}`);
588
+ failed++;
589
+ continue;
590
+ }
12
591
 
13
- const out = reason({ proofComments: false }, input);
592
+ if (tc.expectError) {
593
+ ok(`${tc.name} ${C.dim}(expected error, ${dur} ms)${C.n}`);
594
+ passed++;
595
+ continue;
596
+ }
14
597
 
15
- // Verwacht dat de afgeleide triple aanwezig is (tolerant voor formatting)
16
- assert.match(
17
- out,
18
- /<http:\/\/example\.org\/s>\s+<http:\/\/example\.org\/q>\s+<http:\/\/example\.org\/o>\s*\./
19
- );
598
+ fail(`${tc.name} ${C.dim}(${dur} ms)${C.n}`);
599
+ fail(e && e.stack ? e.stack : String(e));
600
+ failed++;
601
+ }
602
+ }
20
603
 
21
- console.log('OK: reason() inferred expected triple');
604
+ console.log('');
605
+ if (failed === 0) {
606
+ ok(`All API tests passed (${passed}/${cases.length})`);
607
+ process.exit(0);
608
+ } else {
609
+ fail(`Some API tests failed (${passed}/${cases.length})`);
610
+ process.exit(1);
611
+ }
612
+ })();
22
613
 
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [ -t 1 ]; then
5
+ RED=$'\e[31m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; NORMAL=$'\e[0m'
6
+ else
7
+ RED=""; GREEN=""; YELLOW=""; NORMAL=""
8
+ fi
9
+
10
+ say() { echo -e "${YELLOW}== $* ==${NORMAL}"; }
11
+ ok() { echo -e "${GREEN}OK${NORMAL} $*"; }
12
+
13
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
14
+ TMP="$(mktemp -d)"
15
+ cleanup() { rm -rf "$TMP"; }
16
+ trap cleanup EXIT
17
+
18
+ cd "$ROOT"
19
+
20
+ say "Building tarball"
21
+ TGZ="$(npm pack --silent)"
22
+ mv "$TGZ" "$TMP/"
23
+ cd "$TMP"
24
+
25
+ say "Installing tarball into temp project"
26
+ npm init -y >/dev/null 2>&1
27
+ npm install "./$TGZ" >/dev/null 2>&1
28
+
29
+ say "API smoke test"
30
+ node - <<'NODE'
31
+ const { reason } = require('eyeling');
32
+ const input = `
33
+ { <http://example.org/s> <http://example.org/p> <http://example.org/o>. }
34
+ => { <http://example.org/s> <http://example.org/q> <http://example.org/o>. }.
35
+
36
+ <http://example.org/s> <http://example.org/p> <http://example.org/o>.
37
+ `;
38
+ const out = reason({ proofComments: false }, input);
39
+ if (!/<http:\/\/example\.org\/s>\s+<http:\/\/example\.org\/q>\s+<http:\/\/example\.org\/o>\s*\./.test(out)) {
40
+ console.error("Unexpected output:\n" + out);
41
+ process.exit(1);
42
+ }
43
+ NODE
44
+ ok "API works"
45
+
46
+ say "CLI smoke test"
47
+ ./node_modules/.bin/eyeling -v
48
+ ok "CLI works"
49
+
50
+ say "Examples test (installed package)"
51
+ cd node_modules/eyeling/examples
52
+ ./test
53
+
54
+ ok "packaged install smoke test passed"
55
+
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const cp = require('node:child_process');
5
+ const fs = require('node:fs');
6
+
7
+ const TTY = process.stdout.isTTY;
8
+ const C = TTY
9
+ ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', n: '\x1b[0m' }
10
+ : { g: '', r: '', y: '', n: '' };
11
+
12
+ function ok(msg) { console.log(`${C.g}OK${C.n} ${msg}`); }
13
+ function info(msg) { console.log(`${C.y}${msg}${C.n}`); }
14
+ function fail(msg) { console.error(`${C.r}FAIL${C.n} ${msg}`); }
15
+
16
+ try {
17
+ info('Checking packlist + metadata…');
18
+
19
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
20
+
21
+ assert.ok(pkg.name, 'package.json: name missing');
22
+ assert.ok(pkg.version, 'package.json: version missing');
23
+ assert.equal(pkg.main, './index.js', 'package.json: main should be ./index.js');
24
+ assert.ok(pkg.bin && pkg.bin.eyeling, 'package.json: bin.eyeling missing');
25
+
26
+ assert.ok(fs.existsSync('eyeling.js'), 'eyeling.js missing');
27
+ assert.ok(fs.existsSync('index.js'), 'index.js missing');
28
+
29
+ const firstLine = fs.readFileSync('eyeling.js', 'utf8').split(/\r?\n/, 1)[0];
30
+ assert.match(firstLine, /^#!\/usr\/bin\/env node\b/, 'eyeling.js should start with "#!/usr/bin/env node"');
31
+
32
+ let packJson;
33
+ try {
34
+ packJson = cp.execSync('npm pack --dry-run --json', { encoding: 'utf8' });
35
+ } catch (e) {
36
+ throw new Error('npm pack --dry-run --json failed\n' + (e.stderr || e.message));
37
+ }
38
+
39
+ const pack = JSON.parse(packJson)[0];
40
+ const paths = new Set(pack.files.map(f => f.path));
41
+
42
+ const mustHave = [
43
+ 'package.json',
44
+ 'README.md',
45
+ 'LICENSE.md',
46
+ 'eyeling.js',
47
+ 'index.js',
48
+ 'examples/test',
49
+ ];
50
+
51
+ for (const p of mustHave) assert.ok(paths.has(p), `missing from npm pack: ${p}`);
52
+
53
+ assert.ok(
54
+ [...paths].some(p => p.startsWith('examples/output/')),
55
+ 'missing from npm pack: examples/output/*'
56
+ );
57
+
58
+ ok('packlist + metadata sanity checks passed');
59
+ } catch (e) {
60
+ fail(e && e.stack ? e.stack : String(e));
61
+ process.exit(1);
62
+ }
63
+