dsh-plugin-codegraph-tree-sitter 0.6.2 → 0.6.4
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 +8 -0
- package/lib/index.js +19 -1
- package/lib/schema.js +132 -3
- package/lib/walk.js +11 -1
- package/package.json +7 -7
package/lib/index.d.ts
CHANGED
|
@@ -67,6 +67,14 @@ export declare const DEFAULT_INDEXER_ID = "codegraph-tree-sitter";
|
|
|
67
67
|
* Directory segments never descended into by default. Includes this package's own `.codegraph`
|
|
68
68
|
* output — without it, a watcher would treat its own `writeGraph()` as an in-scope change and
|
|
69
69
|
* rebuild forever.
|
|
70
|
+
*
|
|
71
|
+
* `target` is here for a reason that cost real file descriptors to learn: this plugin is developed
|
|
72
|
+
* in its own repository, and `crates/indexer-rust/target/` holds thousands of cargo artefacts
|
|
73
|
+
* across ~1000 directories. Without the exclusion the walk descends into every one of them, and on
|
|
74
|
+
* macOS each watched file pins a descriptor — a single workspace accumulated 9.7k open handles,
|
|
75
|
+
* which is what makes a host's `spawn()` fail with `EBADF` long before any real limit is reached.
|
|
76
|
+
* `.scratch` and `vendor` cover the same shape of accident for working copies and dependency
|
|
77
|
+
* checkouts.
|
|
70
78
|
*/
|
|
71
79
|
export declare const DEFAULT_EXCLUDE: string[];
|
|
72
80
|
/** Default ceiling on a single file's size before it is skipped. */
|
package/lib/index.js
CHANGED
|
@@ -71,8 +71,26 @@ export const DEFAULT_INDEXER_ID = 'codegraph-tree-sitter';
|
|
|
71
71
|
* Directory segments never descended into by default. Includes this package's own `.codegraph`
|
|
72
72
|
* output — without it, a watcher would treat its own `writeGraph()` as an in-scope change and
|
|
73
73
|
* rebuild forever.
|
|
74
|
+
*
|
|
75
|
+
* `target` is here for a reason that cost real file descriptors to learn: this plugin is developed
|
|
76
|
+
* in its own repository, and `crates/indexer-rust/target/` holds thousands of cargo artefacts
|
|
77
|
+
* across ~1000 directories. Without the exclusion the walk descends into every one of them, and on
|
|
78
|
+
* macOS each watched file pins a descriptor — a single workspace accumulated 9.7k open handles,
|
|
79
|
+
* which is what makes a host's `spawn()` fail with `EBADF` long before any real limit is reached.
|
|
80
|
+
* `.scratch` and `vendor` cover the same shape of accident for working copies and dependency
|
|
81
|
+
* checkouts.
|
|
74
82
|
*/
|
|
75
|
-
export const DEFAULT_EXCLUDE = [
|
|
83
|
+
export const DEFAULT_EXCLUDE = [
|
|
84
|
+
'node_modules',
|
|
85
|
+
'dist',
|
|
86
|
+
'build',
|
|
87
|
+
'coverage',
|
|
88
|
+
'.git',
|
|
89
|
+
'.codegraph',
|
|
90
|
+
'target',
|
|
91
|
+
'.scratch',
|
|
92
|
+
'vendor',
|
|
93
|
+
];
|
|
76
94
|
/** Default ceiling on a single file's size before it is skipped. */
|
|
77
95
|
export const DEFAULT_MAX_FILE_BYTES = 2_000_000;
|
|
78
96
|
/** Default ceiling on how many files one run indexes. */
|
package/lib/schema.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* naive "delete then recreate" leaves open between `CREATE TABLE` and the first `COMMIT`.
|
|
11
11
|
* @module dsh-plugin-codegraph-tree-sitter/schema
|
|
12
12
|
*/
|
|
13
|
-
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { mkdir, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
14
14
|
import { basename, dirname, join } from 'node:path';
|
|
15
15
|
import { DatabaseSync } from 'node:sqlite';
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
@@ -52,6 +52,121 @@ async function removeGraphFiles(path) {
|
|
|
52
52
|
await rm(`${path}-wal`, { force: true });
|
|
53
53
|
await rm(`${path}-shm`, { force: true });
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Report whether the process that claimed a lock file is still running.
|
|
57
|
+
*
|
|
58
|
+
* A timestamp is the wrong staleness test here: an index over a large tree legitimately runs
|
|
59
|
+
* for minutes, so any fixed window either steals a lock that is still held or waits forever on
|
|
60
|
+
* one whose holder crashed. The holder writes its pid after the exclusive create, and a pid the
|
|
61
|
+
* OS no longer knows is garbage. `process.kill(pid, 0)` sends no signal — it only asks whether
|
|
62
|
+
* the process exists; `EPERM` means it does and is simply not ours to signal.
|
|
63
|
+
* @param lockPath - absolute path of the `.graph.lock` file to inspect.
|
|
64
|
+
* @returns true when the recorded holder still exists.
|
|
65
|
+
*/
|
|
66
|
+
async function lockHolderIsAlive(lockPath) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = await readFile(lockPath, 'utf8');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Vanished between the failed `wx` and this read: let the caller retry the create.
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const pid = Number.parseInt(raw, 10);
|
|
76
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
77
|
+
return false;
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 0);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// ESRCH: no such process. EPERM: alive, but owned by another user.
|
|
84
|
+
return error.code === 'EPERM';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Serialize graph writes on one workspace, across processes.
|
|
89
|
+
*
|
|
90
|
+
* Two runs used to be allowed to overlap (see writeStatusFile): each built its own
|
|
91
|
+
* temporary file and the last rename won. That is safe for the file, but the loser's
|
|
92
|
+
* status.json can then describe a graph the winner already replaced.
|
|
93
|
+
* @param dir - the `.codegraph` directory, which must already exist.
|
|
94
|
+
* @param work - the write to run while the lock is held.
|
|
95
|
+
*/
|
|
96
|
+
async function withGraphLock(dir, work) {
|
|
97
|
+
const lockPath = join(dir, '.graph.lock');
|
|
98
|
+
const ACQUIRE_MS = 10_000;
|
|
99
|
+
const RETRY_MS = 25;
|
|
100
|
+
const started = Date.now();
|
|
101
|
+
let handle = null;
|
|
102
|
+
while (handle === null) {
|
|
103
|
+
try {
|
|
104
|
+
handle = await open(lockPath, 'wx');
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error.code !== 'EEXIST')
|
|
108
|
+
throw error;
|
|
109
|
+
// A holder that died leaves the file behind; only a dead pid may be stolen, so a
|
|
110
|
+
// live run slower than ACQUIRE_MS is waited out rather than overrun.
|
|
111
|
+
if (!(await lockHolderIsAlive(lockPath))) {
|
|
112
|
+
try {
|
|
113
|
+
await rm(lockPath, { force: true });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// another contender reclaimed it first
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (Date.now() - started > ACQUIRE_MS) {
|
|
121
|
+
throw new Error(`codegraph: another index run holds ${lockPath}`);
|
|
122
|
+
}
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
// Record the holder so the next contender can tell a live run from a crashed one. An
|
|
128
|
+
// absent or unparseable pid reads as dead, which is also the right answer for a lock
|
|
129
|
+
// left by a process that died between the create and this write.
|
|
130
|
+
await handle.writeFile(String(process.pid));
|
|
131
|
+
return await work();
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
try {
|
|
135
|
+
await handle.close();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// already closed
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await rm(lockPath, { force: true });
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// best effort
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* `rename` on Windows fails with EPERM/EBUSY/EACCES while ANY handle holds the target.
|
|
150
|
+
* Those are transient by nature -- an antivirus read, a reader closing its connection --
|
|
151
|
+
* and a single attempt turns one of them into a failed index.
|
|
152
|
+
*/
|
|
153
|
+
async function renameWithRetry(from, to) {
|
|
154
|
+
let last = null;
|
|
155
|
+
for (let attempt = 0; attempt <= 3; attempt += 1) {
|
|
156
|
+
try {
|
|
157
|
+
await rename(from, to);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
last = error;
|
|
162
|
+
const retryable = ['EPERM', 'EBUSY', 'EACCES'];
|
|
163
|
+
if (!retryable.includes(error.code ?? '') || attempt === 3)
|
|
164
|
+
break;
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, 25 * (attempt + 1)));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
throw last;
|
|
169
|
+
}
|
|
55
170
|
/**
|
|
56
171
|
* Replace the graph at `databasePath` with one indexing run's output.
|
|
57
172
|
*
|
|
@@ -63,6 +178,12 @@ async function removeGraphFiles(path) {
|
|
|
63
178
|
* @param input - the run's resolved graph and per-file metadata.
|
|
64
179
|
*/
|
|
65
180
|
export async function writeGraph(databasePath, input) {
|
|
181
|
+
// The lock file lives beside the database, so its directory must exist before the lock can
|
|
182
|
+
// be created — the first index of a workspace has no `.codegraph/` yet.
|
|
183
|
+
await mkdir(dirname(databasePath), { recursive: true });
|
|
184
|
+
return withGraphLock(dirname(databasePath), () => writeGraphUnlocked(databasePath, input));
|
|
185
|
+
}
|
|
186
|
+
async function writeGraphUnlocked(databasePath, input) {
|
|
66
187
|
await mkdir(dirname(databasePath), { recursive: true });
|
|
67
188
|
const tempPath = `${databasePath}.tmp-${process.pid}-${randomUUID()}`;
|
|
68
189
|
// Defensive: a previous run that crashed mid-write may have left this exact temp name behind.
|
|
@@ -119,7 +240,15 @@ export async function writeGraph(databasePath, input) {
|
|
|
119
240
|
// before the rename so the new file never inherits a stale WAL/SHM pair.
|
|
120
241
|
await rm(`${databasePath}-wal`, { force: true });
|
|
121
242
|
await rm(`${databasePath}-shm`, { force: true });
|
|
122
|
-
|
|
243
|
+
try {
|
|
244
|
+
await renameWithRetry(tempPath, databasePath);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
// The rename sat outside the build's cleanup, so a failed replace left a finished
|
|
248
|
+
// multi-megabyte temp behind. Reproduced before this change.
|
|
249
|
+
await removeGraphFiles(tempPath);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
123
252
|
// rename() only moves the main file; the temp name's own sidecars (should the driver have left any)
|
|
124
253
|
// never travel with it and would otherwise linger under the temp name forever.
|
|
125
254
|
await removeGraphFiles(tempPath);
|
|
@@ -149,7 +278,7 @@ export async function writeGraph(databasePath, input) {
|
|
|
149
278
|
export async function writeJsonAtomically(target, value) {
|
|
150
279
|
const temp = `${target}.tmp-${process.pid}-${randomUUID()}`;
|
|
151
280
|
await writeFile(temp, JSON.stringify(value));
|
|
152
|
-
await
|
|
281
|
+
await renameWithRetry(temp, target);
|
|
153
282
|
}
|
|
154
283
|
/**
|
|
155
284
|
* Write the prompt-time status sidecar beside the graph, replacing it atomically.
|
package/lib/walk.js
CHANGED
|
@@ -171,9 +171,12 @@ async function prepareCandidate(candidate, config, cache, signal) {
|
|
|
171
171
|
*/
|
|
172
172
|
async function extractWithJs(file, signal) {
|
|
173
173
|
const parser = await createParser(file.spec);
|
|
174
|
+
// Declared outside the `try` so the `finally` can free it: a `const` inside the `try` is not
|
|
175
|
+
// in scope there, and the tree is where a parse keeps its memory.
|
|
176
|
+
let tree = null;
|
|
174
177
|
try {
|
|
175
178
|
signal?.throwIfAborted();
|
|
176
|
-
|
|
179
|
+
tree = parser.parse(file.text);
|
|
177
180
|
/* v8 ignore next 2 -- createParser() always assigns a language before returning; parse() returns
|
|
178
181
|
* null only when the parser has none. */
|
|
179
182
|
if (tree === null)
|
|
@@ -181,6 +184,13 @@ async function extractWithJs(file, signal) {
|
|
|
181
184
|
return extractFile(tree, file.spec);
|
|
182
185
|
}
|
|
183
186
|
finally {
|
|
187
|
+
// The memory a parse holds lives in the tree, not the parser: a 362 KB source keeps tens of
|
|
188
|
+
// megabytes of AST. Deleting only the parser leaked it for the life of the process -- measured
|
|
189
|
+
// on a 477-file workspace: RSS +103 MB per index run, +1.1 GB across 11 runs before the host ran
|
|
190
|
+
// out of memory. With this line the same loop is flat at 95 MB.
|
|
191
|
+
/* v8 ignore next -- the same impossible null as the check above: parse() returns null only
|
|
192
|
+
* when the parser has no language, which createParser() never returns. */
|
|
193
|
+
tree?.delete();
|
|
184
194
|
parser.delete();
|
|
185
195
|
}
|
|
186
196
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-codegraph-tree-sitter",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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.6.
|
|
54
|
+
"dsh-plugin-codegraph-service": "^0.6.4"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@deepseek-ai/cordis": ">=4.0.1",
|
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
"@deepseek-ai/dsh-invariants": ">=0.1.0-rc.6"
|
|
60
60
|
},
|
|
61
61
|
"optionalDependencies": {
|
|
62
|
-
"dsh-plugin-codegraph-indexer-darwin-arm64": "0.6.
|
|
63
|
-
"dsh-plugin-codegraph-indexer-darwin-x64": "0.6.
|
|
64
|
-
"dsh-plugin-codegraph-indexer-linux-x64-gnu": "0.6.
|
|
65
|
-
"dsh-plugin-codegraph-indexer-linux-arm64-gnu": "0.6.
|
|
66
|
-
"dsh-plugin-codegraph-indexer-win32-x64-msvc": "0.6.
|
|
62
|
+
"dsh-plugin-codegraph-indexer-darwin-arm64": "0.6.4",
|
|
63
|
+
"dsh-plugin-codegraph-indexer-darwin-x64": "0.6.4",
|
|
64
|
+
"dsh-plugin-codegraph-indexer-linux-x64-gnu": "0.6.4",
|
|
65
|
+
"dsh-plugin-codegraph-indexer-linux-arm64-gnu": "0.6.4",
|
|
66
|
+
"dsh-plugin-codegraph-indexer-win32-x64-msvc": "0.6.4"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@deepseek-ai/cordis": "^4.0.1",
|