motely-wasm 20.0.1 → 20.1.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/.smoke-test.mjs +58 -0
- package/README.md +156 -82
- package/__boot.html +14 -0
- package/cli.mjs +126 -0
- package/dist/dotnet/dotnet.native.js +1 -1
- package/dist/generated/imports.g.mjs +0 -2
- package/dist/generated/modules/bootsharp/file-system.g.d.mts +1 -1
- package/dist/generated/modules/bootsharp/file-system.g.mjs +2 -2
- package/dist/generated/modules/motely/enums.g.d.mts +1 -108
- package/dist/generated/modules/motely/enums.g.mjs +0 -206
- package/dist/generated/modules/motely/filters/jaml.g.d.mts +1 -11
- package/dist/generated/modules/motely/filters.g.d.mts +1 -1
- package/dist/generated/modules/motely/wasm.g.d.mts +5 -7
- package/dist/generated/modules/motely/wasm.g.mjs +0 -1
- package/dist/generated/modules/motely.g.d.mts +1 -14
- package/dist/generated/modules/motely.g.mjs +0 -18
- package/dist/generated/modules/system/threading.g.d.mts +2 -2
- package/dist/generated/resources.g.mjs +15 -1
- package/dist/generated/serializer.g.mjs +0 -143
- package/package.json +13 -3
- package/dist/bin/motely-wasm.wasm +0 -0
- package/dist/generated/modules/motely/analysis.g.d.mts +0 -39
- package/dist/generated/modules/motely/analysis.g.mjs +0 -6
package/.smoke-test.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Temporary v20 smoke test — boots the EMBEDDED build and runs a real bounded
|
|
2
|
+
// search end to end. Delete after use. Run: node .smoke-test.mjs
|
|
3
|
+
import bootsharp from "./dist/index.mjs";
|
|
4
|
+
import { Program } from "./dist/generated/modules/motely/wasm.g.mjs";
|
|
5
|
+
|
|
6
|
+
const t0 = Date.now();
|
|
7
|
+
const log = (m) => console.log(`[+${((Date.now() - t0) / 1000).toFixed(2)}s] ${m}`);
|
|
8
|
+
|
|
9
|
+
let progressTicks = 0;
|
|
10
|
+
let scored = 0;
|
|
11
|
+
let firstMatch = null;
|
|
12
|
+
|
|
13
|
+
Program.onProgress.subscribe(() => progressTicks++);
|
|
14
|
+
Program.onSeedMatch.subscribe((seed) => {
|
|
15
|
+
if (!firstMatch) firstMatch = seed;
|
|
16
|
+
});
|
|
17
|
+
Program.onScoredResult.subscribe((r) => {
|
|
18
|
+
scored++;
|
|
19
|
+
if (scored <= 3) log(` scored seed=${r.seed} score=${r.score} tallies=[${r.tallies}]`);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
log("booting embedded WASM (no args)...");
|
|
23
|
+
await bootsharp.boot();
|
|
24
|
+
log(`booted. status=${bootsharp.getStatus()} (expect 2=Booted)`);
|
|
25
|
+
|
|
26
|
+
// 1. JAML parse — throws on invalid.
|
|
27
|
+
const JAML = `must:
|
|
28
|
+
- joker: Blueprint
|
|
29
|
+
antes: [1, 2, 3, 4, 5, 6, 7, 8]
|
|
30
|
+
deck: Red
|
|
31
|
+
stake: White
|
|
32
|
+
`;
|
|
33
|
+
const config = Program.parseJaml(JAML);
|
|
34
|
+
log(`parseJaml OK: must=${config.must?.length ?? "?"} deck=${config.deck} stake=${config.stake}`);
|
|
35
|
+
|
|
36
|
+
// 2. JAML <-> JSON roundtrip.
|
|
37
|
+
const json = Program.jamlToJson(JAML);
|
|
38
|
+
const backToJaml = Program.jsonToJaml(json);
|
|
39
|
+
log(`jamlToJson OK (${json.length} chars), jsonToJaml OK (${backToJaml.length} chars)`);
|
|
40
|
+
|
|
41
|
+
// 3. A real bounded random search (small N — NOT a full sweep).
|
|
42
|
+
log("running runRandomSearch(config, 25000)...");
|
|
43
|
+
const search = Program.runRandomSearch(config, 25000);
|
|
44
|
+
log(
|
|
45
|
+
`search done: searched=${search.totalSeedsSearched} matches=${search.matchingSeeds} ` +
|
|
46
|
+
`completed=${search.isCompleted}`
|
|
47
|
+
);
|
|
48
|
+
log(`events fired: progress=${progressTicks} scored=${scored} firstMatch=${firstMatch ?? "(none)"}`);
|
|
49
|
+
|
|
50
|
+
// Verdict.
|
|
51
|
+
const ok =
|
|
52
|
+
bootsharp.getStatus() === 2 &&
|
|
53
|
+
config.deck === "Red" &&
|
|
54
|
+
json.length > 0 &&
|
|
55
|
+
search.isCompleted &&
|
|
56
|
+
search.totalSeedsSearched > 0n;
|
|
57
|
+
log(ok ? "✅ SMOKE TEST PASSED" : "❌ SMOKE TEST FAILED");
|
|
58
|
+
process.exit(ok ? 0 : 1);
|
package/README.md
CHANGED
|
@@ -1,126 +1,200 @@
|
|
|
1
1
|
# motely-wasm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Balatro seed search + per-seed analysis in the browser — a vectorized SIMD engine compiled to WebAssembly.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/motely-wasm)
|
|
6
|
+
[](./package.json)
|
|
7
|
+
[](#subpath-exports--where-the-types-live)
|
|
8
|
+
[](#boot)
|
|
9
|
+
|
|
10
|
+
The C# Motely seed engine, AOT/SIMD-compiled to WebAssembly via [Bootsharp](https://github.com/elringus/bootsharp). Author filters in **JAML** (Jimbo's Ante Markup Language), run real searches over Balatro's seed space, and analyze any single seed — all client-side, no server.
|
|
11
|
+
|
|
12
|
+
- **Embedded** — the `.wasm` is inlined into the module. `boot()` takes no args, nothing to copy or serve.
|
|
13
|
+
- **Typed** — full TypeScript declarations, emitted per C# namespace, served on subpath imports.
|
|
14
|
+
- **Streaming** — subscribe to progress/match/scored events, or filter live with a JS predicate (`jimmolate`).
|
|
4
15
|
|
|
5
16
|
## Install
|
|
6
17
|
|
|
7
18
|
```sh
|
|
8
19
|
npm install motely-wasm
|
|
20
|
+
# or
|
|
21
|
+
pnpm add motely-wasm
|
|
9
22
|
```
|
|
10
23
|
|
|
11
|
-
|
|
24
|
+
Requires a host with WASM + ES modules: modern browsers, Node 18+, Deno, Bun. For browser UIs, run searches in a **Web Worker** — every `run*` call blocks its thread until the search completes (WASM has no pthreads).
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
## Boot
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
The WASM binary is **embedded** — no files to copy, no path to serve, no `bin/` to wire up. `boot()` takes no arguments.
|
|
16
29
|
|
|
17
|
-
```
|
|
18
|
-
import { readFile } from "node:fs/promises";
|
|
19
|
-
import { dirname, resolve } from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
30
|
+
```ts
|
|
21
31
|
import bootsharp from "motely-wasm";
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
const wasm = await readFile(resolve(dist, "bin", bootsharp.manifest.wasm));
|
|
25
|
-
await bootsharp.boot({ wasm });
|
|
33
|
+
await bootsharp.boot(); // embedded: no args
|
|
26
34
|
```
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
> **`bootsharp` is the _default_ export.** `boot`, `exit`, `getStatus`, `BootStatus`, and `manifest` hang off it. Do **not** write `import * as bootsharp` — `boot` is not a named export and `bootsharp.boot` would be `undefined`.
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
await bootsharp.boot("/motely-wasm/dist");
|
|
32
|
-
```
|
|
38
|
+
> **`[Import]` bindings are snapshotted at `boot()`.** Assign `Program.jimmolatePredicate` (and any other `[Import]`) **before** calling `boot()`. You may reassign the predicate after boot to swap dispatch logic.
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
## Quick start — run a search
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
```ts
|
|
43
|
+
import bootsharp from "motely-wasm";
|
|
44
|
+
import { Program } from "motely-wasm/motely/wasm";
|
|
45
|
+
import type { MotelyScoredSeedResult, MotelyProgress } from "motely-wasm/motely";
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
// Main API — the C# Program class, renamed to Motely
|
|
40
|
-
import { Program as Motely } from "motely-wasm/dist/generated/modules/motely/wasm.g.mjs";
|
|
47
|
+
await bootsharp.boot();
|
|
41
48
|
|
|
42
|
-
//
|
|
43
|
-
|
|
49
|
+
// 1. Parse JAML — throws on invalid input.
|
|
50
|
+
const config = Program.parseJaml(`
|
|
51
|
+
must:
|
|
52
|
+
- joker: Blueprint
|
|
53
|
+
antes: [1, 2, 3, 4, 5, 6, 7, 8]
|
|
54
|
+
deck: Red
|
|
55
|
+
stake: White
|
|
56
|
+
`);
|
|
44
57
|
|
|
45
|
-
//
|
|
46
|
-
|
|
58
|
+
// 2. Stream results as the engine scores seeds.
|
|
59
|
+
Program.onProgress.subscribe((p: MotelyProgress) =>
|
|
60
|
+
console.log(`${p.percentComplete.toFixed(1)}% (${p.matchingSeeds} hits)`)
|
|
61
|
+
);
|
|
62
|
+
Program.onScoredResult.subscribe((r: MotelyScoredSeedResult) =>
|
|
63
|
+
console.log(`MATCH ${r.seed} score=${r.score}`)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// 3. Run. Blocks until done (call from a Web Worker for a live UI).
|
|
67
|
+
const search = Program.runRandomSearch(config, 100_000);
|
|
68
|
+
console.log(`done: ${search.matchingSeeds}/${search.totalSeedsSearched} seeds`);
|
|
47
69
|
```
|
|
48
70
|
|
|
49
|
-
|
|
71
|
+
### Search entry points
|
|
50
72
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
| Method | Searches |
|
|
74
|
+
|--------|----------|
|
|
75
|
+
| `runSequentialSearch(config, startBatch?, endBatch?, batchChars?, intervalMs?)` | The seed space in order, batch by batch |
|
|
76
|
+
| `runRandomSearch(config, count)` | `count` random seeds |
|
|
77
|
+
| `runSeedListSearch(config)` | The seeds listed in `config.seeds` |
|
|
78
|
+
| `runAestheticSearch(config, aesthetic)` | Seeds matching a `JamlAesthetic` lens |
|
|
79
|
+
| `runNativeListSearch(filterName, seeds)` | A list through a built-in native filter |
|
|
80
|
+
| `runPassthroughListSearch(seeds)` | A list with no filtering (decode/inspect) |
|
|
55
81
|
|
|
56
|
-
|
|
57
|
-
Motely.onSeedMatch.subscribe(seed => console.log("match:", seed));
|
|
58
|
-
Motely.onProgress.subscribe(p => console.log(`${p.percentComplete.toFixed(1)}%`));
|
|
82
|
+
Each returns an `IMotelySearch` whose counters (`matchingSeeds`, `totalSeedsSearched`, `isCompleted`, …) are ready to read on return.
|
|
59
83
|
|
|
60
|
-
|
|
61
|
-
// object string parses too, which is what makes it round-trip cleanly over MCP.
|
|
62
|
-
const config = Motely.parseJaml(`
|
|
63
|
-
name: WeeMonday
|
|
64
|
-
deck: Erratic
|
|
65
|
-
stake: Black
|
|
66
|
-
must:
|
|
67
|
-
- joker: WeeJoker
|
|
68
|
-
antes: [1]
|
|
69
|
-
`);
|
|
84
|
+
## Jimmolate — filter live with a JS predicate
|
|
70
85
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
`jimmolate` is the original Immolate `filter(seed) => keep?` model, in the browser. The C# engine does the SIMD work; your JS predicate decides which **scored** results survive. A seed reaches your handler only if the predicate keeps it.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import bootsharp from "motely-wasm";
|
|
90
|
+
import { Program } from "motely-wasm/motely/wasm";
|
|
91
|
+
import type { MotelyScoredSeedResult } from "motely-wasm/motely";
|
|
92
|
+
|
|
93
|
+
// Assign the predicate BEFORE boot (it's an [Import], snapshotted at boot).
|
|
94
|
+
Program.jimmolatePredicate = (r: MotelyScoredSeedResult) => r.score >= 20;
|
|
95
|
+
|
|
96
|
+
await bootsharp.boot();
|
|
97
|
+
|
|
98
|
+
Program.jimmolateEnabled = true; // gate on — without this, every scored seed is reported
|
|
99
|
+
Program.onScoredResult.subscribe((r) => keep(r)); // fires only for kept seeds
|
|
100
|
+
|
|
101
|
+
Program.runRandomSearch(Program.parseJaml(jaml), 1_000_000);
|
|
74
102
|
```
|
|
75
103
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
Leave `jimmolateEnabled` false to receive **every** scored seed (no filtering).
|
|
105
|
+
|
|
106
|
+
JAML ⇄ JSON conversion is available too: `Program.jamlToJson(jaml)` and `Program.jsonToJaml(json)`.
|
|
107
|
+
|
|
108
|
+
## Subpath exports — where the types live
|
|
109
|
+
|
|
110
|
+
Bootsharp emits one declaration file **per C# namespace** ([Bootsharp · Type Declarations](https://github.com/elringus/bootsharp) → "One `.g.d.mts` file is emitted per C# namespace, colocated with the matching `.g.mjs` binding"). Members in a namespace land on a matching **subpath**; the bare top-level import is intentionally empty. **Import types from the subpath, not the package root.**
|
|
111
|
+
|
|
112
|
+
| Import | Contents |
|
|
113
|
+
|--------|----------|
|
|
114
|
+
| `motely-wasm` | Default export: `boot()`, `exit()`, `getStatus()`, `BootStatus`, `manifest` |
|
|
115
|
+
| `motely-wasm/motely/wasm` | `Program` — the engine API (search, JAML parse, file I/O) |
|
|
116
|
+
| `motely-wasm/motely` | Core types: `MotelyScoredSeedResult`, `MotelyProgress`, `IMotelySearch`, `MotelyItem`, `MotelyMatchSource` |
|
|
117
|
+
| `motely-wasm/motely/enums` | Engine enums: `MotelyDeck`, `MotelyStake`, `MotelyVoucher`, `MotelyTag`, `MotelyBossBlind`, `MotelyBoosterPack`, joker enums, … |
|
|
118
|
+
| `motely-wasm/motely/filters/jaml` | `JamlConfig`, `JamlAesthetic` |
|
|
119
|
+
| `motely-wasm/motely/filters` | `JamlSearchPlan` |
|
|
120
|
+
| `motely-wasm/bootsharp/file-system` | `IFileSystem`, `IFileMounter`, `Change`, `MountOptions`, `PickOptions` |
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { Program } from "motely-wasm/motely/wasm";
|
|
124
|
+
import type { IMotelySearch, MotelyProgress, MotelyScoredSeedResult } from "motely-wasm/motely";
|
|
125
|
+
import { MotelyDeck, MotelyStake } from "motely-wasm/motely/enums";
|
|
126
|
+
import { JamlAesthetic } from "motely-wasm/motely/filters/jaml";
|
|
93
127
|
```
|
|
94
128
|
|
|
95
129
|
## Events
|
|
96
130
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
131
|
+
Subscribe before running a search.
|
|
132
|
+
|
|
133
|
+
| Event | Payload | Fires |
|
|
134
|
+
|-------|---------|-------|
|
|
135
|
+
| `Program.onProgress` | `MotelyProgress` | On the progress interval during a run |
|
|
136
|
+
| `Program.onSeedMatch` | `string` (seed) | When a seed matches the filter |
|
|
137
|
+
| `Program.onScoredResult` | `MotelyScoredSeedResult` | Per scored seed (post-`jimmolate` filter) |
|
|
138
|
+
| `Program.onFileChanges` | `Change[]` | When a mounted directory changes |
|
|
103
139
|
|
|
104
|
-
## File system (browser)
|
|
140
|
+
## File system (browser File System Access API)
|
|
105
141
|
|
|
106
|
-
|
|
142
|
+
The engine reads and writes JAML files from a user-picked directory via the browser File System Access API. This capability ships as a **separate peer package** — [`@rewaffle/bootsharp-file-system`](https://www.npmjs.com/package/@rewaffle/bootsharp-file-system), a sponsor-exclusive Bootsharp extension — because the WASM engine doesn't bundle the mounter itself. Install it and **wire the mounter before `boot()`**: `IFileMounter` is a Bootsharp `[Import]`, so calling `Program.pickRoot()` without `fs.init()` throws.
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
npm install @rewaffle/bootsharp-file-system # peer package
|
|
146
|
+
```
|
|
107
147
|
|
|
108
|
-
```
|
|
148
|
+
```ts
|
|
149
|
+
import bootsharp from "motely-wasm";
|
|
150
|
+
import { Program } from "motely-wasm/motely/wasm";
|
|
151
|
+
import { IFileMounter, PermissionMode } from "motely-wasm/bootsharp/file-system";
|
|
109
152
|
import * as fs from "@rewaffle/bootsharp-file-system";
|
|
110
|
-
import { Bootsharp } from "motely-wasm/dist/generated/modules/bootsharp/file-system.g.mjs";
|
|
111
153
|
|
|
112
|
-
|
|
113
|
-
|
|
154
|
+
// 1. Bind the mounter BEFORE boot (every [Import] is snapshotted at boot()).
|
|
155
|
+
fs.init(IFileMounter);
|
|
156
|
+
await bootsharp.boot();
|
|
157
|
+
|
|
158
|
+
// 2. Now the file APIs on Program work.
|
|
159
|
+
const root = await Program.pickRoot({ mode: PermissionMode.ReadWrite }); // null if cancelled
|
|
160
|
+
if (root) {
|
|
161
|
+
await Program.mountRoot(root, { mode: PermissionMode.ReadWrite });
|
|
162
|
+
const jaml = await Program.readTextFile(root, "filters/blueprint.jaml");
|
|
163
|
+
await Program.writeTextFile(root, "filters/out.jaml", updated);
|
|
164
|
+
await Program.unmountRoot(root);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The search / JAML / analysis APIs don't route through this — they run whether or not the mounter is wired. The directory APIs (`pickRoot`, `mountRoot`, `readTextFile`, `writeTextFile`) are what need it; unwired, they throw.
|
|
169
|
+
|
|
170
|
+
### Mobile browsers / single file
|
|
171
|
+
|
|
172
|
+
The directory mount relies on `showDirectoryPicker`, which iOS Safari and most mobile browsers **do not support** — and `Program` exposes only directory-*root* mounting (`pickRoot` / `mountRoot`), no single-file pick. So don't mount on mobile. You don't need to: `parseJaml`, `runRandomSearch`, etc. all take a JAML **string**. Read one file with a plain `<input type="file">` and feed its text straight in — works on every browser, no FS package required:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const file = input.files[0]; // <input type="file" accept=".jaml,.yaml">
|
|
176
|
+
const config = Program.parseJaml(await file.text());
|
|
177
|
+
const search = Program.runRandomSearch(config, 100_000);
|
|
114
178
|
```
|
|
115
179
|
|
|
116
|
-
##
|
|
180
|
+
## Bundler notes
|
|
181
|
+
|
|
182
|
+
The `browser` field in `package.json` stubs Node built-ins (`node:fs`, `node:url`, `node:path`, `node:module`, `node:crypto`, `node:process`) to `false`, so Vite/Webpack skip them cleanly. In library wrappers, **externalize** `motely-wasm` (and `motely-wasm/*`) so the consuming app controls WASM resolution.
|
|
117
183
|
|
|
118
|
-
|
|
184
|
+
## Build from source
|
|
119
185
|
|
|
120
|
-
```
|
|
121
|
-
Motely.
|
|
122
|
-
// result is MotelyScoredSeedResult — { seed, score, tallies }
|
|
123
|
-
return result.score > 0; // return true to keep the seed
|
|
124
|
-
};
|
|
125
|
-
Motely.jimmolateEnabled = true;
|
|
186
|
+
```sh
|
|
187
|
+
npm run build # dotnet publish ../Motely.Wasm/Motely.Wasm.csproj -c Release
|
|
126
188
|
```
|
|
189
|
+
|
|
190
|
+
Release builds use NativeAOT-LLVM (speed-optimized, trimmed). Output lands in `dist/`, which Bootsharp **regenerates wholesale on every publish** — never hand-edit anything under `dist/`.
|
|
191
|
+
|
|
192
|
+
## Maintainers
|
|
193
|
+
|
|
194
|
+
`package.json` is **hand-maintained and static** — edit it directly. Do **not** point `BootsharpPackageDirectory` at this directory: Bootsharp overwrites `package.json` on every build with a bare template (no version, types, or author), which breaks `npm publish`. The build parks that throwaway template in `obj/` (gitignored) instead. See the comment in `Motely.Wasm.csproj`.
|
|
195
|
+
|
|
196
|
+
Bump versions with `npm version patch` (never hand-edit the `version` field), then `npm publish`.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT © Nathanial P. Howard
|
package/__boot.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body><pre id="o">booting…</pre>
|
|
2
|
+
<script type="module">
|
|
3
|
+
const o=document.getElementById("o");
|
|
4
|
+
const S="name: t\ndeck: Red\nmust:\n - joker: Blueprint\n";
|
|
5
|
+
try{
|
|
6
|
+
const bs=(await import("./dist/index.mjs")).default;
|
|
7
|
+
const {Program}=await import("./dist/generated/modules/motely/wasm.g.mjs");
|
|
8
|
+
await bs.boot();
|
|
9
|
+
const json=Program.jamlToJson(S);
|
|
10
|
+
const cfg=Program.parseJaml(S);
|
|
11
|
+
window.__R={ok:true,json:json.slice(0,120),cfg:cfg!=null};
|
|
12
|
+
o.textContent="OK "+JSON.stringify(window.__R);
|
|
13
|
+
}catch(e){window.__R={ok:false,err:String(e&&e.stack||e)};o.textContent="FAIL "+window.__R.err;}
|
|
14
|
+
</script></body></html>
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// motely-wasm CLI — boot the embedded engine and DO something from the terminal.
|
|
3
|
+
// Doubles as the v20 end-to-end test nobody wrote. Exercises the full Program surface.
|
|
4
|
+
//
|
|
5
|
+
// node cli.mjs search <filter.jaml> --random <N> bounded random search
|
|
6
|
+
// node cli.mjs search <filter.jaml> --seeds A,B,C seed-list search
|
|
7
|
+
// node cli.mjs jimmolate <filter.jaml> --random <N> --min <s> JS predicate keeps score>=s
|
|
8
|
+
// node cli.mjs sequential <filter.jaml> [--start N] [--end M] ORDERED sweep (bounded!)
|
|
9
|
+
// node cli.mjs aesthetic <filter.jaml> <aestheticIndex> aesthetic-lens search of --seeds/--random
|
|
10
|
+
// node cli.mjs native [<name> --seeds A,B,C] list or run built-in native filters
|
|
11
|
+
// node cli.mjs explain <filter.jaml> print the search plan in English
|
|
12
|
+
// node cli.mjs convert <file.jaml> JAML -> JSON -> JAML roundtrip
|
|
13
|
+
//
|
|
14
|
+
import bootsharp from "./dist/index.mjs";
|
|
15
|
+
import { Program } from "./dist/generated/modules/motely/wasm.g.mjs";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
|
|
18
|
+
const t0 = Date.now();
|
|
19
|
+
const secs = () => ((Date.now() - t0) / 1000).toFixed(2);
|
|
20
|
+
const die = (m) => { console.error(`✗ ${m}`); process.exit(1); };
|
|
21
|
+
const readJaml = (p) => { try { return readFileSync(p, "utf8"); } catch { die(`cannot read JAML: ${p}`); } };
|
|
22
|
+
|
|
23
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
24
|
+
const flag = (name, def) => { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : def; };
|
|
25
|
+
const arg0 = () => (rest[0] && !rest[0].startsWith("--") ? rest[0] : null);
|
|
26
|
+
|
|
27
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
28
|
+
console.log(readFileSync(new URL(import.meta.url)).toString().split("\n").slice(1, 16).join("\n").replace(/^\/\/ ?/gm, ""));
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── jimmolate predicate MUST be wired before boot (Bootsharp [Import], snapshotted at boot). ──
|
|
33
|
+
let scoredTotal = 0, keptTotal = 0;
|
|
34
|
+
const MIN = Number(flag("--min", "0"));
|
|
35
|
+
if (cmd === "jimmolate") {
|
|
36
|
+
Program.jimmolatePredicate = (r) => {
|
|
37
|
+
scoredTotal++;
|
|
38
|
+
const keep = r.score >= MIN;
|
|
39
|
+
if (keep) keptTotal++;
|
|
40
|
+
return keep;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(`[+${secs()}s] booting embedded WASM (no args)…`);
|
|
45
|
+
await bootsharp.boot();
|
|
46
|
+
console.log(`[+${secs()}s] booted (status ${bootsharp.getStatus()}).`);
|
|
47
|
+
|
|
48
|
+
// ── commands that don't need a search ──
|
|
49
|
+
if (cmd === "native" && !arg0()) {
|
|
50
|
+
console.log("Built-in native filters:");
|
|
51
|
+
for (const n of Program.nativeFilterNames()) console.log(` • ${n}`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
if (cmd === "explain") {
|
|
55
|
+
const cfg = Program.parseJaml(readJaml(arg0() ?? die("usage: explain <filter.jaml>")));
|
|
56
|
+
console.log(`\n${Program.explainJaml(cfg) || "(no must/should/mustNot clauses to explain)"}`);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
if (cmd === "convert") {
|
|
60
|
+
const src = readJaml(arg0() ?? die("usage: convert <file.jaml>"));
|
|
61
|
+
const json = Program.jamlToJson(src);
|
|
62
|
+
const back = Program.jsonToJaml(json);
|
|
63
|
+
console.log(`JAML -> JSON (${json.length} chars):\n${json}\n\nJSON -> JAML (${back.length} chars):\n${back}`);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
// ── search-family commands ──
|
|
67
|
+
const wireStreams = () => {
|
|
68
|
+
let shown = 0;
|
|
69
|
+
Program.onScoredResult.subscribe((r) => {
|
|
70
|
+
if (shown++ < 15) console.log(` ★ ${r.seed} score=${r.score} tallies=[${r.tallies}]`);
|
|
71
|
+
});
|
|
72
|
+
Program.onProgress.subscribe((p) => {
|
|
73
|
+
process.stdout.write(`\r[+${secs()}s] ${p.percentComplete.toFixed(0)}% hits=${p.matchingSeeds} `);
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const report = (search) => {
|
|
77
|
+
console.log(`\n[+${secs()}s] done: searched=${search.totalSeedsSearched} matches=${search.matchingSeeds} completed=${search.isCompleted}`);
|
|
78
|
+
process.exit(search.isCompleted ? 0 : 1);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (cmd === "native") {
|
|
82
|
+
const seeds = (flag("--seeds") ?? die("usage: native <name> --seeds A,B,C")).split(",");
|
|
83
|
+
wireStreams();
|
|
84
|
+
report(Program.runNativeListSearch(arg0(), seeds));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cfgFile = arg0() ?? die(`usage: ${cmd} <filter.jaml> …`);
|
|
88
|
+
const config = Program.parseJaml(readJaml(cfgFile));
|
|
89
|
+
console.log(`[+${secs()}s] parsed: must=${config.must?.length ?? 0} should=${config.should?.length ?? 0} deck=${config.deck} stake=${config.stake}`);
|
|
90
|
+
wireStreams();
|
|
91
|
+
|
|
92
|
+
if (cmd === "jimmolate") {
|
|
93
|
+
Program.jimmolateEnabled = true;
|
|
94
|
+
console.log(`[+${secs()}s] jimmolate ON — predicate keeps score >= ${MIN}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let search;
|
|
98
|
+
if (cmd === "sequential") {
|
|
99
|
+
// SAFETY RAIL: never default to an unbounded sweep (the forbidden ~2.3T run).
|
|
100
|
+
const start = BigInt(flag("--start", "0"));
|
|
101
|
+
const end = BigInt(flag("--end", String(start + 1n))); // default: exactly one batch
|
|
102
|
+
if (end <= start) die("--end must be greater than --start");
|
|
103
|
+
if (end - start > 64n) die(`refusing ${end - start} batches — that's a huge sweep. Cap it (<=64 batches) for the CLI.`);
|
|
104
|
+
console.log(`[+${secs()}s] sequential sweep: batches [${start}, ${end}) …`);
|
|
105
|
+
search = Program.runSequentialSearch(config, start, end);
|
|
106
|
+
} else if (cmd === "aesthetic") {
|
|
107
|
+
const aesthetic = Number(rest[1] ?? die("usage: aesthetic <filter.jaml> <aestheticIndex> --seeds …/--random …"));
|
|
108
|
+
const seeds = flag("--seeds"); const random = flag("--random");
|
|
109
|
+
if (seeds) config.seeds = seeds.split(",");
|
|
110
|
+
// aesthetic search reads its seed source from the config/settings the same way;
|
|
111
|
+
// run it and let the engine drive.
|
|
112
|
+
search = Program.runAestheticSearch(config, aesthetic);
|
|
113
|
+
} else if (cmd === "search" || cmd === "jimmolate") {
|
|
114
|
+
const random = flag("--random"); const seeds = flag("--seeds");
|
|
115
|
+
search = random
|
|
116
|
+
? Program.runRandomSearch(config, Number(random))
|
|
117
|
+
: seeds
|
|
118
|
+
? (() => { config.seeds = seeds.split(","); return Program.runSeedListSearch(config); })()
|
|
119
|
+
: die("specify --random <N> or --seeds A,B,C");
|
|
120
|
+
} else {
|
|
121
|
+
die(`unknown command "${cmd}" — try --help`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (cmd === "jimmolate")
|
|
125
|
+
console.log(`\n[+${secs()}s] jimmolate: predicate saw ${scoredTotal} scored seeds, KEPT ${keptTotal} (score >= ${MIN}).`);
|
|
126
|
+
report(search);
|