@polycode-projects/seonix 0.10.0 → 0.10.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
@@ -357,6 +357,7 @@ template — see `templates/seonix.toml` and
357
357
  - `include_text`, `include_structure`, `respect_gitignore`, `markdown_sections`, `vue` — recognized and normalized, but **not yet wired to a consumer** (see "unwired keys" below).
358
358
  - **`[tune]`** — the locate/digest scoring knobs: `score_gap_k`, `literal_mention` (shipped default `true`), `demote_non_prod`, `call_adjacency`, `impl_of_interface`, `beam_search` (+ `beam_width`), `embed_rank`, `prose_layers`.
359
359
  - **`[tune.expansion]`** — graph-traversal expansion mode used by locate/the viewer's navigation: `strategy` (`none` | `beam` | `spiral` | `ppr`; `none` is the byte-identical shipped default), `nodes` (breadth/token-budget cap), `q` (proportion of most-distinctive neighbours followed per step), `depth` (hop radius).
360
+ - **`[interfaces]`** — HTTP route↔handler edges the AST graph can't see on its own: a `Route` node per endpoint, linked to its handler by a `serves` edge, and (JS/TS + tree-sitter C#) an HTTP client call linked to the route it targets by a `callsHttp` edge. Off by default; turn it on with `enabled = true` or `SEONIX_INTERFACES=1` (env wins). Tunable: `frameworks` (default `["aspnet", "express"]`), `verbs`, `combine_controller_prefix`, `prefix_strip`, `match_threshold`.
360
361
  - **`[telemetry]`** — `enabled = true` opts in (see [Telemetry](#telemetry) below).
361
362
 
362
363
  **Precedence, everywhere:** explicit CLI arg > `seonix.toml` value > shipped
@@ -427,11 +428,15 @@ seonix supplies the graph; tmct owns the conversation. The old opt-in
427
428
 
428
429
  Python (stdlib `ast`), JS/TS (the TypeScript compiler API, including CommonJS
429
430
  `require`/`module.exports`), C# (Roslyn, with a tree-sitter fallback) and Java
430
- (JavaParser, with a tree-sitter fallback), all merged into the one typed graph. The C# Roslyn helper's source ships in the package under `roslyn/`; build
431
- it on the consumer side (needs the .NET SDK) if you index C#. The Java JavaParser
432
- helper is a jar built from the repository's `java/` directory (`mvn package`) and
433
- is not included in the npm package, so a package install indexes Java with the
434
- tree-sitter fallback.
431
+ (JavaParser, with a tree-sitter fallback), all merged into the one typed graph.
432
+ The C# Roslyn helper's source ships in the package under `roslyn/`. If the .NET
433
+ SDK is on your PATH, seonix builds it the first time it indexes a C# repo, one
434
+ build, cached after that. No `dotnet` on PATH just means it uses the tree-sitter
435
+ fallback, silently, no error. Run `npm run build:roslyn` yourself first if you
436
+ want the dll ready ahead of time, e.g. in a Docker image build. The Java
437
+ JavaParser helper is a jar built from the repository's `java/` directory (`mvn
438
+ package`) and is not included in the npm package, so a package install indexes
439
+ Java with the tree-sitter fallback.
435
440
 
436
441
  ## Licence & safety
437
442
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/seonix",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SEONIX — a deterministic, offline, $0 code-graph tool that makes a cheap coding agent edit like an expensive one. Indexes a repo with Python ast + git (zero model calls) and renders a bounded edit-digest.",
@@ -35,6 +35,7 @@
35
35
  "src/",
36
36
  "roslyn/*.cs",
37
37
  "roslyn/*.csproj",
38
+ "scripts/build-roslyn.mjs",
38
39
  "README.md",
39
40
  "LICENSE"
40
41
  ],
@@ -45,6 +46,7 @@
45
46
  "infra"
46
47
  ],
47
48
  "scripts": {
49
+ "build:roslyn": "node scripts/build-roslyn.mjs",
48
50
  "setup": "node scripts/setup.mjs",
49
51
  "bench": "node bench/run.mjs",
50
52
  "bench:quick": "node bench/run.mjs --instances 1 --runs 1",
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // build-roslyn.mjs — explicit prewarm command (`npm run build:roslyn`) for the
3
+ // Roslyn C# semantic extractor. NOT run automatically on `npm install` — the
4
+ // dll lazy-builds on first real C# extraction attempt instead (src/cs_roslyn.mjs
5
+ // via src/roslyn_build.mjs), so consumers who never touch C# never pay a .NET
6
+ // build cost at install time. Use this command ahead of time when you want the
7
+ // dll baked in up front (e.g. a Docker image build step, or a CI cache warm)
8
+ // rather than paying the lazy-build cost on first use.
9
+ import { ensureRoslynBuilt } from "../src/roslyn_build.mjs";
10
+
11
+ await ensureRoslynBuilt();
package/src/cs_roslyn.mjs CHANGED
@@ -3,15 +3,21 @@
3
3
  // prints the {modules:[…]} contract; this wrapper just shells out and parses it.
4
4
  // Falls back is the caller's job (cs_treesitter) when dotnet/the tool is absent.
5
5
  import { spawn } from "node:child_process";
6
- import { dirname, join } from "node:path";
7
- import { fileURLToPath } from "node:url";
8
- import { access } from "node:fs/promises";
6
+ import { DLL as TOOL_DLL, ensureRoslynBuilt } from "./roslyn_build.mjs";
9
7
 
10
- const here = dirname(fileURLToPath(import.meta.url));
11
- const TOOL_DLL = join(here, "..", "roslyn", "publish", "roslyn-extract.dll");
8
+ // Cached across calls within a process: chooseCs() in extract_lang.mjs calls
9
+ // toolAvailable() once per ingestRepo() run, but tests and multi-repo indexing
10
+ // can call it repeatedly — avoid re-spawning `dotnet --version` every time.
11
+ // A fresh process (the normal case: one index run = one process) always
12
+ // re-checks, so a dotnet toolchain installed after a prior failed attempt is
13
+ // picked up automatically on the next run, no reinstall needed.
14
+ let cachedAvailable;
12
15
 
16
+ /** True if the Roslyn dll is present, lazily building it first (once per
17
+ * process) when it's missing and `dotnet` is on PATH. */
13
18
  export async function toolAvailable() {
14
- try { await access(TOOL_DLL); return true; } catch { return false; }
19
+ if (cachedAvailable === undefined) cachedAvailable = await ensureRoslynBuilt();
20
+ return cachedAvailable;
15
21
  }
16
22
 
17
23
  function run(cmd, args) {
@@ -26,7 +32,7 @@ function run(cmd, args) {
26
32
  }
27
33
 
28
34
  export async function ingest(root) {
29
- if (!(await toolAvailable())) throw new Error("roslyn-extract.dll not built (run dotnet publish in roslyn/)");
35
+ if (!(await toolAvailable())) throw new Error("roslyn-extract.dll not available (no dotnet on PATH, or the lazy build failed — run `npm run build:roslyn` to retry with warnings)");
30
36
  const { code, out, err } = await run("dotnet", [TOOL_DLL, root]);
31
37
  if (code !== 0) throw new Error(`roslyn-extract failed (exit ${code}): ${err.slice(-300)}`);
32
38
  let parsed;
@@ -0,0 +1,59 @@
1
+ // roslyn_build.mjs — shared build logic for the Roslyn C# semantic extractor
2
+ // (roslyn/publish/roslyn-extract.dll). Used by two callers: the lazy-build path
3
+ // in cs_roslyn.mjs (triggered on the first real C# extraction attempt in a
4
+ // process, not on `npm install`) and the explicit `npm run build:roslyn`
5
+ // prewarm command (e.g. baking the dll into a Docker image ahead of time).
6
+ //
7
+ // Best-effort and silent-by-default: no dotnet on PATH -> skip, tree-sitter
8
+ // (src/cs_treesitter.mjs) covers the absent case. A `dotnet publish` failure
9
+ // (e.g. no network for the first NuGet restore) is a warning, never thrown.
10
+ import { access } from "node:fs/promises";
11
+ import { spawn } from "node:child_process";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ export const ROOT = join(here, "..");
17
+ export const ROSLYN_DIR = join(ROOT, "roslyn");
18
+ export const DLL = join(ROSLYN_DIR, "publish", "roslyn-extract.dll");
19
+
20
+ function run(cmd, args, opts = {}) {
21
+ return new Promise((resolve) => {
22
+ const child = spawn(cmd, args, {
23
+ env: { ...process.env, DOTNET_CLI_TELEMETRY_OPTOUT: "1", DOTNET_NOLOGO: "1" },
24
+ ...opts,
25
+ });
26
+ let out = "";
27
+ let err = "";
28
+ child.stdout?.on("data", (d) => (out += d));
29
+ child.stderr?.on("data", (d) => (err += d));
30
+ child.on("close", (code) => resolve({ code: code ?? -1, out, err }));
31
+ child.on("error", () => resolve({ code: -1, out, err: "" }));
32
+ });
33
+ }
34
+
35
+ async function exists(p) {
36
+ try { await access(p); return true; } catch { return false; }
37
+ }
38
+
39
+ /** Build the Roslyn dll if it's missing and `dotnet` is on PATH.
40
+ * @returns {Promise<boolean>} true if the dll is present after this call
41
+ * (already built, or built now); false otherwise (no dotnet, or publish
42
+ * failed — caller falls back to tree-sitter). */
43
+ export async function ensureRoslynBuilt({ quiet = false } = {}) {
44
+ if (await exists(DLL)) return true;
45
+
46
+ const dotnetCheck = await run("dotnet", ["--version"]);
47
+ if (dotnetCheck.code !== 0) return false; // no .NET SDK — tree-sitter fallback covers C#
48
+
49
+ const publish = await run("dotnet", ["publish", "-c", "Release", "-o", "publish"], { cwd: ROSLYN_DIR });
50
+ if (publish.code !== 0) {
51
+ if (!quiet) {
52
+ console.warn("[seonix] Roslyn C# extractor build failed — falling back to tree-sitter for C#.");
53
+ console.warn(`[seonix] dotnet publish exited ${publish.code}: ${publish.err.slice(-300)}`);
54
+ }
55
+ return false;
56
+ }
57
+ if (!quiet) console.log("[seonix] Roslyn C# extractor built (full semantic call/type resolution enabled).");
58
+ return true;
59
+ }