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.
@@ -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
- WebAssembly build of [MotelyJAML](https://github.com/OptimusPi/MotelyJAML) the SIMD Balatro seed search engine with JAML filter support. Powers [seedfinder.app](https://seedfinder.app).
3
+ > Balatro seed search + per-seed analysis in the browser a vectorized SIMD engine compiled to WebAssembly.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/motely-wasm.svg)](https://www.npmjs.com/package/motely-wasm)
6
+ [![license](https://img.shields.io/npm/l/motely-wasm.svg)](./package.json)
7
+ [![types](https://img.shields.io/badge/types-included-blue.svg)](#subpath-exports--where-the-types-live)
8
+ [![WASM](https://img.shields.io/badge/runtime-embedded%20WASM-purple.svg)](#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
- ## Boot
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
- The WASM binary is **sideloaded** — a separate `dist/bin/motely-wasm.wasm`, **not** base64-embedded — so `boot()` **must** be told where to find it. A no-arg `boot()` will not load the runtime and every search silently does nothing.
26
+ ## Boot
14
27
 
15
- In Node, read the bytes and pass them directly (`fetch` can't read `file://` URLs):
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
- ```js
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
- const dist = resolve(dirname(fileURLToPath(import.meta.url)), "node_modules/motely-wasm/dist");
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
- In the browser, pass the root URL your host serves `dist/` at:
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
- ```js
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
- Guard re-boots with `bootsharp.getStatus() === bootsharp.BootStatus.Standby`.
40
+ ## Quick start run a search
35
41
 
36
- ## Imports
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
- ```js
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
- // EnumsMotelyDeck, MotelyStake, etc.
43
- import * as enums from "motely-wasm/dist/generated/modules/motely/enums.g.mjs";
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
- // Types MotelyProgress, IMotelySearch, MotelyScoredSeedResult, etc.
46
- import * as types from "motely-wasm/dist/generated/modules/motely.g.mjs";
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
- ## Quick start
71
+ ### Search entry points
50
72
 
51
- ```js
52
- // Optional [Import] hook — surface WASM-side errors. Bind BEFORE boot:
53
- // Bootsharp snapshots [Import] bindings at boot().
54
- Motely.reportWasmError = (msg) => console.error("[WASM]", msg);
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
- // Subscribe to events
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
- // Parse a JAML filter. JAML is YAML, and YAML is a JSON superset — a JSON
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
- // Run a search (blocks until complete run in a Worker for non-blocking UI).
72
- const search = Motely.runSequentialSearch(config);
73
- console.log(search.matchingSeeds, "matches out of", search.totalSeedsSearched);
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
- ## API
77
-
78
- ```js
79
- Motely.parseJaml(jaml) // string → JamlConfig (throws on invalid JAML)
80
- Motely.explainJaml(config) // human-readable plan summary
81
- Motely.createPlan(config) // JamlSearchPlan (tally columns, CSV header)
82
- Motely.jamlToJson(jaml) // JAML stringJSON string
83
- Motely.jsonToJaml(json) // JSON string → JAML string
84
- Motely.jamlyzer(seed, lens) // analyze ONE seed through a JamlConfig lens → JamlyzerSnapshot
85
- Motely.nativeFilterNames() // built-in native filter names
86
-
87
- Motely.runSequentialSearch(config, ...) // sequential seed search
88
- Motely.runRandomSearch(config, count) // random seed sample
89
- Motely.runSeedListSearch(config) // search only config.seeds
90
- Motely.runAestheticSearch(config, aesthetic) // aesthetic / scored search
91
- Motely.runNativeListSearch(name, seeds) // named native filter over a seed list
92
- Motely.runPassthroughListSearch(seeds) // no filter, just iterate seeds
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
- ```js
98
- Motely.onSeedMatch.subscribe(seed => {}) // string — each matching seed
99
- Motely.onScoredResult.subscribe(result => {}) // MotelyScoredSeedResult { seed, score, tallies }
100
- Motely.onProgress.subscribe(p => {}) // MotelyProgress
101
- Motely.onFileChanges.subscribe(changes => {}) // file system changes (browser OPFS)
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
- `Motely.pickRoot`, `mountRoot`, `unmountRoot`, `readTextFile`, `writeTextFile` use the browser File System Access API via `Bootsharp.FileSystem`. Initialize the JS extension before booting:
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
- ```js
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
- fs.init(Bootsharp.FileSystem.FileMounter);
113
- await bootsharp.boot("/motely-wasm/dist");
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
- ## Jimmolate
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
- Scalar predicate filtering after the base SIMD pass. Bind the predicate **before** boot (Bootsharp snapshots `[Import]` bindings at boot), then enable it:
184
+ ## Build from source
119
185
 
120
- ```js
121
- Motely.jimmolatePredicate = (result) => {
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);