@mrclrchtr/supi-tree-sitter 9.1.0 → 9.1.1

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
@@ -19,6 +19,8 @@ to access structural code-understanding workflows in pi.
19
19
  npm install @mrclrchtr/supi-tree-sitter
20
20
  ```
21
21
 
22
+ Structural analysis always runs in a Node.js child process. Under Bun, including compiled applications such as Pi-Bolt, install Node.js and make the `node` executable available on `PATH`. SuPi validates that executable before it starts the process host. If Node.js is unavailable, structural analysis reports an installation error. There is no Bun Worker or main-thread parser fallback.
23
+
22
24
  ![Tree-sitter outline in action](https://raw.githubusercontent.com/mrclrchtr/supi/main/screenshots/supi-tree-sitter.png)
23
25
 
24
26
  ## What you get
@@ -107,7 +109,7 @@ The benchmark reports cold and repeated outline results. It also reports cold an
107
109
 
108
110
  The Structural Worker reads files asynchronously. It identifies fresh parsed files by canonical path, grammar, and SHA-256 content hash. Cached trees use true least-recently-used eviction with limits of 128 files and 32 MiB of retained UTF-8 source bytes. Compiled queries use the same policy with limits of 128 queries and 512 KiB of retained UTF-8 query text. Source and query byte counts are memory-related proxies because `web-tree-sitter` does not report WASM resource sizes. Cached canonical trees stay private in the Worker.
109
111
 
110
- The Structural Worker runs in a dedicated Node child process. The process uses `--liftoff-only` so V8 avoids the optimizing WebAssembly compiler that uses unsafe amounts of memory for large grammars. This applies to every grammar in the Worker. Pi's main process does not receive this flag, and a V8 failure in the Worker process cannot crash Pi.
112
+ The Structural Worker runs in a dedicated Node child process. The process uses `--liftoff-only` so V8 avoids the optimizing WebAssembly compiler that uses unsafe amounts of memory for large grammars. This applies to every grammar in the Worker. Pi's main process does not receive this flag, and a V8 failure in the Worker process cannot crash Pi. A Node parent uses its current executable and advanced IPC. A Bun parent selects Node explicitly and uses JSON IPC with bounded base64 result chunks; the private Worker-thread protocol stays unchanged.
111
113
 
112
114
  All structural service operations accept optional `CodeRequestControl`. The parent converts cancellation to one shared atomic flag and forwards absolute deadlines. When present, it forwards only the opaque Debug Operation ID through the Worker protocol so parse and query timing events keep direct request ownership. Direct library calls have no ID. The process host creates a shared atomic flag for its Worker thread because Node IPC cannot share the parent flag. Worker read phases observe a local abort signal. Parser and query progress callbacks observe the flag and deadline. If cooperative interruption does not settle in the fixed 250 ms grace period, the parent terminates the Worker process. Valid queued work keeps FIFO order on one fresh Worker with cold caches. There is no main-thread parser fallback.
113
115
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "9.1.0",
3
+ "version": "9.1.1",
4
4
  "description": "Shared workspace context and capability contracts for code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "9.1.0",
3
+ "version": "9.1.1",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-tree-sitter",
3
- "version": "9.1.0",
3
+ "version": "9.1.1",
4
4
  "description": "Structural AST analysis for SuPi code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,8 +36,8 @@
36
36
  "dependencies": {
37
37
  "jiti": "^2.7.0",
38
38
  "web-tree-sitter": "^0.27.0",
39
- "@mrclrchtr/supi-code-runtime": "9.1.0",
40
- "@mrclrchtr/supi-core": "9.1.0"
39
+ "@mrclrchtr/supi-core": "9.1.1",
40
+ "@mrclrchtr/supi-code-runtime": "9.1.1"
41
41
  },
42
42
  "bundledDependencies": [
43
43
  "@mrclrchtr/supi-code-runtime",
@@ -0,0 +1,23 @@
1
+ import { assertStructuralProtocolMessageSize } from "./structural-worker-message-size.ts";
2
+ import { STRUCTURAL_WORKER_LIMITS } from "./structural-worker-protocol.ts";
3
+
4
+ /** Restore bounded binary chunks before the private Worker protocol validates them. */
5
+ export function decodeStructuralWorkerJsonMessage(message: unknown): unknown {
6
+ assertStructuralProtocolMessageSize(message);
7
+ if (!message || typeof message !== "object" || !("kind" in message) || message.kind !== "chunk") {
8
+ return message;
9
+ }
10
+ const payload = "payload" in message ? message.payload : undefined;
11
+ const maxLength = Math.ceil(STRUCTURAL_WORKER_LIMITS.targetChunkBytes / 3) * 4;
12
+ if (typeof payload !== "string" || payload.length > maxLength) {
13
+ throw new Error("Invalid Structural Worker JSON chunk payload");
14
+ }
15
+ const bytes = Buffer.from(payload, "base64");
16
+ if (
17
+ bytes.byteLength > STRUCTURAL_WORKER_LIMITS.targetChunkBytes ||
18
+ bytes.toString("base64") !== payload
19
+ ) {
20
+ throw new Error("Invalid Structural Worker JSON chunk payload");
21
+ }
22
+ return { ...message, payload: bytes };
23
+ }
@@ -0,0 +1,29 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { isAbsolute } from "node:path";
3
+
4
+ /** Select a real Node executable without re-entering a compiled Bun application. */
5
+ export function resolveStructuralWorkerNode(): string {
6
+ if (!process.versions.bun) return process.execPath;
7
+
8
+ const probe = spawnSync(
9
+ "node",
10
+ [
11
+ "--liftoff-only",
12
+ "--eval",
13
+ "if (!process.versions.bun) process.stdout.write(process.execPath)",
14
+ ],
15
+ {
16
+ encoding: "utf8",
17
+ env: { ...process.env, NODE_OPTIONS: "" },
18
+ timeout: 2_000,
19
+ maxBuffer: 4_096,
20
+ windowsHide: true,
21
+ },
22
+ );
23
+ if (probe.status !== 0 || !probe.stdout || !isAbsolute(probe.stdout)) {
24
+ throw new Error(
25
+ "Structural analysis under Bun requires Node.js on PATH. Install Node.js and restart Pi.",
26
+ );
27
+ }
28
+ return probe.stdout;
29
+ }
@@ -1,6 +1,8 @@
1
1
  import { type ChildProcess, fork } from "node:child_process";
2
2
  import { EventEmitter } from "node:events";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { decodeStructuralWorkerJsonMessage } from "./structural-worker-json.ts";
5
+ import { resolveStructuralWorkerNode } from "./structural-worker-node.ts";
4
6
  import type {
5
7
  ParentToStructuralWorkerMessage,
6
8
  StructuralWorkerToParentMessage,
@@ -23,23 +25,28 @@ export function createStructuralWorkerProcess(options: {
23
25
  readonly generation: number;
24
26
  }): StructuralWorkerLike {
25
27
  const hostPath = fileURLToPath(new URL("../worker/process-host.mjs", import.meta.url));
28
+ const jsonIpc = Boolean(process.versions.bun);
29
+ const args = [options.cwd, String(options.generation), ...(jsonIpc ? ["json"] : [])];
26
30
  // A child process accepts the V8 flag that a Worker thread rejects.
27
- const child = fork(hostPath, [options.cwd, String(options.generation)], {
31
+ const child = fork(hostPath, args, {
32
+ execPath: resolveStructuralWorkerNode(),
28
33
  execArgv: ["--liftoff-only"],
29
34
  env: { ...process.env, NODE_OPTIONS: "" },
30
- serialization: "advanced",
35
+ serialization: jsonIpc ? "json" : "advanced",
31
36
  stdio: ["ignore", "ignore", "inherit", "ipc"],
32
37
  windowsHide: true,
33
38
  });
34
- return new StructuralWorkerProcess(child);
39
+ return new StructuralWorkerProcess(child, jsonIpc);
35
40
  }
36
41
 
37
42
  class StructuralWorkerProcess implements StructuralWorkerLike {
38
43
  readonly #events = new EventEmitter();
39
44
  readonly #child: ChildProcess;
45
+ readonly #jsonIpc: boolean;
40
46
 
41
- constructor(child: ChildProcess) {
47
+ constructor(child: ChildProcess, jsonIpc: boolean) {
42
48
  this.#child = child;
49
+ this.#jsonIpc = jsonIpc;
43
50
  child.on("message", (value: unknown) => this.#receive(value));
44
51
  child.on("error", (error) => this.#events.emit("error", error));
45
52
  child.on("exit", (code) => this.#events.emit("exit", code ?? 1));
@@ -96,7 +103,16 @@ class StructuralWorkerProcess implements StructuralWorkerLike {
96
103
  this.#events.emit("error", new Error(value.message.message));
97
104
  return;
98
105
  }
99
- this.#events.emit("message", value.message);
106
+ let message: unknown = value.message;
107
+ if (this.#jsonIpc) {
108
+ try {
109
+ message = decodeStructuralWorkerJsonMessage(message);
110
+ } catch (error) {
111
+ this.#events.emit("error", error instanceof Error ? error : new Error(String(error)));
112
+ return;
113
+ }
114
+ }
115
+ this.#events.emit("message", message);
100
116
  }
101
117
  }
102
118
 
@@ -1,8 +1,13 @@
1
1
  import { Worker } from "node:worker_threads";
2
2
 
3
- const [cwd, generationValue] = process.argv.slice(2);
3
+ const [cwd, generationValue, serialization = "advanced"] = process.argv.slice(2);
4
4
  const generation = Number(generationValue);
5
- if (!cwd || !Number.isSafeInteger(generation) || generation < 1) {
5
+ if (
6
+ !cwd ||
7
+ !Number.isSafeInteger(generation) ||
8
+ generation < 1 ||
9
+ !["advanced", "json"].includes(serialization)
10
+ ) {
6
11
  throw new Error("Structural Worker process received invalid startup data");
7
12
  }
8
13
 
@@ -76,7 +81,19 @@ function isParentChannel(value) {
76
81
  }
77
82
 
78
83
  function send(message, callback) {
79
- if (process.connected && process.send) process.send(message, callback);
84
+ if (!process.connected || !process.send) return;
85
+ // Bun and Node use different binary IPC formats. Keep the thread protocol unchanged.
86
+ const forwarded =
87
+ serialization === "json" && message.message.kind === "chunk"
88
+ ? {
89
+ ...message,
90
+ message: {
91
+ ...message.message,
92
+ payload: Buffer.from(message.message.payload).toString("base64"),
93
+ },
94
+ }
95
+ : message;
96
+ process.send(forwarded, callback);
80
97
  }
81
98
 
82
99
  function disconnect() {