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 +4 -2
- package/test/api.test.js +603 -12
- package/test/package-smoke.sh +55 -0
- package/test/packlist.test.js +63 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyeling",
|
|
3
|
-
"version": "1.5.
|
|
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": "
|
|
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('..');
|
|
4
|
+
const { reason } = require('..');
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
592
|
+
if (tc.expectError) {
|
|
593
|
+
ok(`${tc.name} ${C.dim}(expected error, ${dur} ms)${C.n}`);
|
|
594
|
+
passed++;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
14
597
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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('
|
|
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
|
+
|