eyeling 1.5.14 → 1.5.16
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/README.md +84 -63
- package/package.json +3 -2
- package/test/api.test.js +603 -12
- package/test/package-smoke.sh +55 -0
- package/test/packlist.test.js +63 -0
package/README.md
CHANGED
|
@@ -4,12 +4,13 @@ A minimal [Notation3 (N3)](https://notation3.org/) reasoner in **JavaScript**.
|
|
|
4
4
|
|
|
5
5
|
`eyeling` is:
|
|
6
6
|
|
|
7
|
-
- a single self-contained file (`eyeling.js`, no external deps)
|
|
8
|
-
- intentionally tiny and close in spirit to EYE
|
|
9
|
-
- a practical N3/Turtle superset (enough for lots of real rulesets)
|
|
10
|
-
- supports forward (`=>`) + backward (`<=`) chaining over Horn-style rules
|
|
11
|
-
- prints only newly derived forward facts, optionally preceded by compact proof comments
|
|
12
|
-
- we never want to leak raw data, hence pass-only-new and backward rules for functions that work with raw data
|
|
7
|
+
- a single self-contained file (`eyeling.js`, no external deps)
|
|
8
|
+
- intentionally tiny and close in spirit to EYE
|
|
9
|
+
- a practical N3/Turtle superset (enough for lots of real rulesets)
|
|
10
|
+
- supports forward (`=>`) + backward (`<=`) chaining over Horn-style rules
|
|
11
|
+
- prints only newly derived forward facts, optionally preceded by compact proof comments
|
|
12
|
+
- we never want to leak raw data, hence pass-only-new and backward rules for functions that work with raw data
|
|
13
|
+
- and of course we also keep all reasoning in the browser
|
|
13
14
|
|
|
14
15
|
## Playground (in your browser)
|
|
15
16
|
|
|
@@ -18,21 +19,22 @@ Try it here:
|
|
|
18
19
|
- [Eyeling playground](https://eyereasoner.github.io/eyeling/demo)
|
|
19
20
|
|
|
20
21
|
The playground runs `eyeling` client-side. You can:
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
22
|
+
|
|
23
|
+
- edit an N3 program directly
|
|
24
|
+
- load an N3 program from a URL
|
|
25
|
+
- share a link with the program encoded in the URL fragment (`#...`)
|
|
24
26
|
|
|
25
27
|
### Example (Socrates)
|
|
26
28
|
|
|
27
29
|
This link preloads a small “Socrates is Mortal” ruleset:
|
|
28
30
|
|
|
29
|
-
[Socrates example](https://eyereasoner.github.io/eyeling/demo#%23%20------------------%0A%23%20Socrates%20inference%0A%23%20------------------%0A%0A%40prefix%20rdfs%3A%20%3Chttp%3A%2F%2Fwww.w3.org%2F2000%2F01%2Frdf-schema%23%3E.%0A%40prefix%20%3A%20%3Chttp%3A%2F%2Fexample.org%2Fsocrates%23%3E.%0A%0A%23%20facts%0A%3ASocrates%20a%20%3AHuman.%0A%3AHuman%20rdfs%3AsubClassOf%20%3AMortal.%0A%0A%23%20subclass%20rule%0A%7B%0A%20%20%20%20%3FS%20a%20%3FA.%0A%20%20%20%20%3FA%20rdfs%3AsubClassOf%20%3FB.%0A%7D%20%3D%3E%20%7B%0A%20%20%20%20%3FS%20a%20%3FB.%0A%7D%2E%0A)
|
|
31
|
+
- [Socrates example](https://eyereasoner.github.io/eyeling/demo#%23%20------------------%0A%23%20Socrates%20inference%0A%23%20------------------%0A%0A%40prefix%20rdfs%3A%20%3Chttp%3A%2F%2Fwww.w3.org%2F2000%2F01%2Frdf-schema%23%3E.%0A%40prefix%20%3A%20%3Chttp%3A%2F%2Fexample.org%2Fsocrates%23%3E.%0A%0A%23%20facts%0A%3ASocrates%20a%20%3AHuman.%0A%3AHuman%20rdfs%3AsubClassOf%20%3AMortal.%0A%0A%23%20subclass%20rule%0A%7B%0A%20%20%20%20%3FS%20a%20%3FA.%0A%20%20%20%20%3FA%20rdfs%3AsubClassOf%20%3FB.%0A%7D%20%3D%3E%20%7B%0A%20%20%20%20%3FS%20a%20%3FB.%0A%7D%2E%0A)
|
|
30
32
|
|
|
31
33
|
## Quick start (Node.js)
|
|
32
34
|
|
|
33
35
|
### Requirements
|
|
34
36
|
|
|
35
|
-
-
|
|
37
|
+
- Node.js >= 18 (anything modern with `BigInt` support is fine)
|
|
36
38
|
|
|
37
39
|
### Install (npm)
|
|
38
40
|
|
|
@@ -53,7 +55,7 @@ npx eyeling examples/socrates.n3
|
|
|
53
55
|
### JavaScript API (Node)
|
|
54
56
|
|
|
55
57
|
```js
|
|
56
|
-
const { reason } = require(
|
|
58
|
+
const { reason } = require("eyeling");
|
|
57
59
|
|
|
58
60
|
const input = `
|
|
59
61
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
|
|
@@ -72,7 +74,7 @@ console.log(output);
|
|
|
72
74
|
ESM:
|
|
73
75
|
|
|
74
76
|
```js
|
|
75
|
-
import eyeling from
|
|
77
|
+
import eyeling from "eyeling";
|
|
76
78
|
|
|
77
79
|
const output = eyeling.reason({ proofComments: false }, input);
|
|
78
80
|
console.log(output);
|
|
@@ -80,6 +82,28 @@ console.log(output);
|
|
|
80
82
|
|
|
81
83
|
Note: the API currently shells out to the bundled `eyeling.js` CLI under the hood (simple + robust).
|
|
82
84
|
|
|
85
|
+
### Testing
|
|
86
|
+
|
|
87
|
+
From a repo checkout:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm test
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or run individual suites:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm run test:api
|
|
97
|
+
npm run test:examples
|
|
98
|
+
npm run test:package
|
|
99
|
+
npm run test:packlist
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `test:api` runs an independent JS API test suite (does not rely on `examples/`).
|
|
103
|
+
- `test:examples` runs the `examples/test` runner and compares against the golden outputs in `examples/output`.
|
|
104
|
+
- `test:package` does a “real consumer” smoke test: `npm pack` → install tarball into a temp project → run API + CLI + examples.
|
|
105
|
+
- `test:packlist` sanity-checks what will be published in the npm tarball (and the CLI shebang/bin wiring).
|
|
106
|
+
|
|
83
107
|
### Run a single file
|
|
84
108
|
|
|
85
109
|
From the repo root:
|
|
@@ -94,10 +118,10 @@ node eyeling.js examples/socrates.n3
|
|
|
94
118
|
|
|
95
119
|
By default, `eyeling`:
|
|
96
120
|
|
|
97
|
-
1. parses the input (facts + rules)
|
|
98
|
-
2. runs **forward chaining to a fixpoint
|
|
99
|
-
3. prints only **newly derived forward facts** (not the original input facts)
|
|
100
|
-
4. prints a compact per-triple explanation as `#` comments (can be disabled)
|
|
121
|
+
1. parses the input (facts + rules)
|
|
122
|
+
2. runs **forward chaining to a fixpoint**
|
|
123
|
+
3. prints only **newly derived forward facts** (not the original input facts)
|
|
124
|
+
4. prints a compact per-triple explanation as `#` comments (can be disabled)
|
|
101
125
|
|
|
102
126
|
### Options
|
|
103
127
|
|
|
@@ -116,7 +140,8 @@ node eyeling.js -n examples/socrates.n3
|
|
|
116
140
|
npm run test:examples
|
|
117
141
|
```
|
|
118
142
|
|
|
119
|
-
This runs `eyeling.js` over each example and compares against the golden outputs in `examples/output
|
|
143
|
+
This runs `eyeling.js` over each example and compares against the golden outputs in `examples/output`
|
|
144
|
+
(works both in a git checkout and in an npm-installed package).
|
|
120
145
|
|
|
121
146
|
## What output do I get?
|
|
122
147
|
|
|
@@ -131,14 +156,14 @@ The proof comments are compact “local justifications” per derived triple (no
|
|
|
131
156
|
|
|
132
157
|
### Forward + backward chaining
|
|
133
158
|
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
- **Forward chaining to fixpoint** for forward rules written as `{ P } => { C } .`
|
|
160
|
+
- **Backward chaining (SLD-style)** for backward rules written as `{ H } <= { B } .` and for built-ins.
|
|
136
161
|
|
|
137
162
|
Forward rule premises are proved using:
|
|
138
163
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
- ground facts (input + derived)
|
|
165
|
+
- backward rules
|
|
166
|
+
- built-ins
|
|
142
167
|
|
|
143
168
|
The CLI prints only newly derived forward facts.
|
|
144
169
|
|
|
@@ -146,63 +171,59 @@ The CLI prints only newly derived forward facts.
|
|
|
146
171
|
|
|
147
172
|
`eyeling` stays tiny, but includes a few key performance mechanisms:
|
|
148
173
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
* for very deep backward chains, substitutions may be compactified (semantics-preserving) to avoid quadratic “copy a growing substitution object” behavior.
|
|
174
|
+
- facts are indexed for matching:
|
|
175
|
+
- by predicate, and (when possible) by **(predicate, object)** (important for type-heavy workloads)
|
|
176
|
+
- duplicate detection uses a fast key path when a triple is fully IRI/Literal-shaped
|
|
177
|
+
- backward rules are indexed by head predicate
|
|
178
|
+
- the backward prover is **iterative** (explicit stack), so deep chains won’t blow the JS call stack
|
|
179
|
+
- for very deep backward chains, substitutions may be compactified (semantics-preserving) to avoid quadratic “copy a growing substitution object” behavior
|
|
156
180
|
|
|
157
181
|
## Parsing: practical N3 subset
|
|
158
182
|
|
|
159
183
|
Supported:
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
* datatyped literals with `^^`
|
|
175
|
-
* `#` line comments
|
|
185
|
+
- `@prefix` / `@base`
|
|
186
|
+
- triples with `;` and `,`
|
|
187
|
+
- variables `?x`
|
|
188
|
+
- blank nodes:
|
|
189
|
+
- anonymous `[]`
|
|
190
|
+
- property lists `[ :p :o; :q :r ]`
|
|
191
|
+
- collections `( ... )`
|
|
192
|
+
- quoted formulas `{ ... }`
|
|
193
|
+
- implications:
|
|
194
|
+
- forward rules `{ P } => { C } .`
|
|
195
|
+
- backward rules `{ H } <= { B } .`
|
|
196
|
+
- datatyped literals with `^^`
|
|
197
|
+
- `#` line comments
|
|
176
198
|
|
|
177
199
|
Non-goals / current limits:
|
|
178
200
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
201
|
+
- not a full W3C N3 grammar (some edge cases for identifiers, quantifiers, advanced syntax)
|
|
202
|
+
- quoted formulas are matched as whole formulas (no pattern matching inside formulas yet)
|
|
203
|
+
- proof output is local per derived triple (not a global exported proof tree)
|
|
182
204
|
|
|
183
205
|
## Blank nodes and quantification (pragmatic N3/EYE-style)
|
|
184
206
|
|
|
185
207
|
`eyeling` follows the usual N3 intuition:
|
|
186
208
|
|
|
187
|
-
1. blank nodes in facts are normal RDF blanks (`_:b1`, `_:b2`, … within a run)
|
|
188
|
-
2. blank nodes in rule premises behave like rule-scoped universals (similar to variables)
|
|
189
|
-
3. blank nodes only in rule conclusions behave like existentials:
|
|
190
|
-
each rule firing generates fresh Skolem blanks (`_:sk_0`, `_:sk_1`, …).
|
|
209
|
+
1. blank nodes in facts are normal RDF blanks (`_:b1`, `_:b2`, … within a run)
|
|
210
|
+
2. blank nodes in rule premises behave like rule-scoped universals (similar to variables)
|
|
211
|
+
3. blank nodes only in rule conclusions behave like existentials: each rule firing generates fresh Skolem blanks (`_:sk_0`, `_:sk_1`, …)
|
|
191
212
|
|
|
192
213
|
Equal facts up to renaming of Skolem IDs are treated as duplicates and are not re-added.
|
|
193
214
|
|
|
194
215
|
## Rule-producing rules (meta-rules)
|
|
195
216
|
|
|
196
|
-
`eyeling` understands the `log:implies` / `log:impliedBy` idiom
|
|
217
|
+
`eyeling` understands the `log:implies` / `log:impliedBy` idiom.
|
|
197
218
|
|
|
198
219
|
Top level:
|
|
199
220
|
|
|
200
|
-
|
|
201
|
-
|
|
221
|
+
- `{ P } log:implies { C } .` becomes a forward rule `{ P } => { C } .`
|
|
222
|
+
- `{ H } log:impliedBy { B } .` becomes a backward rule `{ H } <= { B } .`
|
|
202
223
|
|
|
203
224
|
During reasoning:
|
|
204
225
|
|
|
205
|
-
|
|
226
|
+
- any **derived** `log:implies` / `log:impliedBy` triple with formula subject/object is turned into a new live forward/backward rule.
|
|
206
227
|
|
|
207
228
|
## Inference fuse — `{ ... } => false.`
|
|
208
229
|
|
|
@@ -211,6 +232,7 @@ Rules whose conclusion is `false` are treated as hard failures:
|
|
|
211
232
|
```n3
|
|
212
233
|
:stone :color :black .
|
|
213
234
|
:stone :color :white .
|
|
235
|
+
|
|
214
236
|
{ ?X :color :black . ?X :color :white . } => false.
|
|
215
237
|
```
|
|
216
238
|
|
|
@@ -220,13 +242,12 @@ As soon as the premise is provable, `eyeling` exits with status code `2`.
|
|
|
220
242
|
|
|
221
243
|
`eyeling` implements a pragmatic subset of common N3 builtin families and evaluates them during backward goal proving:
|
|
222
244
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
245
|
+
- **crypto**: `crypto:md5` `crypto:sha` `crypto:sha256` `crypto:sha512`
|
|
246
|
+
- **list**: `list:append` `list:first` `list:firstRest` `list:in` `list:iterate` `list:last` `list:length` `list:map` `list:member` `list:memberAt` `list:notMember` `list:remove` `list:reverse` `list:sort`
|
|
247
|
+
- **log**: `log:collectAllIn` `log:equalTo` `log:forAllIn` `log:impliedBy` `log:implies` `log:notEqualTo` `log:notIncludes` `log:skolem` `log:uri`
|
|
248
|
+
- **math**: `math:absoluteValue` `math:acos` `math:asin` `math:atan` `math:cos` `math:cosh` `math:degrees` `math:difference` `math:equalTo` `math:exponentiation` `math:greaterThan` `math:lessThan` `math:negation` `math:notEqualTo` `math:notGreaterThan` `math:notLessThan` `math:product` `math:quotient` `math:remainder` `math:rounded` `math:sin` `math:sinh` `math:sum` `math:tan` `math:tanh`
|
|
249
|
+
- **string**: `string:concatenation` `string:contains` `string:containsIgnoringCase` `string:endsWith` `string:equalIgnoringCase` `string:format` `string:greaterThan` `string:lessThan` `string:matches` `string:notEqualIgnoringCase` `string:notGreaterThan` `string:notLessThan` `string:notMatches` `string:replace` `string:scrape` `string:startsWith`
|
|
250
|
+
- **time**: `time:localTime`
|
|
230
251
|
|
|
231
252
|
## License
|
|
232
253
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyeling",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.16",
|
|
4
4
|
"description": "A minimal Notation3 (N3) reasoner in JavaScript.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -29,9 +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
35
|
"test:package": "bash test/package-smoke.sh",
|
|
35
|
-
"test": "npm run test:api && npm run test:examples"
|
|
36
|
+
"test": "npm run test:packlist && npm run test:api && npm run test:examples"
|
|
36
37
|
}
|
|
37
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
|
+
|