dsh-plugin-codegraph-tree-sitter 0.5.0 → 0.5.2
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.d.ts +22 -0
- package/lib/index.js +122 -21
- package/package.json +2 -2
package/lib/index.d.ts
CHANGED
|
@@ -73,12 +73,25 @@ export declare const DEFAULT_EXCLUDE: string[];
|
|
|
73
73
|
export declare const DEFAULT_MAX_FILE_BYTES = 2000000;
|
|
74
74
|
/** Default ceiling on how many files one run indexes. */
|
|
75
75
|
export declare const DEFAULT_MAX_FILES = 50000;
|
|
76
|
+
/**
|
|
77
|
+
* How many candidate source files a workspace may contain before a **session-start** index declines
|
|
78
|
+
* to run (default 8,000).
|
|
79
|
+
*
|
|
80
|
+
* `maxFiles` above is the bound for a run someone asked for; this one guards the run nobody asked
|
|
81
|
+
* for. A session opened with `cwd` pointing at a home directory or a tool's own data directory can
|
|
82
|
+
* cover tens of thousands of files — a real case reached 21,752 under `~/.dsh`, under the 50,000
|
|
83
|
+
* ceiling, and the Tree-sitter WASM heap died mid-parse while the desktop froze. Declining early
|
|
84
|
+
* costs one `readdir` pass; declining late costs the whole machine.
|
|
85
|
+
*/
|
|
86
|
+
export declare const DEFAULT_AUTO_INDEX_MAX_FILES = 8000;
|
|
76
87
|
/** Default number of files parsed concurrently. */
|
|
77
88
|
export declare const DEFAULT_CONCURRENCY = 4;
|
|
78
89
|
/** Default quiet period after the last change before a watch-triggered refresh runs. */
|
|
79
90
|
export declare const DEFAULT_WATCH_DEBOUNCE_MS = 2000;
|
|
80
91
|
/** Default cap on directories watched individually, one watch each, by the fs seam. */
|
|
81
92
|
export declare const DEFAULT_MAX_WATCHED_DIRECTORIES = 4000;
|
|
93
|
+
/** Default age past which a session-open refreshes an existing index (ten minutes). */
|
|
94
|
+
export declare const DEFAULT_AUTO_REFRESH_AFTER_MS: number;
|
|
82
95
|
/** Plugin configuration: the indexer's identity and every deployment-varying bound on a run. */
|
|
83
96
|
export interface Config {
|
|
84
97
|
/**
|
|
@@ -173,7 +186,16 @@ export interface Config {
|
|
|
173
186
|
* Requires the host's `jobs` service: the build is never run synchronously inside session creation,
|
|
174
187
|
* so a composition without background jobs simply schedules nothing.
|
|
175
188
|
*/
|
|
189
|
+
/** Cap on candidate source files a session-start scan may find before it declines to index. */
|
|
190
|
+
autoIndexMaxFiles?: number;
|
|
176
191
|
autoIndexOnSession?: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* How stale an existing index may be before a new session refreshes it in the background (default
|
|
194
|
+
* 600000, ten minutes). An index older than this is rebuilt on session start using the parse cache,
|
|
195
|
+
* which is milliseconds — and rebuilding is also what restarts the file watcher, since watching
|
|
196
|
+
* only ever begins after a successful index.
|
|
197
|
+
*/
|
|
198
|
+
autoRefreshAfterMs?: number;
|
|
177
199
|
}
|
|
178
200
|
export declare const Config: z<Config>;
|
|
179
201
|
/**
|
package/lib/index.js
CHANGED
|
@@ -27,13 +27,14 @@
|
|
|
27
27
|
* this package ships with.
|
|
28
28
|
* @module dsh-plugin-codegraph-tree-sitter
|
|
29
29
|
*/
|
|
30
|
-
import {
|
|
30
|
+
import { readFileSync } from 'node:fs';
|
|
31
|
+
import { readdir } from 'node:fs/promises';
|
|
31
32
|
import { stat } from 'node:fs/promises';
|
|
32
|
-
import { join } from 'node:path';
|
|
33
|
+
import { join, posix } from 'node:path';
|
|
33
34
|
import z from '@deepseek-ai/schemastery';
|
|
34
35
|
import { CodegraphIndexerId, INDEX_STATUS_RELATIVE_PATH } from 'dsh-plugin-codegraph-service';
|
|
35
|
-
import { LANGUAGE_TABLE } from "./languages.js";
|
|
36
|
-
import { walkAndExtract } from "./walk.js";
|
|
36
|
+
import { LANGUAGE_TABLE, languageFor } from "./languages.js";
|
|
37
|
+
import { isExcluded, walkAndExtract } from "./walk.js";
|
|
37
38
|
import { writeParseCache } from "./cache.js";
|
|
38
39
|
import { resolveWorkspace } from "./resolve.js";
|
|
39
40
|
import { writeGraph } from "./schema.js";
|
|
@@ -77,12 +78,25 @@ export const DEFAULT_EXCLUDE = ['node_modules', 'dist', 'build', 'coverage', '.g
|
|
|
77
78
|
export const DEFAULT_MAX_FILE_BYTES = 2_000_000;
|
|
78
79
|
/** Default ceiling on how many files one run indexes. */
|
|
79
80
|
export const DEFAULT_MAX_FILES = 50_000;
|
|
81
|
+
/**
|
|
82
|
+
* How many candidate source files a workspace may contain before a **session-start** index declines
|
|
83
|
+
* to run (default 8,000).
|
|
84
|
+
*
|
|
85
|
+
* `maxFiles` above is the bound for a run someone asked for; this one guards the run nobody asked
|
|
86
|
+
* for. A session opened with `cwd` pointing at a home directory or a tool's own data directory can
|
|
87
|
+
* cover tens of thousands of files — a real case reached 21,752 under `~/.dsh`, under the 50,000
|
|
88
|
+
* ceiling, and the Tree-sitter WASM heap died mid-parse while the desktop froze. Declining early
|
|
89
|
+
* costs one `readdir` pass; declining late costs the whole machine.
|
|
90
|
+
*/
|
|
91
|
+
export const DEFAULT_AUTO_INDEX_MAX_FILES = 8_000;
|
|
80
92
|
/** Default number of files parsed concurrently. */
|
|
81
93
|
export const DEFAULT_CONCURRENCY = 4;
|
|
82
94
|
/** Default quiet period after the last change before a watch-triggered refresh runs. */
|
|
83
95
|
export const DEFAULT_WATCH_DEBOUNCE_MS = 2_000;
|
|
84
96
|
/** Default cap on directories watched individually, one watch each, by the fs seam. */
|
|
85
97
|
export const DEFAULT_MAX_WATCHED_DIRECTORIES = 4_000;
|
|
98
|
+
/** Default age past which a session-open refreshes an existing index (ten minutes). */
|
|
99
|
+
export const DEFAULT_AUTO_REFRESH_AFTER_MS = 10 * 60 * 1000;
|
|
86
100
|
export const Config = z.object({
|
|
87
101
|
indexerId: z.string().default(DEFAULT_INDEXER_ID),
|
|
88
102
|
languages: z.array(z.string()).default([...new Set(LANGUAGE_TABLE.map(spec => spec.language))]),
|
|
@@ -98,7 +112,10 @@ export const Config = z.object({
|
|
|
98
112
|
watch: z.boolean().default(true),
|
|
99
113
|
watchDebounceMs: z.number().default(DEFAULT_WATCH_DEBOUNCE_MS),
|
|
100
114
|
maxWatchedDirectories: z.number().default(DEFAULT_MAX_WATCHED_DIRECTORIES),
|
|
115
|
+
/** Cap on candidate source files a session-start scan may find before it declines to index. */
|
|
116
|
+
autoIndexMaxFiles: z.number().default(DEFAULT_AUTO_INDEX_MAX_FILES),
|
|
101
117
|
autoIndexOnSession: z.boolean().default(true),
|
|
118
|
+
autoRefreshAfterMs: z.number().default(DEFAULT_AUTO_REFRESH_AFTER_MS),
|
|
102
119
|
});
|
|
103
120
|
/**
|
|
104
121
|
* Register the tree-sitter indexer.
|
|
@@ -124,10 +141,11 @@ export function apply(ctx, config) {
|
|
|
124
141
|
// cross-plugin relation here for an invariant to describe — just process-local bookkeeping this
|
|
125
142
|
// effect's cleanup below tears down on unload.
|
|
126
143
|
const watchers = new Map();
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
144
|
+
// When this instance last started an automatic index, keyed by root. This is not a "already done"
|
|
145
|
+
// set: an index goes stale while the process runs, so a later session in the same workspace may
|
|
146
|
+
// legitimately trigger another refresh. The map only keeps a burst of sessions opened at once from
|
|
147
|
+
// queueing the same rebuild repeatedly; the freshness decision itself reads the status sidecar.
|
|
148
|
+
const lastAutoRun = new Map();
|
|
131
149
|
// `agent/created` is typed by `@deepseek-ai/dsh-agent`'s module augmentation. Reading the event
|
|
132
150
|
// through a local structural face keeps this package's host dependencies unchanged: it needs one
|
|
133
151
|
// event name and one payload shape, not the whole agent package.
|
|
@@ -192,7 +210,7 @@ export function apply(ctx, config) {
|
|
|
192
210
|
if (resolved.autoIndexOnSession) {
|
|
193
211
|
// Registered inside this effect so the listener follows the plugin's lifetime: unloading the
|
|
194
212
|
// plugin must stop new sessions from scheduling work this instance can no longer run.
|
|
195
|
-
yield agentEvents.on('agent/created', payload => autoIndexOnSession(ctx,
|
|
213
|
+
yield agentEvents.on('agent/created', payload => autoIndexOnSession(ctx, resolved, lastAutoRun, payload));
|
|
196
214
|
}
|
|
197
215
|
yield ctx.codegraph.registerIndexer(indexer);
|
|
198
216
|
}, 'codegraph-tree-sitter');
|
|
@@ -200,30 +218,39 @@ export function apply(ctx, config) {
|
|
|
200
218
|
/** Stable job kind of an automatic session-start index, used for retention and diagnostics. */
|
|
201
219
|
const AUTO_INDEX_JOB_KIND = 'codegraph-index';
|
|
202
220
|
/**
|
|
203
|
-
* Index a new session's workspace in the background
|
|
221
|
+
* Index a new session's workspace in the background — when nothing has indexed it yet, or when what
|
|
222
|
+
* is there has gone stale.
|
|
204
223
|
*
|
|
205
224
|
* Runs synchronously inside `agent/created` dispatch, so it must never block or throw: it only decides
|
|
206
225
|
* whether a build is warranted and hands one to the host's job registry. A synchronous build here
|
|
207
226
|
* would stall agent creation for the length of a parse, which is why a composition without background
|
|
208
227
|
* jobs does nothing instead.
|
|
228
|
+
*
|
|
229
|
+
* Refreshing a stale index is what restarts the file watcher after a host restart: watching only
|
|
230
|
+
* begins once a successful index has established a baseline, so a workspace whose index is minutes
|
|
231
|
+
* old would otherwise stay frozen at whatever the last process left behind.
|
|
209
232
|
* @param ctx - the plugin context, consulted for the optional job registry.
|
|
210
|
-
* @param
|
|
233
|
+
* @param config - the resolved plugin configuration, for the staleness threshold.
|
|
234
|
+
* @param lastAutoRun - when this instance last started an automatic index for each root.
|
|
211
235
|
* @param payload - the `agent/created` dispatch payload.
|
|
212
236
|
*/
|
|
213
|
-
function autoIndexOnSession(ctx,
|
|
237
|
+
function autoIndexOnSession(ctx, config, lastAutoRun, payload) {
|
|
214
238
|
try {
|
|
215
239
|
const agent = payload.agent;
|
|
216
240
|
const root = agent?.session?.header?.cwd;
|
|
217
241
|
// No workspace means no root to claim: an agent created without a `cwd` has nothing to index.
|
|
218
242
|
if (agent === undefined || !root)
|
|
219
243
|
return;
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
// A burst of sessions opened on one workspace must not queue the same rebuild repeatedly.
|
|
246
|
+
const previous = lastAutoRun.get(root);
|
|
247
|
+
if (previous !== undefined && now - previous < config.autoRefreshAfterMs)
|
|
223
248
|
return;
|
|
224
|
-
//
|
|
225
|
-
// the
|
|
226
|
-
|
|
249
|
+
// 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. Checked before the job lookup so a composition
|
|
251
|
+
// without background jobs never pays for the probe.
|
|
252
|
+
const indexedAt = readStatusIndexedAt(root);
|
|
253
|
+
if (indexedAt !== undefined && now - indexedAt < config.autoRefreshAfterMs)
|
|
227
254
|
return;
|
|
228
255
|
// `ctx.get` rather than an `inject` entry: `jobs` is a host composition choice, and requiring it in
|
|
229
256
|
// `inject` would hide this whole indexer from a profile that runs no background jobs.
|
|
@@ -235,11 +262,11 @@ function autoIndexOnSession(ctx, autoIndexed, payload) {
|
|
|
235
262
|
owner: agent.id,
|
|
236
263
|
kind: AUTO_INDEX_JOB_KIND,
|
|
237
264
|
label: `codegraph auto index ${root}`,
|
|
238
|
-
run: handle => runAutoIndexJob(ctx, root, handle),
|
|
265
|
+
run: handle => runAutoIndexJob(ctx, root, config, handle),
|
|
239
266
|
});
|
|
240
267
|
// Marked only after the registry accepted the job, so a transient refusal leaves the workspace
|
|
241
268
|
// eligible for the next session that opens in it.
|
|
242
|
-
|
|
269
|
+
lastAutoRun.set(root, now);
|
|
243
270
|
}
|
|
244
271
|
catch (error) {
|
|
245
272
|
// The host treats a throwing listener as vetoing publication. Losing an index is a degradation,
|
|
@@ -247,6 +274,71 @@ function autoIndexOnSession(ctx, autoIndexed, payload) {
|
|
|
247
274
|
console.error(`codegraph-tree-sitter: automatic indexing for a new session failed: ${String(error)}`);
|
|
248
275
|
}
|
|
249
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* How many candidate source files `root` holds, when that is past the session-start limit.
|
|
279
|
+
*
|
|
280
|
+
* Deliberately a **bounded directory walk that never reads a file**: it counts what `discover` would
|
|
281
|
+
* hand the parser and stops one past the limit. The alternative — starting the real run and bailing
|
|
282
|
+
* when memory runs out — is what froze the machine this guard exists to prevent.
|
|
283
|
+
* @param root - absolute workspace path.
|
|
284
|
+
* @param config - the resolved plugin configuration (for the limit and the exclude list).
|
|
285
|
+
* @param signal - aborts the walk.
|
|
286
|
+
* @returns the count once it exceeds the limit, or `undefined` when the workspace is small enough.
|
|
287
|
+
*/
|
|
288
|
+
async function exceedsAutoIndexLimit(root, config, signal) {
|
|
289
|
+
const excluded = new Set(config.exclude);
|
|
290
|
+
const limit = config.autoIndexMaxFiles;
|
|
291
|
+
let count = 0;
|
|
292
|
+
async function scan(relativeDir) {
|
|
293
|
+
signal.throwIfAborted();
|
|
294
|
+
let entries;
|
|
295
|
+
try {
|
|
296
|
+
entries = await readdir(posix.join(root, relativeDir), { withFileTypes: true });
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// An unreadable directory contributes nothing to a count that exists to protect against size.
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
const relativePath = relativeDir === '' ? entry.name : `${relativeDir}/${entry.name}`;
|
|
304
|
+
if (isExcluded(entry.name, relativePath, entry.isDirectory(), excluded, []))
|
|
305
|
+
continue;
|
|
306
|
+
if (entry.isDirectory()) {
|
|
307
|
+
if (await scan(relativePath))
|
|
308
|
+
return true;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (!entry.isFile())
|
|
312
|
+
continue;
|
|
313
|
+
const spec = languageFor(posix.extname(entry.name));
|
|
314
|
+
if (spec === undefined)
|
|
315
|
+
continue;
|
|
316
|
+
count += 1;
|
|
317
|
+
if (count > limit)
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
return (await scan('')) ? count : undefined;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* When the workspace's sidecar says its index was built.
|
|
326
|
+
* @param root - the workspace root.
|
|
327
|
+
* @returns the epoch-millisecond timestamp, or `undefined` when the sidecar is absent or unreadable —
|
|
328
|
+
* either way the graph cannot be trusted to be current.
|
|
329
|
+
*/
|
|
330
|
+
function readStatusIndexedAt(root) {
|
|
331
|
+
try {
|
|
332
|
+
const parsed = JSON.parse(readFileSync(join(root, INDEX_STATUS_RELATIVE_PATH), 'utf8'));
|
|
333
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
334
|
+
return undefined;
|
|
335
|
+
const indexedAt = parsed.indexedAt;
|
|
336
|
+
return typeof indexedAt === 'number' && Number.isFinite(indexedAt) ? indexedAt : undefined;
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
250
342
|
/**
|
|
251
343
|
* The producer body of one automatic indexing job.
|
|
252
344
|
*
|
|
@@ -258,10 +350,19 @@ function autoIndexOnSession(ctx, autoIndexed, payload) {
|
|
|
258
350
|
* @param handle - the job's producer face, for the job's live log.
|
|
259
351
|
* @returns the job's cancellation hook and settlement promise.
|
|
260
352
|
*/
|
|
261
|
-
function runAutoIndexJob(ctx, root, handle) {
|
|
353
|
+
function runAutoIndexJob(ctx, root, config, handle) {
|
|
262
354
|
const controller = new AbortController();
|
|
263
355
|
const done = (async () => {
|
|
264
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
|
+
}
|
|
265
366
|
const report = await ctx.codegraph.index(root, controller.signal);
|
|
266
367
|
handle.append(`Indexed ${root}: ${report.filesIndexed} files, ${report.nodeCount} symbols, ` +
|
|
267
368
|
`${report.edgeCount} relationships.\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-codegraph-tree-sitter",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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,7 +51,7 @@
|
|
|
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.5.
|
|
54
|
+
"dsh-plugin-codegraph-service": "^0.5.2"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@deepseek-ai/cordis": ">=4.0.1",
|