@dallaylaen/ski-interpreter 2.2.0 → 2.3.0
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/CHANGELOG.md +51 -0
- package/README.md +86 -11
- package/bin/ski.js +195 -82
- package/lib/ski-interpreter.cjs.js +522 -329
- package/lib/ski-interpreter.cjs.js.map +3 -3
- package/lib/ski-interpreter.esm.js +522 -329
- package/lib/ski-interpreter.esm.js.map +2 -2
- package/lib/ski-interpreter.min.js +4 -0
- package/lib/ski-interpreter.min.js.map +7 -0
- package/lib/ski-quest.min.js +4 -0
- package/lib/ski-quest.min.js.map +7 -0
- package/package.json +11 -4
- package/types/src/expr.d.ts +201 -102
- package/types/src/extras.d.ts +17 -1
- package/types/src/internal.d.ts +23 -10
- package/types/src/parser.d.ts +46 -19
- package/types/src/quest.d.ts +87 -41
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.3.0] - 2026-02-28
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
- `ski.js` CLI interface changed (see below)
|
|
13
|
+
- Quest page css changes (see below)
|
|
14
|
+
- `diag()` does not indent `Alias` to reduce diffs
|
|
15
|
+
- Swapped `Expr.expect()` arguments: expected.expect(actual).
|
|
16
|
+
Mnemonic: expected is always an expr, actual may be whatever.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `Quest.Group` class and `Quest.verify()` for quest self-check capability.
|
|
21
|
+
- `Quest.selfCheck({ accepted: ..., rejected: ... })` replaces the homegrown `solution` mechanism.
|
|
22
|
+
- `{order: LI|LO}` option to `Expr.traverse()` for left-innermost / left-outermost traversal order.
|
|
23
|
+
- `FreeVar.global` constant as an explicit replacement for the `context=SKI` convention.
|
|
24
|
+
- `TermInfo` typedef for `infer()` results.
|
|
25
|
+
- CLI (`bin/ski.js`) rewritten with `commander`, now supports subcommands: `repl`, `eval`, `file`, `quest-check`.
|
|
26
|
+
- Quest authoring guide added to docs.
|
|
27
|
+
- `expr.expect(actual)` now returns `diag()` as -actual / +expected.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- `infer()` now limits recursion depth to prevent stack overflows (#10).
|
|
32
|
+
- Alias indentation in `diag()` output corrected.
|
|
33
|
+
- Quest page links fixed after previous rewrite.
|
|
34
|
+
- Aliases are no longer canonized without an explicit request.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `toLambda()` and `toSKI()` rewritten using left-innermost traverse.
|
|
39
|
+
- `Church` is now a descendant of `Expr` instead of `Native` (minor speedup).
|
|
40
|
+
- `Expr.constructor` removed for a minor speedup.
|
|
41
|
+
- `parser.add()` now accepts an options object in place of a plain note string.
|
|
42
|
+
- Quest solutions moved to `data/quest-solutions.json`; removed from quest-data.
|
|
43
|
+
- Quest nav items use stable `.ski-quest-nav-item` class instead of fragile `.ski-quest-index a`.
|
|
44
|
+
|
|
45
|
+
## [2.2.1] - 2026-02-22
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- `Expr.diag()` outputs an expression as an indented tree (breadth-first) with class names and variables labeled for deduplication.
|
|
50
|
+
- `lib/ski-quest.min.js` bundle to create quest pages.
|
|
51
|
+
- The [playground](https://dallaylaen.github.io/ski-interpreter/playground.html) gets history!
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Greatly improved type definitions.
|
|
56
|
+
- Quests calculate solution complexity via `fold()`.
|
|
57
|
+
|
|
8
58
|
## [2.2.0] - 2026-02-14
|
|
9
59
|
|
|
10
60
|
### BREAKING CHANGES
|
|
@@ -20,6 +70,7 @@ that give the initial expression when applied
|
|
|
20
70
|
from left to right: `((a, b), (c, d)) => [a, b, (c, d)]`
|
|
21
71
|
- Parser: Support for chained assignments (`'foo=bar=baz'` expressions)
|
|
22
72
|
- Parser: Support for multi-line comment syntax (`/* comments */`)
|
|
73
|
+
- `SKI_REPL=1 node -r @dallaylaen/ski-interpreter` will now start a REPL with the `SKI` class available globally.
|
|
23
74
|
|
|
24
75
|
### Changed
|
|
25
76
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Simple Kombinator Interpreter
|
|
2
2
|
|
|
3
|
+
> **A humane tooling for inhuman logic**
|
|
4
|
+
|
|
3
5
|
This package contains a
|
|
4
6
|
[combinatory logic](https://en.wikipedia.org/wiki/Combinatory_logic)
|
|
5
7
|
and [lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus)
|
|
@@ -49,11 +51,12 @@ back to human readable form.
|
|
|
49
51
|
|
|
50
52
|
# Execution strategy
|
|
51
53
|
|
|
52
|
-
Applications and native terms use
|
|
54
|
+
Applications and native terms use leftmost-outermost strategy, i.e. the first term in the tree
|
|
53
55
|
that has enough arguments is executed and the step ends there.
|
|
54
56
|
|
|
55
|
-
Lambda terms are lazy, i.e. the body is not touched
|
|
56
|
-
all free variables are bound.
|
|
57
|
+
Lambda terms are lazy, i.e. the body is not touched whatsoever
|
|
58
|
+
until all the free variables are bound.
|
|
59
|
+
This is consistent with combinator behavior under LO order.
|
|
57
60
|
|
|
58
61
|
# Playground
|
|
59
62
|
|
|
@@ -62,15 +65,13 @@ all free variables are bound.
|
|
|
62
65
|
* all of the above features (except comparison and JS-native terms) in your browser
|
|
63
66
|
* expressions have permalinks
|
|
64
67
|
* can configure verbosity and execution speed
|
|
68
|
+
* switchable visual highlighting of redexes and outline of subexpressions
|
|
65
69
|
|
|
66
70
|
* [Quests](https://dallaylaen.github.io/ski-interpreter/quest.html)
|
|
67
71
|
|
|
68
|
-
This page contains small
|
|
72
|
+
This page contains small combinatory logic exercises of increasing (hopefully) diffuculty.
|
|
69
73
|
Each task requires the user to build a combinator with specific properties.
|
|
70
|
-
|
|
71
|
-
# CLI
|
|
72
|
-
|
|
73
|
-
REPL comes with the package as [bin/ski.js](bin/ski.js).
|
|
74
|
+
New combinators are unlocked as the user progresses.
|
|
74
75
|
|
|
75
76
|
# Installation
|
|
76
77
|
|
|
@@ -78,6 +79,35 @@ REPL comes with the package as [bin/ski.js](bin/ski.js).
|
|
|
78
79
|
npm install @dallaylaen/ski-interpreter
|
|
79
80
|
```
|
|
80
81
|
|
|
82
|
+
# CLI
|
|
83
|
+
|
|
84
|
+
[bin/ski.js](bin/ski.js) - also available as `npx ski` - contains several subcommands:
|
|
85
|
+
|
|
86
|
+
## Subcommands
|
|
87
|
+
|
|
88
|
+
* **`repl`** - Start an interactive REPL
|
|
89
|
+
* `--verbose` - Show all evaluation steps
|
|
90
|
+
* Built-in commands (type `!help` in REPL):
|
|
91
|
+
* `!ls` - List all defined terms
|
|
92
|
+
* `!help` - Show available commands
|
|
93
|
+
|
|
94
|
+
* **`eval <expression>`** - Evaluate a single expression
|
|
95
|
+
* `--verbose` - Show all evaluation steps
|
|
96
|
+
* Example: `ski eval "S K K x"`
|
|
97
|
+
|
|
98
|
+
* **`file <filepath>`** - Evaluate expressions from a file
|
|
99
|
+
* `--verbose` - Show all evaluation steps
|
|
100
|
+
* Example: `ski file script.ski`
|
|
101
|
+
|
|
102
|
+
* **`quest-check <files...>`** - Validate quest definition files
|
|
103
|
+
* `--solution <file>` - Load solutions from a JSON file for verification
|
|
104
|
+
* Example: `ski quest-check quest1.json quest2.json --solution solutions.json`
|
|
105
|
+
|
|
106
|
+
If no subcommand is provided, help is displayed.
|
|
107
|
+
|
|
108
|
+
Running `SKI_REPL=1 node -r @dallaylaen/ski-interpreter/bin/ski.js`
|
|
109
|
+
will start a node shell with the `SKI` class available as a global variable.
|
|
110
|
+
|
|
81
111
|
# Usage
|
|
82
112
|
|
|
83
113
|
## A minimal example
|
|
@@ -146,8 +176,38 @@ const lambdaSteps = [...skiExpr.toLambda()];
|
|
|
146
176
|
|
|
147
177
|
## Fancy formatting
|
|
148
178
|
|
|
149
|
-
|
|
150
|
-
|
|
179
|
+
`expr.format(options?)` converts an expression to a string with fine-grained
|
|
180
|
+
control over notation. Called without arguments it is equivalent to
|
|
181
|
+
`expr.toString()`.
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
const expr = ski.parse('S K K');
|
|
185
|
+
|
|
186
|
+
expr.format() // 'S K K' (default, terse)
|
|
187
|
+
expr.format({ terse: false }) // 'S(K)(K)' — every argument gets parentheses
|
|
188
|
+
expr.format({ html: true }) // HTML-safe: free vars wrapped in <var>,
|
|
189
|
+
// '->' becomes '->', fancyName used when set
|
|
190
|
+
|
|
191
|
+
// Custom lambda notation
|
|
192
|
+
expr.format({ lambda: ['', '=>', ''], terse: false }) // JavaScript style
|
|
193
|
+
expr.format({ lambda: ['λ', '.', ''] }) // math style
|
|
194
|
+
expr.format({ lambda: ['(', '->', ')'], around: ['(', ')'], brackets: ['', ''] })
|
|
195
|
+
// Lisp style, still parseable
|
|
196
|
+
|
|
197
|
+
// Redex highlighting (e.g. for step-by-step HTML output)
|
|
198
|
+
ski.parse('I x').format({ html: true, redex: ['<b>', '</b>'] })
|
|
199
|
+
// '<b>I</b> <var>x</var>'
|
|
200
|
+
|
|
201
|
+
// inventory: show listed aliases by name, expand everything else
|
|
202
|
+
const { T } = ski.getTerms();
|
|
203
|
+
expr.format({ inventory: { T } }) // keeps T as 'T', expands any other aliases
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The `brackets`, `var`, `around`, and `redex` options each take a `[open, close]`
|
|
207
|
+
pair of strings; `lambda` takes a `[prefix, separator, suffix]` triple.
|
|
208
|
+
|
|
209
|
+
`expr.diag()` will instead output an indented expression tree (breadth-first)
|
|
210
|
+
with class information and variables labeled for disambiguation.
|
|
151
211
|
|
|
152
212
|
## Variable scoping
|
|
153
213
|
|
|
@@ -183,6 +243,9 @@ expr.traverse(e => e.equals(SKI.I) ? SKI.S.apply(SKI.K, SKI.K) : null);
|
|
|
183
243
|
// replaces all I's with S K K
|
|
184
244
|
// here a returned `Expr` object replaces the subexpression,
|
|
185
245
|
// whereas `null` means "leave it alone and descend if possible"
|
|
246
|
+
|
|
247
|
+
expr.fold(0, (acc, e) => acc + (e.equals(SKI.K) ? acc+1 : acc));
|
|
248
|
+
// counts the number of K's in the expression
|
|
186
249
|
```
|
|
187
250
|
|
|
188
251
|
## Test cases
|
|
@@ -207,12 +270,24 @@ q.check('K'); // fail
|
|
|
207
270
|
q.check('K(K(y x))') // nope! the variable scopes won't match
|
|
208
271
|
```
|
|
209
272
|
|
|
210
|
-
See [quest
|
|
273
|
+
See also [the quest guide](./docs/quest-intro.md) for more details on building your own quests or even interactive quest pages.
|
|
274
|
+
|
|
275
|
+
# Package contents
|
|
276
|
+
|
|
277
|
+
* `lib/ski-interpreter.cjs.js` - main entry point for Node.js;
|
|
278
|
+
* `lib/ski-interpreter.esm.js` - main entry point for ES modules;
|
|
279
|
+
* `lib/ski-interpreter.min.js` - minified version for browsers;
|
|
280
|
+
* `lib/ski-quest.min.js` - script with the interpreter
|
|
281
|
+
plus `QuestBox`, `QuestChapter`, and `QuestPage` classes
|
|
282
|
+
for building interactive quest pages from JSON-encoded quest data;
|
|
283
|
+
* `bin/ski.js` - a CLI REPL;
|
|
284
|
+
* `types` - TypeScript type definitions.
|
|
211
285
|
|
|
212
286
|
# Thanks
|
|
213
287
|
|
|
214
288
|
* [@ivanaxe](https://github.com/ivanaxe) for luring me into [icfpc 2011](http://icfpc2011.blogspot.com/2011/06/task-description-contest-starts-now.html) where I was introduced to combinators.
|
|
215
289
|
* [@akuklev](https://github.com/akuklev) for explaining functional programming to me so many times that I actually got some idea.
|
|
290
|
+
* [One happy fellow](https://github.com/happyfellow-one) whose [riddle](https://blog.happyfellow.dev/a-riddle/) trolled me into writing an early `traverse` prototype.
|
|
216
291
|
|
|
217
292
|
# Prior art and inspiration
|
|
218
293
|
|
package/bin/ski.js
CHANGED
|
@@ -1,121 +1,234 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --stack-size=20600
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs/promises');
|
|
4
|
+
const { Command } = require('commander');
|
|
4
5
|
|
|
5
6
|
const { SKI } = require('../lib/ski-interpreter.cjs');
|
|
7
|
+
const { Quest } = require('../src/quest.js');
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('ski')
|
|
13
|
+
.description('Simple Kombinator Interpreter - a combinatory logic & lambda calculus parser and interpreter')
|
|
14
|
+
.version('2.2.1');
|
|
15
|
+
|
|
16
|
+
// REPL subcommand
|
|
17
|
+
program
|
|
18
|
+
.command('repl')
|
|
19
|
+
.description('Start interactive REPL')
|
|
20
|
+
.option('--verbose', 'Show all evaluation steps')
|
|
21
|
+
.action((options) => {
|
|
22
|
+
startRepl(options.verbose);
|
|
23
|
+
});
|
|
6
24
|
|
|
7
|
-
|
|
25
|
+
// Eval subcommand
|
|
26
|
+
program
|
|
27
|
+
.command('eval <expression>')
|
|
28
|
+
.description('Evaluate a single expression')
|
|
29
|
+
.option('--verbose', 'Show all evaluation steps')
|
|
30
|
+
.action((expression, options) => {
|
|
31
|
+
evaluateExpression(expression, options.verbose);
|
|
32
|
+
});
|
|
8
33
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
34
|
+
// File subcommand
|
|
35
|
+
program
|
|
36
|
+
.command('file <filepath>')
|
|
37
|
+
.description('Evaluate expressions from a file')
|
|
38
|
+
.option('--verbose', 'Show all evaluation steps')
|
|
39
|
+
.action((filepath, options) => {
|
|
40
|
+
evaluateFile(filepath, options.verbose);
|
|
41
|
+
});
|
|
13
42
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
43
|
+
// Quest-check subcommand
|
|
44
|
+
program
|
|
45
|
+
.command('quest-check <files...>')
|
|
46
|
+
.description('Check quest files for validity')
|
|
47
|
+
.option('--solution <file>', 'Load solutions from file')
|
|
48
|
+
.action((files, options) => {
|
|
49
|
+
questCheck(files, options.solution);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Default to REPL if no command provided
|
|
53
|
+
program
|
|
54
|
+
.showHelpAfterError(true)
|
|
55
|
+
.parse(process.argv);
|
|
18
56
|
|
|
19
|
-
|
|
57
|
+
if (!process.argv.slice(2).length)
|
|
58
|
+
startRepl(false);
|
|
20
59
|
|
|
21
|
-
|
|
22
|
-
// interactive console
|
|
60
|
+
function startRepl (verbose) {
|
|
23
61
|
const readline = require('readline');
|
|
62
|
+
const ski = new SKI();
|
|
63
|
+
|
|
24
64
|
const rl = readline.createInterface({
|
|
25
65
|
input: process.stdin,
|
|
26
66
|
output: process.stdout,
|
|
27
67
|
prompt: '> ',
|
|
28
68
|
terminal: true,
|
|
29
69
|
});
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
|
|
71
|
+
console.log('Welcome to SKI interactive shell. Known combinators: ' + ski.showRestrict());
|
|
72
|
+
|
|
32
73
|
rl.on('line', str => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
74
|
+
if (str.match(/\S/)) {
|
|
75
|
+
if (str.startsWith('!'))
|
|
76
|
+
handleCommand(str, ski);
|
|
77
|
+
else {
|
|
78
|
+
processLine(str, ski, verbose, err => {
|
|
79
|
+
console.log('' + err);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
40
82
|
}
|
|
41
83
|
rl.prompt();
|
|
42
84
|
});
|
|
85
|
+
|
|
43
86
|
rl.once('close', () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
process.exit(0)
|
|
87
|
+
console.log('Bye, and may thy bird fly high!');
|
|
88
|
+
process.exit(0);
|
|
47
89
|
});
|
|
90
|
+
|
|
48
91
|
rl.prompt();
|
|
49
|
-
}
|
|
50
|
-
const prom = positional.length > 0
|
|
51
|
-
? fs.readFile(positional[0], 'utf8')
|
|
52
|
-
: Promise.resolve(options.e);
|
|
92
|
+
}
|
|
53
93
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
94
|
+
function evaluateExpression (expression, verbose) {
|
|
95
|
+
const ski = new SKI();
|
|
96
|
+
processLine(expression, ski, verbose, err => {
|
|
97
|
+
console.error('' + err);
|
|
98
|
+
process.exit(3);
|
|
57
99
|
});
|
|
58
100
|
}
|
|
59
101
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (state.final && expr instanceof SKI.classes.Alias)
|
|
74
|
-
ski.add(expr.name, state.expr);
|
|
75
|
-
}
|
|
76
|
-
return 0;
|
|
77
|
-
} catch (err) {
|
|
78
|
-
onErr(err);
|
|
79
|
-
return 1;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
102
|
+
function evaluateFile (filepath, verbose) {
|
|
103
|
+
const ski = new SKI();
|
|
104
|
+
fs.readFile(filepath, 'utf8')
|
|
105
|
+
.then(source => {
|
|
106
|
+
processLine(source, ski, verbose, err => {
|
|
107
|
+
console.error('' + err);
|
|
108
|
+
process.exit(3);
|
|
109
|
+
});
|
|
110
|
+
})
|
|
111
|
+
.catch(err => {
|
|
112
|
+
console.error('ski: ' + err);
|
|
113
|
+
process.exit(2);
|
|
114
|
+
});
|
|
82
115
|
}
|
|
83
116
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
'-t': () => { opt.t = true },
|
|
94
|
-
'-e': () => {
|
|
95
|
-
if (list.length < 1)
|
|
96
|
-
throw new Error('option -e requires an argument');
|
|
97
|
-
opt.e = list.shift();
|
|
98
|
-
},
|
|
99
|
-
};
|
|
117
|
+
function processLine (source, ski, verbose, onErr) {
|
|
118
|
+
if (!source.match(/\S/))
|
|
119
|
+
return; // nothing to see here
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const expr = ski.parse(source);
|
|
123
|
+
const t0 = new Date();
|
|
124
|
+
const isAlias = expr instanceof SKI.classes.Alias;
|
|
125
|
+
const aliasName = isAlias ? expr.name : null;
|
|
100
126
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
for (const state of expr.walk()) {
|
|
128
|
+
if (state.final)
|
|
129
|
+
console.log(`// ${state.steps} step(s) in ${new Date() - t0}ms`);
|
|
104
130
|
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
if (verbose || state.final)
|
|
132
|
+
console.log('' + state.expr.format());
|
|
107
133
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
continue;
|
|
134
|
+
if (state.final && isAlias && aliasName)
|
|
135
|
+
ski.add(aliasName, state.expr);
|
|
111
136
|
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
onErr(err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
112
141
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
async function questCheck (files, solutionFile) {
|
|
143
|
+
try {
|
|
144
|
+
// Load solutions if provided
|
|
145
|
+
let solutions = null;
|
|
146
|
+
if (solutionFile) {
|
|
147
|
+
const data = await fs.readFile(solutionFile, 'utf8');
|
|
148
|
+
solutions = JSON.parse(data);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Load and verify each quest file
|
|
152
|
+
let hasErrors = false;
|
|
153
|
+
const seenIds = new Set();
|
|
154
|
+
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
try {
|
|
157
|
+
const data = await fs.readFile(file, 'utf8');
|
|
158
|
+
const questData = JSON.parse(data);
|
|
159
|
+
|
|
160
|
+
// Handle both single quest objects and quest groups
|
|
161
|
+
const entry = Array.isArray(questData) ? { content: questData } : questData;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const group = new Quest.Group(entry);
|
|
165
|
+
|
|
166
|
+
// Verify the group
|
|
167
|
+
const findings = group.verify({
|
|
168
|
+
date: true,
|
|
169
|
+
solutions,
|
|
170
|
+
seen: seenIds
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Check for errors
|
|
174
|
+
const hasGroupErrors = Object.keys(findings).some(key => {
|
|
175
|
+
if (key === 'content') {
|
|
176
|
+
const contentErrors = findings.content?.filter(item => item !== null);
|
|
177
|
+
return contentErrors && contentErrors.length > 0;
|
|
178
|
+
}
|
|
179
|
+
return findings[key];
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (hasGroupErrors) {
|
|
183
|
+
hasErrors = true;
|
|
184
|
+
console.error(`Error in ${file}:`);
|
|
185
|
+
console.error(JSON.stringify(findings, null, 2));
|
|
186
|
+
} else
|
|
187
|
+
console.log(`✓ ${file}`);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
hasErrors = true;
|
|
190
|
+
console.error(`Error parsing quest group in ${file}:`, err.message);
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
hasErrors = true;
|
|
194
|
+
console.error(`Error reading file ${file}:`, err.message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
116
197
|
|
|
117
|
-
|
|
198
|
+
// Exit with appropriate code
|
|
199
|
+
process.exit(hasErrors ? 1 : 0);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.error('Error in quest-check:', err.message);
|
|
202
|
+
process.exit(2);
|
|
118
203
|
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function handleCommand (input, ski) {
|
|
207
|
+
const parts = input.trim().split(/\s+/);
|
|
208
|
+
const cmd = parts[0];
|
|
209
|
+
|
|
210
|
+
const dispatch = {
|
|
211
|
+
'!ls': () => {
|
|
212
|
+
const terms = ski.getTerms();
|
|
213
|
+
const list = Object.keys(terms).sort();
|
|
214
|
+
for (const name of list) {
|
|
215
|
+
const term = terms[name];
|
|
216
|
+
if (term instanceof SKI.classes.Alias)
|
|
217
|
+
console.log(` ${name} = ${term.impl}`);
|
|
218
|
+
else if (term instanceof SKI.classes.Native)
|
|
219
|
+
console.log(` ${name} ${term.props?.expr ?? '(native)'}`);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
'!help': () => {
|
|
223
|
+
console.log('Available commands:');
|
|
224
|
+
console.log(' !ls - List term inventory');
|
|
225
|
+
console.log(' !help - Show this help message');
|
|
226
|
+
},
|
|
227
|
+
'': () => {
|
|
228
|
+
console.log(`Unknown command: ${cmd}`);
|
|
229
|
+
console.log('Type !help for available commands.');
|
|
230
|
+
}
|
|
231
|
+
};
|
|
119
232
|
|
|
120
|
-
|
|
233
|
+
(dispatch[cmd] || dispatch[''])(...parts.slice(1));
|
|
121
234
|
}
|