dsh-plugin-codegraph-tree-sitter 0.5.3 → 0.6.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/lib/index.js +82 -63
- package/lib/rust-engine.d.ts +49 -11
- package/lib/rust-engine.js +77 -11
- package/lib/schema.js +6 -1
- package/lib/walk.js +1 -1
- package/package.json +9 -2
package/lib/index.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* this package ships with.
|
|
28
28
|
* @module dsh-plugin-codegraph-tree-sitter
|
|
29
29
|
*/
|
|
30
|
-
import { readFileSync } from 'node:fs';
|
|
30
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
31
31
|
import { readdir } from 'node:fs/promises';
|
|
32
32
|
import { stat } from 'node:fs/promises';
|
|
33
33
|
import { join, posix } from 'node:path';
|
|
@@ -146,6 +146,10 @@ export function apply(ctx, config) {
|
|
|
146
146
|
// legitimately trigger another refresh. The map only keeps a burst of sessions opened at once from
|
|
147
147
|
// queueing the same rebuild repeatedly; the freshness decision itself reads the status sidecar.
|
|
148
148
|
const lastAutoRun = new Map();
|
|
149
|
+
// Roots whose background run is still executing. Kept apart from `lastAutoRun`, which is a time
|
|
150
|
+
// window: a first build can outlive the window, and two builds on one root would only race the same
|
|
151
|
+
// index files.
|
|
152
|
+
const autoIndexInFlight = new Set();
|
|
149
153
|
// `agent/created` is typed by `@deepseek-ai/dsh-agent`'s module augmentation. Reading the event
|
|
150
154
|
// through a local structural face keeps this package's host dependencies unchanged: it needs one
|
|
151
155
|
// event name and one payload shape, not the whole agent package.
|
|
@@ -210,63 +214,88 @@ export function apply(ctx, config) {
|
|
|
210
214
|
if (resolved.autoIndexOnSession) {
|
|
211
215
|
// Registered inside this effect so the listener follows the plugin's lifetime: unloading the
|
|
212
216
|
// plugin must stop new sessions from scheduling work this instance can no longer run.
|
|
213
|
-
yield agentEvents.on('agent/created', payload => autoIndexOnSession(ctx, resolved, lastAutoRun, payload));
|
|
217
|
+
yield agentEvents.on('agent/created', payload => autoIndexOnSession(ctx, resolved, lastAutoRun, autoIndexInFlight, payload, ensureWatching));
|
|
214
218
|
}
|
|
215
219
|
yield ctx.codegraph.registerIndexer(indexer);
|
|
216
220
|
}, 'codegraph-tree-sitter');
|
|
217
221
|
}
|
|
218
|
-
/**
|
|
219
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Whether a directory is the harness's own home rather than a project checkout.
|
|
224
|
+
*
|
|
225
|
+
* `sessions/` and `profiles/` together are what makes it one: either alone is a plausible folder
|
|
226
|
+
* name inside an ordinary repository, and requiring both keeps a real workspace eligible. The check
|
|
227
|
+
* exists because a DSH home is by construction tens of thousands of files — indexing it can only
|
|
228
|
+
* ever hit the session-start size guard, so queueing the job produces a failure and nothing else.
|
|
229
|
+
* @param root - the absolute workspace path.
|
|
230
|
+
* @returns true when the directory holds the harness's own state.
|
|
231
|
+
*/
|
|
232
|
+
function isHarnessHome(root) {
|
|
233
|
+
return existsSync(join(root, 'sessions')) && existsSync(join(root, 'profiles'));
|
|
234
|
+
}
|
|
220
235
|
/**
|
|
221
236
|
* Index a new session's workspace in the background — when nothing has indexed it yet, or when what
|
|
222
237
|
* is there has gone stale.
|
|
223
238
|
*
|
|
224
239
|
* Runs synchronously inside `agent/created` dispatch, so it must never block or throw: it only decides
|
|
225
|
-
* whether a build is warranted and
|
|
226
|
-
*
|
|
227
|
-
*
|
|
240
|
+
* whether a build is warranted and starts one in the background. A synchronous build here would stall
|
|
241
|
+
* agent creation for the length of a parse.
|
|
242
|
+
*
|
|
243
|
+
* The run deliberately does **not** go through the host's job registry. A registered job reports its
|
|
244
|
+
* settlement back to the owning agent: the host injects a completion notice into the next step and,
|
|
245
|
+
* under its default `wakeup` delivery, opens a turn on an idle one. An automatic index nobody asked
|
|
246
|
+
* for must not appear in the transcript or start a reply of its own, so this runs as a plain
|
|
247
|
+
* background promise. The explicit `codegraph_index` tool still registers a job — there a caller is
|
|
248
|
+
* waiting for the id.
|
|
228
249
|
*
|
|
229
250
|
* Refreshing a stale index is what restarts the file watcher after a host restart: watching only
|
|
230
251
|
* begins once a successful index has established a baseline, so a workspace whose index is minutes
|
|
231
252
|
* old would otherwise stay frozen at whatever the last process left behind.
|
|
232
|
-
* @param ctx - the plugin context
|
|
253
|
+
* @param ctx - the plugin context.
|
|
233
254
|
* @param config - the resolved plugin configuration, for the staleness threshold.
|
|
234
255
|
* @param lastAutoRun - when this instance last started an automatic index for each root.
|
|
256
|
+
* @param autoIndexInFlight - roots whose background run is still executing, so a slow build cannot be
|
|
257
|
+
* started twice by sessions that open past the freshness window.
|
|
235
258
|
* @param payload - the `agent/created` dispatch payload.
|
|
259
|
+
* @param ensureWatching - starts file watching for a workspace that is already current.
|
|
236
260
|
*/
|
|
237
|
-
function autoIndexOnSession(ctx, config, lastAutoRun, payload) {
|
|
261
|
+
function autoIndexOnSession(ctx, config, lastAutoRun, autoIndexInFlight, payload, ensureWatching) {
|
|
238
262
|
try {
|
|
239
263
|
const agent = payload.agent;
|
|
240
264
|
const root = agent?.session?.header?.cwd;
|
|
241
265
|
// No workspace means no root to claim: an agent created without a `cwd` has nothing to index.
|
|
242
266
|
if (agent === undefined || !root)
|
|
243
267
|
return;
|
|
268
|
+
// DSH's own home directory is not a code repository — `sessions/`, `attachments/`, the index
|
|
269
|
+
// databases and the installed plugins below it are a program's state. It is also tens of
|
|
270
|
+
// thousands of files, so every session opened there queued a job whose only possible outcome was
|
|
271
|
+
// the size guard failing. Skipping here keeps the job list honest: what is not a candidate never
|
|
272
|
+
// becomes a failure the user cannot act on.
|
|
273
|
+
if (isHarnessHome(root))
|
|
274
|
+
return;
|
|
244
275
|
const now = Date.now();
|
|
245
276
|
// A burst of sessions opened on one workspace must not queue the same rebuild repeatedly.
|
|
246
277
|
const previous = lastAutoRun.get(root);
|
|
247
278
|
if (previous !== undefined && now - previous < config.autoRefreshAfterMs)
|
|
248
279
|
return;
|
|
280
|
+
// A build already running for this root is the one the window would have deduplicated had it been
|
|
281
|
+
// slow enough to straddle it; starting a second would only race the same index files.
|
|
282
|
+
if (autoIndexInFlight.has(root))
|
|
283
|
+
return;
|
|
249
284
|
// A fresh sidecar means a recent run is already describing this workspace; an absent, unreadable
|
|
250
|
-
// or old one means the graph is missing or behind.
|
|
251
|
-
// without background jobs never pays for the probe.
|
|
285
|
+
// or old one means the graph is missing or behind.
|
|
252
286
|
const indexedAt = readStatusIndexedAt(root);
|
|
253
|
-
if (indexedAt !== undefined && now - indexedAt < config.autoRefreshAfterMs)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
287
|
+
if (indexedAt !== undefined && now - indexedAt < config.autoRefreshAfterMs) {
|
|
288
|
+
// The graph is current, so no build is warranted — but watching is process-local state and
|
|
289
|
+
// this is the first time this process has seen this workspace. Without this call, a restart
|
|
290
|
+
// followed by a session in an up-to-date workspace would leave it unwatched until the sidecar
|
|
291
|
+
// aged out, and every edit until then would silently not reach the graph.
|
|
292
|
+
ensureWatching(root);
|
|
259
293
|
return;
|
|
260
|
-
|
|
261
|
-
// The registry keys a job by its owning session id, not the agent object.
|
|
262
|
-
owner: agent.id,
|
|
263
|
-
kind: AUTO_INDEX_JOB_KIND,
|
|
264
|
-
label: `codegraph auto index ${root}`,
|
|
265
|
-
run: handle => runAutoIndexJob(ctx, root, config, handle),
|
|
266
|
-
});
|
|
267
|
-
// Marked only after the registry accepted the job, so a transient refusal leaves the workspace
|
|
268
|
-
// eligible for the next session that opens in it.
|
|
294
|
+
}
|
|
269
295
|
lastAutoRun.set(root, now);
|
|
296
|
+
autoIndexInFlight.add(root);
|
|
297
|
+
// Never rejects: a lost index is a degradation, not an error the session can act on.
|
|
298
|
+
void runAutomaticIndex(ctx, root, config).finally(() => autoIndexInFlight.delete(root));
|
|
270
299
|
}
|
|
271
300
|
catch (error) {
|
|
272
301
|
// The host treats a throwing listener as vetoing publication. Losing an index is a degradation,
|
|
@@ -274,6 +303,33 @@ function autoIndexOnSession(ctx, config, lastAutoRun, payload) {
|
|
|
274
303
|
console.error(`codegraph-tree-sitter: automatic indexing for a new session failed: ${String(error)}`);
|
|
275
304
|
}
|
|
276
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Run one automatic index to completion, reporting a failure instead of propagating it.
|
|
308
|
+
*
|
|
309
|
+
* The size guard stays: parsing a workspace past `autoIndexMaxFiles` would exhaust the WASM heap and
|
|
310
|
+
* take the desktop down with it, and an automatic index is a convenience. Skipping it is a decision,
|
|
311
|
+
* not a failure, so it is logged once and the session continues untouched.
|
|
312
|
+
* @param ctx - the plugin context.
|
|
313
|
+
* @param root - absolute workspace path.
|
|
314
|
+
* @param config - the resolved plugin configuration.
|
|
315
|
+
*/
|
|
316
|
+
async function runAutomaticIndex(ctx, root, config) {
|
|
317
|
+
const controller = new AbortController();
|
|
318
|
+
try {
|
|
319
|
+
const oversized = await exceedsAutoIndexLimit(root, config, controller.signal);
|
|
320
|
+
if (oversized !== undefined) {
|
|
321
|
+
console.error(`codegraph-tree-sitter: skipped automatic indexing of ${root}: it holds at least ${String(oversized)} ` +
|
|
322
|
+
`candidate source files, past the ${String(config.autoIndexMaxFiles)}-file session-start limit. ` +
|
|
323
|
+
'Run codegraph_index yourself if you want a graph for it.');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// A successful index is what starts file watching, inside `indexer.index`.
|
|
327
|
+
await ctx.codegraph.index(root, controller.signal);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
console.error(`codegraph-tree-sitter: automatic index for ${root} failed: ${String(error)}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
277
333
|
/**
|
|
278
334
|
* How many candidate source files `root` holds, when that is past the session-start limit.
|
|
279
335
|
*
|
|
@@ -339,43 +395,6 @@ function readStatusIndexedAt(root) {
|
|
|
339
395
|
return undefined;
|
|
340
396
|
}
|
|
341
397
|
}
|
|
342
|
-
/**
|
|
343
|
-
* The producer body of one automatic indexing job.
|
|
344
|
-
*
|
|
345
|
-
* Both halves of the host's job contract are mandatory: `cancel` aborts the run, and `done` settles
|
|
346
|
-
* even on failure — a rejected `done` breaks the registry's bookkeeping and leaves the job looking
|
|
347
|
-
* stuck forever.
|
|
348
|
-
* @param ctx - the plugin context.
|
|
349
|
-
* @param root - the workspace to index.
|
|
350
|
-
* @param handle - the job's producer face, for the job's live log.
|
|
351
|
-
* @returns the job's cancellation hook and settlement promise.
|
|
352
|
-
*/
|
|
353
|
-
function runAutoIndexJob(ctx, root, config, handle) {
|
|
354
|
-
const controller = new AbortController();
|
|
355
|
-
const done = (async () => {
|
|
356
|
-
try {
|
|
357
|
-
const oversized = await exceedsAutoIndexLimit(root, config, controller.signal);
|
|
358
|
-
if (oversized !== undefined) {
|
|
359
|
-
// Declining here is the whole point of the check: parsing this workspace would exhaust the
|
|
360
|
-
// WASM heap and take the desktop down with it, and an automatic index is a convenience.
|
|
361
|
-
handle.append(`Skipped automatic indexing of ${root}: it holds at least ${String(oversized)} candidate ` +
|
|
362
|
-
`source files, past the ${String(config.autoIndexMaxFiles)}-file session-start limit. ` +
|
|
363
|
-
`Run codegraph_index yourself if you want a graph for it.\n`);
|
|
364
|
-
return { status: 'failed', detail: 'workspace too large for automatic indexing' };
|
|
365
|
-
}
|
|
366
|
-
const report = await ctx.codegraph.index(root, controller.signal);
|
|
367
|
-
handle.append(`Indexed ${root}: ${report.filesIndexed} files, ${report.nodeCount} symbols, ` +
|
|
368
|
-
`${report.edgeCount} relationships.\n`);
|
|
369
|
-
return { status: 'completed' };
|
|
370
|
-
}
|
|
371
|
-
catch (error) {
|
|
372
|
-
const message = String(error);
|
|
373
|
-
handle.append(`codegraph auto index failed for ${root}: ${message}\n`);
|
|
374
|
-
return { status: 'failed', detail: message };
|
|
375
|
-
}
|
|
376
|
-
})();
|
|
377
|
-
return { cancel: () => controller.abort(), done };
|
|
378
|
-
}
|
|
379
398
|
/**
|
|
380
399
|
* Run one watch-triggered reindex. Always a full rebuild in this version — `runIndex` has no notion of
|
|
381
400
|
* a file subset — so the watcher's own collected paths are not consulted here.
|
package/lib/rust-engine.d.ts
CHANGED
|
@@ -18,9 +18,9 @@ import type { FileExtraction } from './extract.ts';
|
|
|
18
18
|
export declare const RUST_LANGUAGES: readonly string[];
|
|
19
19
|
/**
|
|
20
20
|
* Files handed to one addon call. The addon parses a batch across every core it can, so a large batch
|
|
21
|
-
* is what makes it fast
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* is what makes it fast. The call runs on libuv's thread pool (see {@link RustEngine}), so a batch no
|
|
22
|
+
* longer stalls the event loop; the bound now keeps one promise's lifetime — and the file text it
|
|
23
|
+
* holds in flight — reasonable, instead of capping a blocking call.
|
|
24
24
|
*/
|
|
25
25
|
export declare const RUST_BATCH_SIZE = 200;
|
|
26
26
|
/** One file as the addon takes it. */
|
|
@@ -39,11 +39,16 @@ export interface RustExtractInput {
|
|
|
39
39
|
export interface RustEngine {
|
|
40
40
|
/**
|
|
41
41
|
* Extract definitions, calls, imports, and heritage for a batch of files.
|
|
42
|
+
*
|
|
43
|
+
* Answers a promise because the addon's body runs on libuv's thread pool: a synchronous native call
|
|
44
|
+
* blocked the host's event loop for the whole batch, which is what froze the desktop when automatic
|
|
45
|
+
* indexing ran inside the renderer. Callers `await` it; an older addon that still returns a string
|
|
46
|
+
* keeps working, because `await` on a non-promise is the value itself.
|
|
42
47
|
* @param inputs - the files to extract; inputs in an unsupported language are silently skipped.
|
|
43
48
|
* @param rulesJson - the `LanguageSpec` table, as JSON.
|
|
44
|
-
* @returns
|
|
49
|
+
* @returns a promise of JSON `{ results: [{ path, extraction }], supported: string[] }`.
|
|
45
50
|
*/
|
|
46
|
-
extractWorkspace(inputs: RustExtractInput[], rulesJson: string): string
|
|
51
|
+
extractWorkspace(inputs: RustExtractInput[], rulesJson: string): Promise<string>;
|
|
47
52
|
}
|
|
48
53
|
/** Options for {@link loadRustEngine}. */
|
|
49
54
|
export interface LoadRustEngineOptions {
|
|
@@ -53,14 +58,47 @@ export interface LoadRustEngineOptions {
|
|
|
53
58
|
*/
|
|
54
59
|
readonly modulePath?: string;
|
|
55
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Where the native package for this platform lives, when npm installed one.
|
|
63
|
+
*
|
|
64
|
+
* The addon ships as one optional dependency per platform (`optionalDependencies` in this package's
|
|
65
|
+
* manifest), so a `pnpm add` pulls exactly one binary — the one the running machine can `dlopen` —
|
|
66
|
+
* instead of every platform's. npm's `os`/`cpu` fields do the selection; a platform this project does
|
|
67
|
+
* not build for simply never installs one and the walk falls back to JS.
|
|
68
|
+
* @param platform - `process.platform`.
|
|
69
|
+
* @param arch - `process.arch`.
|
|
70
|
+
* @returns the package name, or `undefined` for a platform with no published build.
|
|
71
|
+
*/
|
|
72
|
+
export declare function nativePackageName(platform: string, arch: string): string | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Resolve this platform's optional native package to its `.node` entry, if it is installed.
|
|
75
|
+
*
|
|
76
|
+
* `require.resolve` is the only thing that knows where a dependency actually landed — pnpm's store,
|
|
77
|
+
* npm's flat tree, a hoisted profile — so the path is asked for rather than constructed. A missing
|
|
78
|
+
* package, or one whose `main` does not resolve, is `undefined`: that is the documented "no addon"
|
|
79
|
+
* answer, not an error.
|
|
80
|
+
* @param platform - `process.platform`.
|
|
81
|
+
* @param arch - `process.arch`.
|
|
82
|
+
* @param resolve - module resolution, injectable so a test does not depend on what this machine has
|
|
83
|
+
* installed.
|
|
84
|
+
* @returns the absolute path to the addon, or `undefined`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function nativePackageModulePath(platform: string, arch: string, resolve?: (name: string) => string): string | undefined;
|
|
56
87
|
/**
|
|
57
88
|
* Every place the addon may live, most specific first.
|
|
58
89
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* and
|
|
63
|
-
* installed or checked out.
|
|
90
|
+
* Three layouts matter: a package that ships the binary puts it in `artifacts/` beside `lib/` (the
|
|
91
|
+
* shape an older hand-copied install has), a repository checkout builds it under
|
|
92
|
+
* `crates/indexer-rust/artifacts/` (three directories up from both `src/` and `lib/`, which sit at the
|
|
93
|
+
* same depth), and an npm install carries it in the platform's optional dependency. Trying all three
|
|
94
|
+
* means one module works whether it was installed, copied in, or checked out.
|
|
95
|
+
* @param base - the importing module's URL; parameterized so a test can point at a scratch tree.
|
|
96
|
+
* @param fromPackage - this platform's optional-dependency path, or `undefined` when there is none.
|
|
97
|
+
* @returns absolute paths to try, in order.
|
|
98
|
+
*/
|
|
99
|
+
export declare function modulePathsFor(base: string, fromPackage: string | undefined): string[];
|
|
100
|
+
/**
|
|
101
|
+
* The paths production should try, with this platform's optional package worked out for it.
|
|
64
102
|
* @param base - the importing module's URL; parameterized so a test can point at a scratch tree.
|
|
65
103
|
* @returns absolute paths to try, in order.
|
|
66
104
|
*/
|
|
@@ -89,7 +127,7 @@ export declare function loadRustEngine(options?: LoadRustEngineOptions): RustEng
|
|
|
89
127
|
* @returns one extraction per path the addon answered for, or `undefined` when no answer can be
|
|
90
128
|
* trusted at all — no addon, a thrown call, or JSON that is not the documented shape.
|
|
91
129
|
*/
|
|
92
|
-
export declare function extractWithRust(files: readonly RustExtractInput[], engine: RustEngine | undefined): Map<string, FileExtraction> | undefined
|
|
130
|
+
export declare function extractWithRust(files: readonly RustExtractInput[], engine: RustEngine | undefined): Promise<Map<string, FileExtraction> | undefined>;
|
|
93
131
|
/**
|
|
94
132
|
* Install a stand-in for the addon the walk resolves, or restore the real one.
|
|
95
133
|
*
|
package/lib/rust-engine.js
CHANGED
|
@@ -20,30 +20,93 @@ import { LANGUAGE_TABLE } from "./languages.js";
|
|
|
20
20
|
export const RUST_LANGUAGES = ['typescript', 'tsx', 'javascript', 'jsx'];
|
|
21
21
|
/**
|
|
22
22
|
* Files handed to one addon call. The addon parses a batch across every core it can, so a large batch
|
|
23
|
-
* is what makes it fast
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* is what makes it fast. The call runs on libuv's thread pool (see {@link RustEngine}), so a batch no
|
|
24
|
+
* longer stalls the event loop; the bound now keeps one promise's lifetime — and the file text it
|
|
25
|
+
* holds in flight — reasonable, instead of capping a blocking call.
|
|
26
26
|
*/
|
|
27
27
|
export const RUST_BATCH_SIZE = 200;
|
|
28
28
|
const require = createRequire(import.meta.url);
|
|
29
|
+
/**
|
|
30
|
+
* Where the native package for this platform lives, when npm installed one.
|
|
31
|
+
*
|
|
32
|
+
* The addon ships as one optional dependency per platform (`optionalDependencies` in this package's
|
|
33
|
+
* manifest), so a `pnpm add` pulls exactly one binary — the one the running machine can `dlopen` —
|
|
34
|
+
* instead of every platform's. npm's `os`/`cpu` fields do the selection; a platform this project does
|
|
35
|
+
* not build for simply never installs one and the walk falls back to JS.
|
|
36
|
+
* @param platform - `process.platform`.
|
|
37
|
+
* @param arch - `process.arch`.
|
|
38
|
+
* @returns the package name, or `undefined` for a platform with no published build.
|
|
39
|
+
*/
|
|
40
|
+
export function nativePackageName(platform, arch) {
|
|
41
|
+
const suffix = NATIVE_PACKAGES[`${platform}-${arch}`];
|
|
42
|
+
return suffix === undefined ? undefined : `dsh-plugin-codegraph-indexer-${suffix}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The suffix each published platform package carries, keyed by `platform-arch`.
|
|
46
|
+
*
|
|
47
|
+
* A suffix is not mechanical: the Linux builds are `-gnu` because a musl one would have to be a
|
|
48
|
+
* separate artifact, while Windows spells its ABI out. Keeping the table explicit is what stops this
|
|
49
|
+
* from silently computing a package name nobody publishes.
|
|
50
|
+
*/
|
|
51
|
+
const NATIVE_PACKAGES = {
|
|
52
|
+
'darwin-arm64': 'darwin-arm64',
|
|
53
|
+
'darwin-x64': 'darwin-x64',
|
|
54
|
+
'linux-x64': 'linux-x64-gnu',
|
|
55
|
+
'linux-arm64': 'linux-arm64-gnu',
|
|
56
|
+
'win32-x64': 'win32-x64-msvc',
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Resolve this platform's optional native package to its `.node` entry, if it is installed.
|
|
60
|
+
*
|
|
61
|
+
* `require.resolve` is the only thing that knows where a dependency actually landed — pnpm's store,
|
|
62
|
+
* npm's flat tree, a hoisted profile — so the path is asked for rather than constructed. A missing
|
|
63
|
+
* package, or one whose `main` does not resolve, is `undefined`: that is the documented "no addon"
|
|
64
|
+
* answer, not an error.
|
|
65
|
+
* @param platform - `process.platform`.
|
|
66
|
+
* @param arch - `process.arch`.
|
|
67
|
+
* @param resolve - module resolution, injectable so a test does not depend on what this machine has
|
|
68
|
+
* installed.
|
|
69
|
+
* @returns the absolute path to the addon, or `undefined`.
|
|
70
|
+
*/
|
|
71
|
+
export function nativePackageModulePath(platform, arch, resolve = require.resolve) {
|
|
72
|
+
const name = nativePackageName(platform, arch);
|
|
73
|
+
if (name === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
try {
|
|
76
|
+
return resolve(name);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
29
82
|
/**
|
|
30
83
|
* Every place the addon may live, most specific first.
|
|
31
84
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* and
|
|
36
|
-
* installed or checked out.
|
|
85
|
+
* Three layouts matter: a package that ships the binary puts it in `artifacts/` beside `lib/` (the
|
|
86
|
+
* shape an older hand-copied install has), a repository checkout builds it under
|
|
87
|
+
* `crates/indexer-rust/artifacts/` (three directories up from both `src/` and `lib/`, which sit at the
|
|
88
|
+
* same depth), and an npm install carries it in the platform's optional dependency. Trying all three
|
|
89
|
+
* means one module works whether it was installed, copied in, or checked out.
|
|
37
90
|
* @param base - the importing module's URL; parameterized so a test can point at a scratch tree.
|
|
91
|
+
* @param fromPackage - this platform's optional-dependency path, or `undefined` when there is none.
|
|
38
92
|
* @returns absolute paths to try, in order.
|
|
39
93
|
*/
|
|
40
|
-
export function
|
|
94
|
+
export function modulePathsFor(base, fromPackage) {
|
|
41
95
|
const artifact = `codegraph-indexer.${process.platform}-${process.arch}.node`;
|
|
42
96
|
return [
|
|
43
97
|
fileURLToPath(new URL(`../artifacts/${artifact}`, base)),
|
|
44
98
|
fileURLToPath(new URL(`../../../crates/indexer-rust/artifacts/${artifact}`, base)),
|
|
99
|
+
...fromPackage === undefined ? [] : [fromPackage],
|
|
45
100
|
];
|
|
46
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* The paths production should try, with this platform's optional package worked out for it.
|
|
104
|
+
* @param base - the importing module's URL; parameterized so a test can point at a scratch tree.
|
|
105
|
+
* @returns absolute paths to try, in order.
|
|
106
|
+
*/
|
|
107
|
+
export function candidateModulePaths(base = import.meta.url) {
|
|
108
|
+
return modulePathsFor(base, nativePackageModulePath(process.platform, process.arch));
|
|
109
|
+
}
|
|
47
110
|
/** Addons already loaded, keyed by path, so a test's stand-in never shadows the real artifact. */
|
|
48
111
|
const loaded = new Map();
|
|
49
112
|
/**
|
|
@@ -149,7 +212,7 @@ function rustRulesJson() {
|
|
|
149
212
|
* @returns one extraction per path the addon answered for, or `undefined` when no answer can be
|
|
150
213
|
* trusted at all — no addon, a thrown call, or JSON that is not the documented shape.
|
|
151
214
|
*/
|
|
152
|
-
export function extractWithRust(files, engine) {
|
|
215
|
+
export async function extractWithRust(files, engine) {
|
|
153
216
|
const supported = files.filter(file => RUST_LANGUAGES.includes(file.language));
|
|
154
217
|
if (supported.length === 0)
|
|
155
218
|
return new Map();
|
|
@@ -159,7 +222,10 @@ export function extractWithRust(files, engine) {
|
|
|
159
222
|
try {
|
|
160
223
|
for (let start = 0; start < supported.length; start += RUST_BATCH_SIZE) {
|
|
161
224
|
const batch = supported.slice(start, start + RUST_BATCH_SIZE);
|
|
162
|
-
|
|
225
|
+
// Awaited one batch at a time: the addon already parses a batch across every core, so
|
|
226
|
+
// overlapping batches would only compete for the same threads while multiplying the text held
|
|
227
|
+
// in flight.
|
|
228
|
+
const answer = JSON.parse(await engine.extractWorkspace(batch, rustRulesJson()));
|
|
163
229
|
if (!isRecord(answer) || !Array.isArray(answer.results))
|
|
164
230
|
return undefined;
|
|
165
231
|
for (const entry of answer.results) {
|
package/lib/schema.js
CHANGED
|
@@ -144,7 +144,12 @@ export async function writeGraph(databasePath, input) {
|
|
|
144
144
|
*/
|
|
145
145
|
async function writeStatusFile(directory, status) {
|
|
146
146
|
const target = join(directory, basename(INDEX_STATUS_RELATIVE_PATH));
|
|
147
|
-
|
|
147
|
+
// Unique like the graph's own staging name, and for the same reason: two runs on one workspace are
|
|
148
|
+
// allowed to overlap (a session-start build and an explicit `codegraph_index`, or a watcher rebuild),
|
|
149
|
+
// and a shared fixed-name `.tmp` makes the loser's `rename` fail with ENOENT after the winner moved
|
|
150
|
+
// it — which turned a harmless duplicate run into a failed tool call. Last rename wins; both graphs
|
|
151
|
+
// are complete.
|
|
152
|
+
const temp = `${target}.tmp-${process.pid}-${randomUUID()}`;
|
|
148
153
|
await writeFile(temp, JSON.stringify(status));
|
|
149
154
|
await rename(temp, target);
|
|
150
155
|
}
|
package/lib/walk.js
CHANGED
|
@@ -223,7 +223,7 @@ async function extractPending(pending, config, signal) {
|
|
|
223
223
|
// those itself (`RUST_LANGUAGES`), so the walk never keeps a second copy of that list in sync.
|
|
224
224
|
const rusted = engine === undefined
|
|
225
225
|
? new Map()
|
|
226
|
-
: extractWithRust(pending.map(toRustInput), engine) ?? new Map();
|
|
226
|
+
: await extractWithRust(pending.map(toRustInput), engine) ?? new Map();
|
|
227
227
|
const extracted = new Array(pending.length);
|
|
228
228
|
const fallback = [];
|
|
229
229
|
for (const [index, file] of pending.entries()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-codegraph-tree-sitter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Self-built code-graph indexer for DeepSeek Harness — parses a workspace with web-tree-sitter and writes schema version 4 to .codegraph/codegraph.db, the same on-disk format the codegraph CLI writes, so no external indexer is required",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -51,13 +51,20 @@
|
|
|
51
51
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
52
52
|
"tree-sitter-wasms": "^0.1.13",
|
|
53
53
|
"web-tree-sitter": "^0.25.10",
|
|
54
|
-
"dsh-plugin-codegraph-service": "^0.
|
|
54
|
+
"dsh-plugin-codegraph-service": "^0.6.1"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@deepseek-ai/cordis": ">=4.0.1",
|
|
58
58
|
"@deepseek-ai/dsh-fs": ">=0.1.0-rc.6",
|
|
59
59
|
"@deepseek-ai/dsh-invariants": ">=0.1.0-rc.6"
|
|
60
60
|
},
|
|
61
|
+
"optionalDependencies": {
|
|
62
|
+
"dsh-plugin-codegraph-indexer-darwin-arm64": "0.6.1",
|
|
63
|
+
"dsh-plugin-codegraph-indexer-darwin-x64": "0.6.1",
|
|
64
|
+
"dsh-plugin-codegraph-indexer-linux-x64-gnu": "0.6.1",
|
|
65
|
+
"dsh-plugin-codegraph-indexer-linux-arm64-gnu": "0.6.1",
|
|
66
|
+
"dsh-plugin-codegraph-indexer-win32-x64-msvc": "0.6.1"
|
|
67
|
+
},
|
|
61
68
|
"devDependencies": {
|
|
62
69
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
63
70
|
"@deepseek-ai/dsh-invariants": "0.1.0-rc.6"
|