eyeling 1.5.15 → 1.5.17

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 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, and of course we also keep all reasoning in the browser.
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
- - edit an N3 program directly,
22
- - load an N3 program from a URL,
23
- - share a link with the program encoded in the URL fragment (`#...`).
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
- - A reasonably recent Node.js (anything modern with `BigInt` support is fine).
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('eyeling');
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 'eyeling';
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 in `examples` directory 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
- * **Forward chaining to fixpoint** for forward rules written as `{ P } => { C } .`
135
- * **Backward chaining (SLD-style)** for backward rules written as `{ H } <= { B } .` and for built-ins.
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
- * ground facts (input + derived),
140
- * backward rules,
141
- * built-ins.
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
- * facts are indexed for matching:
150
-
151
- * by predicate, and (when possible) by **(predicate, object)** (important for type-heavy workloads),
152
- * duplicate detection uses a fast key path when a triple is fully IRI/Literal-shaped,
153
- * backward rules are indexed by head predicate,
154
- * the backward prover is **iterative** (explicit stack), so deep chains won’t blow the JS call stack,
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
- * `@prefix` / `@base`
162
- * triples with `;` and `,`
163
- * variables `?x`
164
- * blank nodes:
165
-
166
- * anonymous `[]`
167
- * property lists `[ :p :o; :q :r ]`
168
- * collections `( ... )`
169
- * quoted formulas `{ ... }`
170
- * implications:
171
-
172
- * forward rules `{ P } => { C } .`
173
- * backward rules `{ H } <= { B } .`
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
- * not a full W3C N3 grammar (some edge cases for identifiers, quantifiers, advanced syntax),
180
- * quoted formulas are matched as whole formulas (no pattern matching inside formulas yet),
181
- * proof output is local per derived triple (not a global exported proof tree).
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
- * `{ P } log:implies { C } .` becomes a forward rule `{ P } => { C } .`
201
- * `{ H } log:impliedBy { B } .` becomes a backward rule `{ H } <= { B } .`
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
- * any **derived** `log:implies` / `log:impliedBy` triple with formula subject/object is turned into a new live forward/backward rule.
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
- * **crypto**: `crypto:md5` `crypto:sha` `crypto:sha256` `crypto:sha512`
224
- * **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`
225
- * **log**: `log:collectAllIn` `log:equalTo` `log:forAllIn` `log:impliedBy` `log:implies` `log:notEqualTo` `log:notIncludes` `log:skolem` `log:uri`
226
- * **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`
227
- * **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`
228
- * **time**: `time:localTime`
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.15",
3
+ "version": "1.5.17",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -31,8 +31,10 @@
31
31
  "scripts": {
32
32
  "test:packlist": "node test/packlist.test.js",
33
33
  "test:api": "node test/api.test.js",
34
- "test:examples": "bash -lc 'cd examples && ./test'",
35
- "test:package": "bash test/package-smoke.sh",
36
- "test": "npm run test:packlist && npm run test:api && npm run test:examples"
34
+ "test:examples": "node test/examples.test.js",
35
+ "test:package": "node test/package.test.js",
36
+ "test": "npm run test:packlist && npm run test:api && npm run test:examples",
37
+ "preversion": "npm test",
38
+ "postversion": "git push origin HEAD --follow-tags"
37
39
  }
38
40
  }
package/test/api.test.js CHANGED
@@ -556,6 +556,7 @@ let passed = 0;
556
556
  let failed = 0;
557
557
 
558
558
  (async function main() {
559
+ const suiteStart = Date.now();
559
560
  info(`Running ${cases.length} API tests (independent of examples/)`);
560
561
 
561
562
  for (const tc of cases) {
@@ -602,6 +603,8 @@ let failed = 0;
602
603
  }
603
604
 
604
605
  console.log('');
606
+ const suiteMs = Date.now() - suiteStart;
607
+ console.log(`${C.y}==${C.n} Total elapsed: ${suiteMs} ms`);
605
608
  if (failed === 0) {
606
609
  ok(`All API tests passed (${passed}/${cases.length})`);
607
610
  process.exit(0);
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const cp = require('node:child_process');
8
+
9
+ const TTY = process.stdout.isTTY;
10
+ const C = TTY
11
+ ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
12
+ : { g: '', r: '', y: '', dim: '', n: '' };
13
+
14
+ function ok(msg) { console.log(`${C.g}OK${C.n} ${msg}`); }
15
+ function fail(msg) { console.error(`${C.r}FAIL${C.n} ${msg}`); }
16
+ function info(msg) { console.log(`${C.y}==${C.n} ${msg}`); }
17
+
18
+ function padRight(s, n) {
19
+ s = String(s);
20
+ return s.length >= n ? s : (s + ' '.repeat(n - s.length));
21
+ }
22
+
23
+ function padLeft(s, n) {
24
+ s = String(s);
25
+ return s.length >= n ? s : (' '.repeat(n - s.length) + s);
26
+ }
27
+
28
+ function run(cmd, args, opts = {}) {
29
+ return cp.spawnSync(cmd, args, {
30
+ encoding: 'utf8',
31
+ maxBuffer: 200 * 1024 * 1024,
32
+ ...opts,
33
+ });
34
+ }
35
+
36
+ function hasGit() {
37
+ const r = run('git', ['--version']);
38
+ return r.status === 0;
39
+ }
40
+
41
+ function inGitWorktree(cwd) {
42
+ if (!hasGit()) return false;
43
+ const r = run('git', ['rev-parse', '--is-inside-work-tree'], { cwd });
44
+ return r.status === 0 && String(r.stdout).trim() === 'true';
45
+ }
46
+
47
+ // Expectation logic (same as bash version):
48
+ // 1) If file contains a comment like: # expect-exit: 2 -> use that
49
+ // 2) Else, if it contains "=> false" -> expect exit 2
50
+ // 3) Else -> expect exit 0
51
+ function expectedExitCode(n3Text) {
52
+ const m = n3Text.match(/^[ \t]*#[: ]*expect-exit:[ \t]*([0-9]+)\b/m);
53
+ if (m) return parseInt(m[1], 10);
54
+ if (/=>\s*false\b/.test(n3Text)) return 2;
55
+ return 0;
56
+ }
57
+
58
+ function getEyelingVersion(nodePath, eyelingJsPath, cwd) {
59
+ const r = run(nodePath, [eyelingJsPath, '-v'], { cwd });
60
+ // eyeling prints version to stdout in your CLI
61
+ const s = (r.stdout || r.stderr || '').trim();
62
+ return s || 'eyeling (unknown version)';
63
+ }
64
+
65
+ function mkTmpFile() {
66
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeling-examples-'));
67
+ const file = path.join(dir, 'generated.n3');
68
+ return { dir, file };
69
+ }
70
+
71
+ function rmrf(p) {
72
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch {}
73
+ }
74
+
75
+ function main() {
76
+ const suiteStart = Date.now();
77
+
78
+ // package root: .../test/examples.test.js -> root is one level up
79
+ const root = path.resolve(__dirname, '..');
80
+ const examplesDir = path.join(root, 'examples');
81
+ const outputDir = path.join(examplesDir, 'output');
82
+ const eyelingJsPath = path.join(root, 'eyeling.js');
83
+ const nodePath = process.execPath;
84
+
85
+ if (!fs.existsSync(examplesDir)) {
86
+ fail(`Missing examples directory: ${examplesDir}`);
87
+ process.exit(1);
88
+ }
89
+ if (!fs.existsSync(eyelingJsPath)) {
90
+ fail(`Missing eyeling.js: ${eyelingJsPath}`);
91
+ process.exit(1);
92
+ }
93
+
94
+ const IN_GIT = inGitWorktree(root);
95
+
96
+ // Header
97
+ console.log(`${C.y}-------------------------------------------------${C.n}`);
98
+ console.log(`${C.y}running eyeling examples${C.n}`);
99
+ console.log(
100
+ `${C.y}using ${getEyelingVersion(nodePath, eyelingJsPath, root)} and node ${process.version}${C.n}`
101
+ );
102
+ console.log(`${C.y}-------------------------------------------------${C.n}`);
103
+ console.log('');
104
+
105
+ // In maintainer mode we write expected outputs (tracked) to examples/output/
106
+ if (IN_GIT) fs.mkdirSync(outputDir, { recursive: true });
107
+
108
+ const files = fs.readdirSync(examplesDir)
109
+ .filter(f => f.endsWith('.n3'))
110
+ .sort((a, b) => a.localeCompare(b));
111
+
112
+ if (files.length === 0) {
113
+ info('No .n3 files found in examples/');
114
+ process.exit(0);
115
+ }
116
+
117
+ let OK = 0;
118
+ let DIFF = 0;
119
+
120
+ for (const file of files) {
121
+ const filePath = path.join(examplesDir, file);
122
+ const expectedPath = path.join(outputDir, file); // examples/output/<file>
123
+
124
+ const start = Date.now();
125
+
126
+ let n3Text = '';
127
+ try {
128
+ n3Text = fs.readFileSync(filePath, 'utf8');
129
+ } catch (e) {
130
+ const ms = Date.now() - start;
131
+ process.stdout.write(padRight(file, 36));
132
+ process.stdout.write(`${C.y}${padLeft(`${ms} ms`, 10)}${C.n} `);
133
+ console.log(`${C.r}DIFF${C.n} (cannot read input: ${e.message})`);
134
+ DIFF++;
135
+ continue;
136
+ }
137
+
138
+ const expectedRc = expectedExitCode(n3Text);
139
+
140
+ // Decide where to write generated output
141
+ let tmp = null;
142
+ let generatedPath = expectedPath;
143
+
144
+ if (!IN_GIT) {
145
+ // npm-installed / no .git: never modify output/ in node_modules
146
+ if (!fs.existsSync(expectedPath)) {
147
+ const ms = Date.now() - start;
148
+ process.stdout.write(padRight(file, 36));
149
+ process.stdout.write(`${C.y}${padLeft(`${ms} ms`, 10)}${C.n} `);
150
+ console.log(`${C.r}MISSING expected output/${file}${C.n}`);
151
+ DIFF++;
152
+ continue;
153
+ }
154
+ tmp = mkTmpFile();
155
+ generatedPath = tmp.file;
156
+ }
157
+
158
+ // Run eyeling, capture exit code without aborting the suite
159
+ // We run `node eyeling.js <file>` from examplesDir so relative paths match old behavior.
160
+ const r = run(nodePath, [eyelingJsPath, file], { cwd: examplesDir });
161
+ const rc = (r.status == null) ? 1 : r.status;
162
+
163
+ // Write stdout to the chosen output file (expected in git mode, tmp in npm mode)
164
+ try {
165
+ fs.writeFileSync(generatedPath, r.stdout || '', 'utf8');
166
+ } catch (e) {
167
+ const ms = Date.now() - start;
168
+ process.stdout.write(padRight(file, 36));
169
+ process.stdout.write(`${C.y}${padLeft(`${ms} ms`, 10)}${C.n} `);
170
+ console.log(`${C.r}DIFF${C.n} (cannot write output: ${e.message})`);
171
+ DIFF++;
172
+ if (tmp) rmrf(tmp.dir);
173
+ continue;
174
+ }
175
+
176
+ const ms = Date.now() - start;
177
+
178
+ // Compare outputs
179
+ let diffOk = false;
180
+
181
+ if (IN_GIT) {
182
+ // Compare expectedPath against HEAD using git diff
183
+ const d = run('git', ['diff', '--quiet', '--', path.posix.join('output', file)], { cwd: examplesDir });
184
+ diffOk = (d.status === 0);
185
+ } else {
186
+ // Compare expectedPath vs generatedPath without needing a repo
187
+ if (hasGit()) {
188
+ const d = run('git', ['diff', '--no-index', '--quiet', expectedPath, generatedPath], { cwd: examplesDir });
189
+ diffOk = (d.status === 0);
190
+ } else {
191
+ const d = run('diff', ['-u', expectedPath, generatedPath], { cwd: examplesDir });
192
+ diffOk = (d.status === 0);
193
+ }
194
+ }
195
+
196
+ // Decide pass/fail
197
+ process.stdout.write(padRight(file, 36));
198
+ process.stdout.write(`${C.y}${padLeft(`${ms} ms`, 10)}${C.n} `);
199
+
200
+ if (diffOk && rc === expectedRc) {
201
+ if (rc === 0) {
202
+ console.log(`${C.g}OK${C.n}`);
203
+ } else {
204
+ console.log(`${C.g}OK${C.n} (exit ${rc})`);
205
+ }
206
+ OK++;
207
+ } else {
208
+ if (rc !== expectedRc) {
209
+ console.log(`${C.r}DIFF${C.n} (exit ${rc}, expected ${expectedRc})`);
210
+ } else {
211
+ console.log(`${C.r}DIFF${C.n}`);
212
+ }
213
+ DIFF++;
214
+
215
+ // In npm mode, show a diff (nice UX) without modifying node_modules
216
+ if (!IN_GIT) {
217
+ if (hasGit()) {
218
+ const d = run('git', ['diff', '--no-index', expectedPath, generatedPath], { cwd: examplesDir });
219
+ if (d.stdout) process.stdout.write(d.stdout);
220
+ if (d.stderr) process.stderr.write(d.stderr);
221
+ } else {
222
+ const d = run('diff', ['-u', expectedPath, generatedPath], { cwd: examplesDir });
223
+ if (d.stdout) process.stdout.write(d.stdout);
224
+ if (d.stderr) process.stderr.write(d.stderr);
225
+ }
226
+ }
227
+ }
228
+
229
+ // cleanup tmp file
230
+ if (tmp) rmrf(tmp.dir);
231
+ }
232
+
233
+ console.log('');
234
+ const suiteMs = Date.now() - suiteStart;
235
+ console.log(`${C.y}==${C.n} Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
236
+ console.log(`${C.y}==${C.n} ${C.g}${OK} OK${C.n} ${C.r}${DIFF} DIFF${C.n}`);
237
+
238
+ process.exit(DIFF === 0 ? 0 : 2);
239
+ }
240
+
241
+ main();
242
+
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const cp = require('node:child_process');
8
+
9
+ const TTY = process.stdout.isTTY;
10
+ const C = TTY
11
+ ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
12
+ : { g: '', r: '', y: '', dim: '', n: '' };
13
+
14
+ function info(msg) { console.log(`${C.y}==${C.n} ${msg}`); }
15
+ function ok(msg) { console.log(`${C.g}OK${C.n} ${msg}`); }
16
+ function fail(msg) { console.error(`${C.r}FAIL${C.n} ${msg}`); }
17
+
18
+ function isWin() { return process.platform === 'win32'; }
19
+ function npmCmd() { return isWin() ? 'npm.cmd' : 'npm'; }
20
+
21
+ function rmrf(p) {
22
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch {}
23
+ }
24
+
25
+ function run(cmd, args, opts = {}) {
26
+ const res = cp.spawnSync(cmd, args, {
27
+ encoding: 'utf8',
28
+ maxBuffer: 200 * 1024 * 1024,
29
+ ...opts,
30
+ });
31
+ return res;
32
+ }
33
+
34
+ function runChecked(cmd, args, opts = {}) {
35
+ // Print the command in a dim style
36
+ console.log(`${C.dim}$ ${cmd} ${args.join(' ')}${C.n}`);
37
+ const res = run(cmd, args, opts);
38
+ if (res.error) throw res.error;
39
+ if (res.status !== 0) {
40
+ const err = new Error(`Command failed (${cmd} ${args.join(' ')}), exit ${res.status}`);
41
+ err.code = res.status;
42
+ err.stdout = res.stdout;
43
+ err.stderr = res.stderr;
44
+ throw err;
45
+ }
46
+ return res;
47
+ }
48
+
49
+ function packTarball(root) {
50
+ // `npm pack --silent` prints the filename (usually one line)
51
+ const res = runChecked(npmCmd(), ['pack', '--silent'], { cwd: root });
52
+ const out = String(res.stdout || '').trim().split(/\r?\n/).filter(Boolean);
53
+ if (out.length === 0) throw new Error('npm pack produced no output');
54
+ return out[out.length - 1].trim(); // tarball filename in root
55
+ }
56
+
57
+ function main() {
58
+ const suiteStart = Date.now();
59
+ const root = path.resolve(__dirname, '..');
60
+
61
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeling-smoke-'));
62
+ const cleanup = () => rmrf(tmp);
63
+
64
+ let tgzInRoot = null;
65
+
66
+ try {
67
+ info('Building tarball (npm pack)');
68
+ tgzInRoot = packTarball(root);
69
+ const srcTgz = path.join(root, tgzInRoot);
70
+ const dstTgz = path.join(tmp, tgzInRoot);
71
+
72
+ fs.renameSync(srcTgz, dstTgz);
73
+
74
+ info('Creating temp project + installing tarball');
75
+ runChecked(npmCmd(), ['init', '-y'], { cwd: tmp, stdio: 'ignore' });
76
+ runChecked(npmCmd(), ['install', `./${tgzInRoot}`, '--no-audit', '--no-fund'], { cwd: tmp, stdio: 'inherit' });
77
+
78
+ info('API smoke test');
79
+ // Run a tiny API check via node -e
80
+ const apiCode = `
81
+ const { reason } = require('eyeling');
82
+ const input = \`
83
+ { <http://example.org/s> <http://example.org/p> <http://example.org/o>. }
84
+ => { <http://example.org/s> <http://example.org/q> <http://example.org/o>. }.
85
+
86
+ <http://example.org/s> <http://example.org/p> <http://example.org/o>.
87
+ \`;
88
+ const out = reason({ proofComments: false }, input);
89
+ const re = /<http:\\/\\/example\\.org\\/s>\\s+<http:\\/\\/example\\.org\\/q>\\s+<http:\\/\\/example\\.org\\/o>\\s*\\./;
90
+ if (!re.test(out)) {
91
+ console.error('Unexpected output:\\n' + out);
92
+ process.exit(1);
93
+ }
94
+ console.log('OK: API works');
95
+ `;
96
+ runChecked(process.execPath, ['-e', apiCode], { cwd: tmp, stdio: 'inherit' });
97
+ ok('API works');
98
+
99
+ info('CLI smoke test');
100
+ const bin = isWin()
101
+ ? path.join(tmp, 'node_modules', '.bin', 'eyeling.cmd')
102
+ : path.join(tmp, 'node_modules', '.bin', 'eyeling');
103
+ runChecked(bin, ['-v'], { cwd: tmp, stdio: 'inherit' });
104
+ ok('CLI works');
105
+
106
+ info('Examples test (installed package)');
107
+ const examplesRunner = path.join(tmp, 'node_modules', 'eyeling', 'test', 'examples.test.js');
108
+ runChecked(process.execPath, [examplesRunner], { cwd: tmp, stdio: 'inherit' });
109
+ ok('Installed examples test passed');
110
+
111
+ const suiteMs = Date.now() - suiteStart;
112
+ console.log('');
113
+ ok(`Packaged install smoke test passed ${C.dim}(${suiteMs} ms, ${(suiteMs / 1000).toFixed(2)} s)${C.n}`);
114
+ process.exit(0);
115
+ } catch (e) {
116
+ console.log('');
117
+ fail(e && e.stack ? e.stack : String(e));
118
+ process.exit(1);
119
+ } finally {
120
+ // If rename failed and the tarball still exists in root, try to delete it
121
+ if (tgzInRoot) {
122
+ const maybe = path.join(root, tgzInRoot);
123
+ if (fs.existsSync(maybe)) {
124
+ try { fs.unlinkSync(maybe); } catch {}
125
+ }
126
+ }
127
+ cleanup();
128
+ }
129
+ }
130
+
131
+ main();
132
+
@@ -45,7 +45,6 @@ try {
45
45
  'LICENSE.md',
46
46
  'eyeling.js',
47
47
  'index.js',
48
- 'examples/test',
49
48
  ];
50
49
 
51
50
  for (const p of mustHave) assert.ok(paths.has(p), `missing from npm pack: ${p}`);
package/examples/test DELETED
@@ -1,154 +0,0 @@
1
- #!/bin/bash
2
- set -euo pipefail
3
-
4
- RED="\e[31m"
5
- GREEN="\e[32m"
6
- YELLOW="\e[33m"
7
- NORMAL="\e[0;39m"
8
-
9
- OK=0
10
- DIFF=0
11
-
12
- pad() {
13
- # pad "text" width
14
- local s="$1" w="$2"
15
- printf "%-${w}s" "$s"
16
- }
17
-
18
- now_ms() {
19
- echo $(( $(date +%s%N) / 1000000 ))
20
- }
21
-
22
- in_git_worktree() {
23
- command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1
24
- }
25
-
26
- # Expectation logic:
27
- # 1) If the .n3 contains a comment like: # expect-exit: 2 -> use that
28
- # 2) Else, if it contains "=> false" -> expect exit 2
29
- # 3) Else -> expect exit 0
30
- expected_exit_code() {
31
- local file="$1"
32
- local line
33
- if line=$(grep -Em1 '^[[:space:]]*#[: ]*expect-exit:[[:space:]]*[0-9]+' "$file" 2>/dev/null); then
34
- if [[ "$line" =~ ([0-9]+) ]]; then
35
- echo "${BASH_REMATCH[1]}"
36
- return
37
- fi
38
- fi
39
- if grep -Eq '=>[[:space:]]*false\b' "$file"; then
40
- echo 2
41
- else
42
- echo 0
43
- fi
44
- }
45
-
46
- echo -e "${YELLOW}-------------------------------------------------${NORMAL}"
47
- echo -e "${YELLOW}running eyeling examples${NORMAL}"
48
- echo -e "${YELLOW}using $(../eyeling.js -v) and node $(node --version)${NORMAL}"
49
- echo -e "${YELLOW}-------------------------------------------------${NORMAL}"
50
- echo ""
51
-
52
- IN_GIT=0
53
- if in_git_worktree; then IN_GIT=1; fi
54
-
55
- # In maintainer mode we write expected outputs (tracked) to output/
56
- if [[ $IN_GIT -eq 1 ]]; then
57
- mkdir -p output
58
- fi
59
-
60
- begin="$(now_ms)"
61
-
62
- for file in *.n3; do
63
- printf "%s" "$(pad "$file" 36)"
64
- start="$(now_ms)"
65
-
66
- expected_rc="$(expected_exit_code "$file")"
67
-
68
- # Decide where we write generated output
69
- tmp=""
70
- generated=""
71
- expected="output/${file}"
72
-
73
- if [[ $IN_GIT -eq 1 ]]; then
74
- generated="$expected" # overwrite expected output in working tree
75
- else
76
- # npm-installed / no .git: never modify output/ in node_modules
77
- if [[ ! -f "$expected" ]]; then
78
- end="$(now_ms)"
79
- ms=$((end-start))
80
- echo -en "${YELLOW}$(pad "${ms} ms" 10)${NORMAL} "
81
- echo -e "${RED}MISSING expected ${expected}${NORMAL}"
82
- ((++DIFF))
83
- continue
84
- fi
85
- tmp="$(mktemp)"
86
- generated="$tmp"
87
- fi
88
-
89
- # Run eyeling, capture exit code without breaking the loop
90
- rc=0
91
- if ../eyeling.js "$file" > "$generated"; then
92
- rc=0
93
- else
94
- rc=$?
95
- fi
96
-
97
- end="$(now_ms)"
98
- ms=$((end-start))
99
- echo -en "${YELLOW}$(pad "${ms} ms" 10)${NORMAL} "
100
-
101
- # Compare output
102
- diff_ok=0
103
- if [[ $IN_GIT -eq 1 ]]; then
104
- if git diff --quiet -- "$expected"; then diff_ok=1; fi
105
- else
106
- if command -v git >/dev/null 2>&1; then
107
- if git diff --no-index --quiet -- "$expected" "$generated" >/dev/null 2>&1; then diff_ok=1; fi
108
- else
109
- if diff -u "$expected" "$generated" >/dev/null 2>&1; then diff_ok=1; fi
110
- fi
111
- fi
112
-
113
- # Decide pass/fail
114
- if [[ $diff_ok -eq 1 && $rc -eq $expected_rc ]]; then
115
- if [[ $rc -eq 0 ]]; then
116
- echo -e "${GREEN}OK${NORMAL}"
117
- else
118
- echo -e "${GREEN}OK${NORMAL} (exit ${rc})"
119
- fi
120
- ((++OK))
121
- else
122
- if [[ $rc -ne $expected_rc ]]; then
123
- echo -e "${RED}DIFF${NORMAL} (exit ${rc}, expected ${expected_rc})"
124
- else
125
- echo -e "${RED}DIFF${NORMAL}"
126
- fi
127
- ((++DIFF))
128
-
129
- # In npm mode, show a git-style diff when available (nice UX)
130
- if [[ $IN_GIT -eq 0 ]]; then
131
- if command -v git >/dev/null 2>&1; then
132
- git diff --no-index -- "$expected" "$generated" || true
133
- else
134
- diff -u "$expected" "$generated" || true
135
- fi
136
- fi
137
- fi
138
-
139
- # cleanup tmp file
140
- if [[ -n "$tmp" ]]; then rm -f "$tmp"; fi
141
- done
142
-
143
- end="$(now_ms)"
144
- total=$((end-begin))
145
-
146
- echo ""
147
- echo -e "${YELLOW}${total} ms${NORMAL} ${GREEN}${OK} OK${NORMAL} ${RED}${DIFF} DIFF${NORMAL}"
148
-
149
- if [[ ${DIFF} -eq 0 ]]; then
150
- exit 0
151
- else
152
- exit 2
153
- fi
154
-
@@ -1,55 +0,0 @@
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
-