dsh-plugin-codegraph-tree-sitter 0.1.6 → 0.2.0
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/cache.d.ts +65 -0
- package/lib/cache.js +142 -0
- package/lib/index.d.ts +45 -7
- package/lib/index.js +36 -7
- package/lib/minified.d.ts +52 -0
- package/lib/minified.js +58 -0
- package/lib/resolve.d.ts +13 -6
- package/lib/resolve.js +50 -13
- package/lib/schema.d.ts +2 -0
- package/lib/schema.js +27 -2
- package/lib/walk.d.ts +35 -2
- package/lib/walk.js +95 -21
- package/lib/watcher.d.ts +106 -41
- package/lib/watcher.js +307 -163
- package/package.json +3 -2
package/lib/cache.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse-result cache: the extraction this package computed for each file on the previous run, so a
|
|
3
|
+
* repeat index of an unchanged workspace can skip the read, the parse, and the tree walk for every
|
|
4
|
+
* file whose content did not change.
|
|
5
|
+
*
|
|
6
|
+
* The cache is an accelerator with no authority. It is read only as a hint — anything missing,
|
|
7
|
+
* unreadable, corrupt, written by another version, or shaped unexpectedly is discarded and the file
|
|
8
|
+
* is parsed again, so losing it costs time and never correctness. That is what lets
|
|
9
|
+
* {@link readParseCache} swallow every failure and {@link writeParseCache} tolerate a workspace this
|
|
10
|
+
* process may not write to.
|
|
11
|
+
* @module dsh-plugin-codegraph-tree-sitter/cache
|
|
12
|
+
*/
|
|
13
|
+
import type { FileExtraction } from './extract.ts';
|
|
14
|
+
import type { ExtractedFile } from './resolve.ts';
|
|
15
|
+
/**
|
|
16
|
+
* Version of the extraction semantics this cache records. Bump it by hand whenever `extract.ts` (or
|
|
17
|
+
* a grammar that feeds it) starts producing a different result for the same source text: a stale
|
|
18
|
+
* entry would otherwise be reused as if it were current, and the graph would silently keep the old
|
|
19
|
+
* shape.
|
|
20
|
+
*/
|
|
21
|
+
export declare const EXTRACTION_CACHE_VERSION = 1;
|
|
22
|
+
/** Path of the parse cache, relative to a project root. */
|
|
23
|
+
export declare const EXTRACTION_CACHE_RELATIVE_PATH = ".codegraph/cache/extractions.json";
|
|
24
|
+
/** One file's cached extraction, together with the identity it was computed from. */
|
|
25
|
+
export interface CachedFile {
|
|
26
|
+
/** Epoch milliseconds of the file's modification time when its extraction was produced. */
|
|
27
|
+
readonly modifiedAt: number;
|
|
28
|
+
/** File size in bytes at that same moment. */
|
|
29
|
+
readonly size: number;
|
|
30
|
+
/** SHA-256 of the content the extraction was produced from, hex-encoded. */
|
|
31
|
+
readonly contentHash: string;
|
|
32
|
+
/** Line count of that content, recorded so a reused file still gets a correct span. */
|
|
33
|
+
readonly lineCount: number;
|
|
34
|
+
/** Seam language label the extraction was produced for. */
|
|
35
|
+
readonly language: string;
|
|
36
|
+
/** The extraction itself. */
|
|
37
|
+
readonly extraction: FileExtraction;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Load the previous run's extractions for `projectRoot`.
|
|
41
|
+
*
|
|
42
|
+
* Never throws: a cache that cannot be read, parsed, or trusted yields an empty map, which makes the
|
|
43
|
+
* caller parse everything. A single malformed entry discards the whole file — an entry is either a
|
|
44
|
+
* complete record this package wrote or evidence the document was damaged, and reasoning about
|
|
45
|
+
* partial reuse is not worth the risk of a graph built from a half-written entry.
|
|
46
|
+
* @param projectRoot - absolute path of the workspace whose cache to load.
|
|
47
|
+
* @returns the cached extractions keyed by project-relative path, empty when none can be trusted.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readParseCache(projectRoot: string): Promise<Map<string, CachedFile>>;
|
|
50
|
+
/**
|
|
51
|
+
* Record this run's extractions as the next run's cache.
|
|
52
|
+
*
|
|
53
|
+
* Written through a sibling temporary name and `rename`d into place, the same way `./schema.ts`
|
|
54
|
+
* replaces its status sidecar, so a reader never sees half a document. The temporary name is fixed
|
|
55
|
+
* rather than unique: a write interrupted before its rename is overwritten by the next attempt
|
|
56
|
+
* instead of accumulating, and a torn leftover is rejected by {@link readParseCache} at the cost of
|
|
57
|
+
* one run's reuse.
|
|
58
|
+
*
|
|
59
|
+
* A failure is swallowed on purpose. A read-only workspace, a full disk, or a locked directory must
|
|
60
|
+
* still be indexable; the cache is the optimization, never the product.
|
|
61
|
+
* @param projectRoot - absolute path of the workspace to record the cache for.
|
|
62
|
+
* @param files - every file this run produced an extraction for.
|
|
63
|
+
*/
|
|
64
|
+
export declare function writeParseCache(projectRoot: string, files: readonly ExtractedFile[]): Promise<void>;
|
|
65
|
+
//# sourceMappingURL=cache.d.ts.map
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse-result cache: the extraction this package computed for each file on the previous run, so a
|
|
3
|
+
* repeat index of an unchanged workspace can skip the read, the parse, and the tree walk for every
|
|
4
|
+
* file whose content did not change.
|
|
5
|
+
*
|
|
6
|
+
* The cache is an accelerator with no authority. It is read only as a hint — anything missing,
|
|
7
|
+
* unreadable, corrupt, written by another version, or shaped unexpectedly is discarded and the file
|
|
8
|
+
* is parsed again, so losing it costs time and never correctness. That is what lets
|
|
9
|
+
* {@link readParseCache} swallow every failure and {@link writeParseCache} tolerate a workspace this
|
|
10
|
+
* process may not write to.
|
|
11
|
+
* @module dsh-plugin-codegraph-tree-sitter/cache
|
|
12
|
+
*/
|
|
13
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
/**
|
|
16
|
+
* Version of the extraction semantics this cache records. Bump it by hand whenever `extract.ts` (or
|
|
17
|
+
* a grammar that feeds it) starts producing a different result for the same source text: a stale
|
|
18
|
+
* entry would otherwise be reused as if it were current, and the graph would silently keep the old
|
|
19
|
+
* shape.
|
|
20
|
+
*/
|
|
21
|
+
export const EXTRACTION_CACHE_VERSION = 1;
|
|
22
|
+
/** Path of the parse cache, relative to a project root. */
|
|
23
|
+
export const EXTRACTION_CACHE_RELATIVE_PATH = '.codegraph/cache/extractions.json';
|
|
24
|
+
/**
|
|
25
|
+
* Whether `value` is a plain JSON object. Arrays are deliberately included: every caller still
|
|
26
|
+
* validates the fields it needs, and an array simply carries none of them.
|
|
27
|
+
* @param value - the parsed JSON value to test.
|
|
28
|
+
* @returns whether it is a non-null object.
|
|
29
|
+
*/
|
|
30
|
+
function isRecord(value) {
|
|
31
|
+
return typeof value === 'object' && value !== null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether `value` is a usable extraction. Deliberately shallow past the four arrays: those are what
|
|
35
|
+
* `resolveWorkspace` indexes into, and the only expected producer is this package's own writer.
|
|
36
|
+
* @param value - the value to test.
|
|
37
|
+
* @returns whether it has the four collections an extraction consists of.
|
|
38
|
+
*/
|
|
39
|
+
function isFileExtraction(value) {
|
|
40
|
+
return isRecord(value)
|
|
41
|
+
&& Array.isArray(value.definitions)
|
|
42
|
+
&& Array.isArray(value.calls)
|
|
43
|
+
&& Array.isArray(value.imports)
|
|
44
|
+
&& Array.isArray(value.heritage);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether `value` is a complete cache entry.
|
|
48
|
+
* @param value - the value to test.
|
|
49
|
+
* @returns whether every field is present and of the type a reader will use it as.
|
|
50
|
+
*/
|
|
51
|
+
function isCachedFile(value) {
|
|
52
|
+
if (!isRecord(value))
|
|
53
|
+
return false;
|
|
54
|
+
return typeof value.modifiedAt === 'number'
|
|
55
|
+
&& typeof value.size === 'number'
|
|
56
|
+
&& typeof value.contentHash === 'string'
|
|
57
|
+
&& typeof value.lineCount === 'number'
|
|
58
|
+
&& typeof value.language === 'string'
|
|
59
|
+
&& isFileExtraction(value.extraction);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Load the previous run's extractions for `projectRoot`.
|
|
63
|
+
*
|
|
64
|
+
* Never throws: a cache that cannot be read, parsed, or trusted yields an empty map, which makes the
|
|
65
|
+
* caller parse everything. A single malformed entry discards the whole file — an entry is either a
|
|
66
|
+
* complete record this package wrote or evidence the document was damaged, and reasoning about
|
|
67
|
+
* partial reuse is not worth the risk of a graph built from a half-written entry.
|
|
68
|
+
* @param projectRoot - absolute path of the workspace whose cache to load.
|
|
69
|
+
* @returns the cached extractions keyed by project-relative path, empty when none can be trusted.
|
|
70
|
+
*/
|
|
71
|
+
export async function readParseCache(projectRoot) {
|
|
72
|
+
const cachePath = join(projectRoot, EXTRACTION_CACHE_RELATIVE_PATH);
|
|
73
|
+
let raw;
|
|
74
|
+
try {
|
|
75
|
+
raw = await readFile(cachePath, 'utf8');
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// The ordinary first run has no cache file yet; an unreadable one is the same answer to the
|
|
79
|
+
// caller — nothing to reuse — and must not fail an index that can still parse the workspace.
|
|
80
|
+
return new Map();
|
|
81
|
+
}
|
|
82
|
+
let document;
|
|
83
|
+
try {
|
|
84
|
+
document = JSON.parse(raw);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return new Map();
|
|
88
|
+
}
|
|
89
|
+
if (!isRecord(document))
|
|
90
|
+
return new Map();
|
|
91
|
+
if (document.cacheVersion !== EXTRACTION_CACHE_VERSION)
|
|
92
|
+
return new Map();
|
|
93
|
+
const entries = document.files;
|
|
94
|
+
if (!isRecord(entries))
|
|
95
|
+
return new Map();
|
|
96
|
+
const cache = new Map();
|
|
97
|
+
for (const [path, entry] of Object.entries(entries)) {
|
|
98
|
+
if (!isCachedFile(entry))
|
|
99
|
+
return new Map();
|
|
100
|
+
cache.set(path, entry);
|
|
101
|
+
}
|
|
102
|
+
return cache;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Record this run's extractions as the next run's cache.
|
|
106
|
+
*
|
|
107
|
+
* Written through a sibling temporary name and `rename`d into place, the same way `./schema.ts`
|
|
108
|
+
* replaces its status sidecar, so a reader never sees half a document. The temporary name is fixed
|
|
109
|
+
* rather than unique: a write interrupted before its rename is overwritten by the next attempt
|
|
110
|
+
* instead of accumulating, and a torn leftover is rejected by {@link readParseCache} at the cost of
|
|
111
|
+
* one run's reuse.
|
|
112
|
+
*
|
|
113
|
+
* A failure is swallowed on purpose. A read-only workspace, a full disk, or a locked directory must
|
|
114
|
+
* still be indexable; the cache is the optimization, never the product.
|
|
115
|
+
* @param projectRoot - absolute path of the workspace to record the cache for.
|
|
116
|
+
* @param files - every file this run produced an extraction for.
|
|
117
|
+
*/
|
|
118
|
+
export async function writeParseCache(projectRoot, files) {
|
|
119
|
+
const cachePath = join(projectRoot, EXTRACTION_CACHE_RELATIVE_PATH);
|
|
120
|
+
const tempPath = `${cachePath}.tmp`;
|
|
121
|
+
try {
|
|
122
|
+
const entries = {};
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
entries[file.path] = {
|
|
125
|
+
modifiedAt: file.modifiedAt,
|
|
126
|
+
size: file.size,
|
|
127
|
+
contentHash: file.contentHash,
|
|
128
|
+
lineCount: file.lineCount,
|
|
129
|
+
language: file.language,
|
|
130
|
+
extraction: file.extraction,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const document = { cacheVersion: EXTRACTION_CACHE_VERSION, files: entries };
|
|
134
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
135
|
+
await writeFile(tempPath, JSON.stringify(document));
|
|
136
|
+
await rename(tempPath, cachePath);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// See above: an unusable cache location only means the next run parses everything again.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=cache.js.map
|
package/lib/index.d.ts
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* runs implicitly from a query — it is a caller-initiated, potentially multi-minute operation, kept
|
|
11
11
|
* out of the seam's read-only query path on purpose.
|
|
12
12
|
*
|
|
13
|
+
* Watching a root runs through `ctx.fs` (see `./watcher.ts`): the package observes each in-scope
|
|
14
|
+
* directory through the host's filesystem seam rather than opening paths itself, so a workspace a
|
|
15
|
+
* backend serves from another execution world is watched where it actually lives, and a backend that
|
|
16
|
+
* cannot watch at all degrades to explicit indexing instead of failing the plugin.
|
|
17
|
+
*
|
|
13
18
|
* Namespace plugin (named exports, no default export). Grammars load lazily, one per language, on
|
|
14
19
|
* first sight of a matching file, and stay cached for the process — never eagerly for every grammar
|
|
15
20
|
* this package ships with.
|
|
@@ -25,11 +30,15 @@ export { loadGitignore, matchesGitignore, parseGitignore } from './gitignore.ts'
|
|
|
25
30
|
export type { GitignoreRule } from './gitignore.ts';
|
|
26
31
|
export { resolveWorkspace } from './resolve.ts';
|
|
27
32
|
export type { ExtractedFile, GraphEdge, GraphNode, ResolvedGraph, UnresolvedRef } from './resolve.ts';
|
|
33
|
+
export { isMinifiedName, looksMinified, measureText, MINIFIED_AVERAGE_LINE_LENGTH } from './minified.ts';
|
|
34
|
+
export type { TextShape } from './minified.ts';
|
|
28
35
|
export { SCHEMA_VERSION, writeGraph } from './schema.ts';
|
|
36
|
+
export { EXTRACTION_CACHE_RELATIVE_PATH, EXTRACTION_CACHE_VERSION, readParseCache, writeParseCache } from './cache.ts';
|
|
37
|
+
export type { CachedFile } from './cache.ts';
|
|
29
38
|
export { isExcluded, walkAndExtract } from './walk.ts';
|
|
30
39
|
export type { WalkConfig, WalkResult } from './walk.ts';
|
|
31
|
-
export { createWatcher
|
|
32
|
-
export type { DegradeReason, WatchConfig,
|
|
40
|
+
export { createWatcher } from './watcher.ts';
|
|
41
|
+
export type { DegradeReason, WatchConfig, WatchDirEntry, WatchFileSystem, WatchTarget, Watcher, } from './watcher.ts';
|
|
33
42
|
export { decideWatch, readProcVersion } from './watch-policy.ts';
|
|
34
43
|
export type { WatchDecision, WatchPolicyInput } from './watch-policy.ts';
|
|
35
44
|
export { detectWorktree, execGit } from './worktree.ts';
|
|
@@ -58,7 +67,7 @@ export declare const DEFAULT_MAX_FILES = 50000;
|
|
|
58
67
|
export declare const DEFAULT_CONCURRENCY = 4;
|
|
59
68
|
/** Default quiet period after the last change before a watch-triggered refresh runs. */
|
|
60
69
|
export declare const DEFAULT_WATCH_DEBOUNCE_MS = 2000;
|
|
61
|
-
/** Default cap on directories watched individually,
|
|
70
|
+
/** Default cap on directories watched individually, one watch each, by the fs seam. */
|
|
62
71
|
export declare const DEFAULT_MAX_WATCHED_DIRECTORIES = 4000;
|
|
63
72
|
/** Plugin configuration: the indexer's identity and every deployment-varying bound on a run. */
|
|
64
73
|
export interface Config {
|
|
@@ -80,12 +89,40 @@ export interface Config {
|
|
|
80
89
|
* see `./gitignore.ts`. A project with no root `.gitignore` is unaffected either way.
|
|
81
90
|
*/
|
|
82
91
|
respectGitignore?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Leave generated (bundled or minified) artifacts out of the index instead of parsing them
|
|
94
|
+
* (default true). A workspace that commits its build output — `dist/` in version control so a
|
|
95
|
+
* server can serve it — hands the walk megabytes of bundler output declaring thousands of short
|
|
96
|
+
* mangled names; indexing it both dominates the run's time and collides with real source symbols,
|
|
97
|
+
* making "one unique workspace-wide name wins" refuse calls the duplicates made ambiguous.
|
|
98
|
+
*
|
|
99
|
+
* Two signals, one structural: a `.min.` path segment, or an average line length past
|
|
100
|
+
* {@link MINIFIED_AVERAGE_LINE_LENGTH}. Set false to index such files anyway, or name their
|
|
101
|
+
* directory in `exclude`.
|
|
102
|
+
*/
|
|
103
|
+
skipMinified?: boolean;
|
|
83
104
|
/** Files larger than this are skipped and counted in the report, never parsed (default 2000000). */
|
|
84
105
|
maxFileBytes?: number;
|
|
85
106
|
/** The walk stops discovering new files once this many have been found (default 50000). */
|
|
86
107
|
maxFiles?: number;
|
|
87
108
|
/** Files parsed concurrently (default 4). */
|
|
88
109
|
concurrency?: number;
|
|
110
|
+
/**
|
|
111
|
+
* Reuse the extraction a previous run cached for every file whose content is unchanged (default
|
|
112
|
+
* true), so a repeat index — or a watch rebuild after one edit — parses only what actually changed
|
|
113
|
+
* instead of the whole workspace. The cache lives at
|
|
114
|
+
* {@link EXTRACTION_CACHE_RELATIVE_PATH} and is rewritten after each successful run. Set false to
|
|
115
|
+
* parse every file every time, at the cost of the full parse each run.
|
|
116
|
+
*/
|
|
117
|
+
reuseParsedCache?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* How a cached file's unchanged content is confirmed before its extraction is reused (default
|
|
120
|
+
* `'stat'`): `stat` compares the modification time and size the cache recorded and reads nothing;
|
|
121
|
+
* `hash` re-reads the file and compares a SHA-256, which also catches a rewrite that preserved both
|
|
122
|
+
* mtime and size (a checkout restoring an older file over a newer one). Hash mode still skips the
|
|
123
|
+
* parse and the tree walk, which dominate the per-file cost.
|
|
124
|
+
*/
|
|
125
|
+
cacheVerify?: 'stat' | 'hash';
|
|
89
126
|
/**
|
|
90
127
|
* Watch the workspace for file changes and refresh the index automatically after a successful
|
|
91
128
|
* `index()` call establishes a baseline (default true). Set `watch: false` to keep indexing purely
|
|
@@ -100,16 +137,17 @@ export interface Config {
|
|
|
100
137
|
/** Quiet period after the last change before a watch-triggered refresh runs, in ms (default 2000). */
|
|
101
138
|
watchDebounceMs?: number;
|
|
102
139
|
/**
|
|
103
|
-
* Hard cap on directories watched individually (default 4000).
|
|
104
|
-
*
|
|
105
|
-
*
|
|
140
|
+
* Hard cap on directories watched individually (default 4000). Watching goes through the host's
|
|
141
|
+
* filesystem seam, whose `watch` covers one directory's direct entries, so a workspace costs one
|
|
142
|
+
* watch per in-scope directory; exceeding this cap degrades that root's watcher rather than
|
|
143
|
+
* covering only part of the tree.
|
|
106
144
|
*/
|
|
107
145
|
maxWatchedDirectories?: number;
|
|
108
146
|
}
|
|
109
147
|
export declare const Config: z<Config>;
|
|
110
148
|
/**
|
|
111
149
|
* Register the tree-sitter indexer.
|
|
112
|
-
* @param ctx - the plugin context (must inject `codegraph`).
|
|
150
|
+
* @param ctx - the plugin context (must inject `codegraph` and `fs`).
|
|
113
151
|
* @param config - the resolved plugin configuration.
|
|
114
152
|
*/
|
|
115
153
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* runs implicitly from a query — it is a caller-initiated, potentially multi-minute operation, kept
|
|
11
11
|
* out of the seam's read-only query path on purpose.
|
|
12
12
|
*
|
|
13
|
+
* Watching a root runs through `ctx.fs` (see `./watcher.ts`): the package observes each in-scope
|
|
14
|
+
* directory through the host's filesystem seam rather than opening paths itself, so a workspace a
|
|
15
|
+
* backend serves from another execution world is watched where it actually lives, and a backend that
|
|
16
|
+
* cannot watch at all degrades to explicit indexing instead of failing the plugin.
|
|
17
|
+
*
|
|
13
18
|
* Namespace plugin (named exports, no default export). Grammars load lazily, one per language, on
|
|
14
19
|
* first sight of a matching file, and stay cached for the process — never eagerly for every grammar
|
|
15
20
|
* this package ships with.
|
|
@@ -21,6 +26,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
21
26
|
import { CodegraphIndexerId } from 'dsh-plugin-codegraph-service';
|
|
22
27
|
import { LANGUAGE_TABLE } from "./languages.js";
|
|
23
28
|
import { walkAndExtract } from "./walk.js";
|
|
29
|
+
import { writeParseCache } from "./cache.js";
|
|
24
30
|
import { resolveWorkspace } from "./resolve.js";
|
|
25
31
|
import { writeGraph } from "./schema.js";
|
|
26
32
|
import { createWatcher } from "./watcher.js";
|
|
@@ -29,16 +35,18 @@ export { LANGUAGE_TABLE, languageFor } from "./languages.js";
|
|
|
29
35
|
export { extractFile } from "./extract.js";
|
|
30
36
|
export { loadGitignore, matchesGitignore, parseGitignore } from "./gitignore.js";
|
|
31
37
|
export { resolveWorkspace } from "./resolve.js";
|
|
38
|
+
export { isMinifiedName, looksMinified, measureText, MINIFIED_AVERAGE_LINE_LENGTH } from "./minified.js";
|
|
32
39
|
export { SCHEMA_VERSION, writeGraph } from "./schema.js";
|
|
40
|
+
export { EXTRACTION_CACHE_RELATIVE_PATH, EXTRACTION_CACHE_VERSION, readParseCache, writeParseCache } from "./cache.js";
|
|
33
41
|
export { isExcluded, walkAndExtract } from "./walk.js";
|
|
34
|
-
export { createWatcher
|
|
42
|
+
export { createWatcher } from "./watcher.js";
|
|
35
43
|
export { decideWatch, readProcVersion } from "./watch-policy.js";
|
|
36
44
|
export { detectWorktree, execGit } from "./worktree.js";
|
|
37
45
|
export { HOOK_MARKER_BEGIN, HOOK_MARKER_END, installGitHooks, uninstallGitHooks } from "./git-hooks.js";
|
|
38
46
|
/** Cordis plugin name for loader diagnostics. */
|
|
39
47
|
export const name = 'codegraph-tree-sitter';
|
|
40
48
|
/** Services required by this plugin. */
|
|
41
|
-
export const inject = ['codegraph'];
|
|
49
|
+
export const inject = ['codegraph', 'fs'];
|
|
42
50
|
/** Path of the on-disk graph this package writes, relative to a project root. */
|
|
43
51
|
export const DATABASE_RELATIVE_PATH = '.codegraph/codegraph.db';
|
|
44
52
|
/** Default branded identity this indexer reserves on the seam. */
|
|
@@ -57,23 +65,26 @@ export const DEFAULT_MAX_FILES = 50_000;
|
|
|
57
65
|
export const DEFAULT_CONCURRENCY = 4;
|
|
58
66
|
/** Default quiet period after the last change before a watch-triggered refresh runs. */
|
|
59
67
|
export const DEFAULT_WATCH_DEBOUNCE_MS = 2_000;
|
|
60
|
-
/** Default cap on directories watched individually,
|
|
68
|
+
/** Default cap on directories watched individually, one watch each, by the fs seam. */
|
|
61
69
|
export const DEFAULT_MAX_WATCHED_DIRECTORIES = 4_000;
|
|
62
70
|
export const Config = z.object({
|
|
63
71
|
indexerId: z.string().default(DEFAULT_INDEXER_ID),
|
|
64
72
|
languages: z.array(z.string()).default([...new Set(LANGUAGE_TABLE.map(spec => spec.language))]),
|
|
65
73
|
exclude: z.array(z.string()).default(DEFAULT_EXCLUDE),
|
|
66
74
|
respectGitignore: z.boolean().default(true),
|
|
75
|
+
skipMinified: z.boolean().default(true),
|
|
67
76
|
maxFileBytes: z.number().default(DEFAULT_MAX_FILE_BYTES),
|
|
68
77
|
maxFiles: z.number().default(DEFAULT_MAX_FILES),
|
|
69
78
|
concurrency: z.number().default(DEFAULT_CONCURRENCY),
|
|
79
|
+
reuseParsedCache: z.boolean().default(true),
|
|
80
|
+
cacheVerify: z.union([z.const('stat'), z.const('hash')]).default('stat'),
|
|
70
81
|
watch: z.boolean().default(true),
|
|
71
82
|
watchDebounceMs: z.number().default(DEFAULT_WATCH_DEBOUNCE_MS),
|
|
72
83
|
maxWatchedDirectories: z.number().default(DEFAULT_MAX_WATCHED_DIRECTORIES),
|
|
73
84
|
});
|
|
74
85
|
/**
|
|
75
86
|
* Register the tree-sitter indexer.
|
|
76
|
-
* @param ctx - the plugin context (must inject `codegraph`).
|
|
87
|
+
* @param ctx - the plugin context (must inject `codegraph` and `fs`).
|
|
77
88
|
* @param config - the resolved plugin configuration.
|
|
78
89
|
*/
|
|
79
90
|
export function apply(ctx, config) {
|
|
@@ -83,6 +94,13 @@ export function apply(ctx, config) {
|
|
|
83
94
|
assertPositiveInteger('concurrency', resolved.concurrency);
|
|
84
95
|
assertPositiveInteger('watchDebounceMs', resolved.watchDebounceMs);
|
|
85
96
|
assertPositiveInteger('maxWatchedDirectories', resolved.maxWatchedDirectories);
|
|
97
|
+
// Watching goes through the host's filesystem seam rather than `node:fs`, so it follows the same
|
|
98
|
+
// execution world the rest of the harness reads: a remote or confined backend watches its own paths.
|
|
99
|
+
// The `fs` service is a declared dependency (see `inject`), so it is present whenever `apply` runs.
|
|
100
|
+
// It is read structurally instead of importing `@deepseek-ai/dsh-fs`: that package is not a
|
|
101
|
+
// dependency here, and its `watch` method only exists from host 0.2 onward, while this package
|
|
102
|
+
// supports older peers that have no such member to import.
|
|
103
|
+
const fileSystem = ctx.fs;
|
|
86
104
|
// Keyed by project root, private to this plugin instance: never registered on `ctx`, so there is no
|
|
87
105
|
// cross-plugin relation here for an invariant to describe — just process-local bookkeeping this
|
|
88
106
|
// effect's cleanup below tears down on unload.
|
|
@@ -113,7 +131,7 @@ export function apply(ctx, config) {
|
|
|
113
131
|
console.error(`codegraph-tree-sitter: file watching for "${projectRoot}" stopped permanently (${reason.code}); ` +
|
|
114
132
|
'the index will no longer refresh on its own — run codegraph_index manually to update it.');
|
|
115
133
|
},
|
|
116
|
-
});
|
|
134
|
+
}, fileSystem);
|
|
117
135
|
watchers.set(projectRoot, watcher);
|
|
118
136
|
watcher.start();
|
|
119
137
|
}
|
|
@@ -167,20 +185,29 @@ async function runSync(projectRoot, config) {
|
|
|
167
185
|
* @returns the run's report.
|
|
168
186
|
*/
|
|
169
187
|
async function runIndex(projectRoot, config, signal) {
|
|
170
|
-
const { files, filesSkipped } = await walkAndExtract(projectRoot, {
|
|
188
|
+
const { files, filesSkipped, filesMinified, filesReused } = await walkAndExtract(projectRoot, {
|
|
171
189
|
exclude: config.exclude,
|
|
172
190
|
respectGitignore: config.respectGitignore,
|
|
191
|
+
skipMinified: config.skipMinified,
|
|
173
192
|
maxFileBytes: config.maxFileBytes,
|
|
174
193
|
maxFiles: config.maxFiles,
|
|
175
194
|
concurrency: config.concurrency,
|
|
176
195
|
languages: config.languages,
|
|
196
|
+
reuseParsedCache: config.reuseParsedCache,
|
|
197
|
+
cacheVerify: config.cacheVerify,
|
|
177
198
|
}, signal);
|
|
178
199
|
signal?.throwIfAborted();
|
|
179
200
|
const indexedAt = Date.now();
|
|
180
201
|
const graph = resolveWorkspace(files, indexedAt);
|
|
181
202
|
signal?.throwIfAborted();
|
|
182
203
|
const databasePath = join(projectRoot, DATABASE_RELATIVE_PATH);
|
|
183
|
-
await writeGraph(databasePath, {
|
|
204
|
+
await writeGraph(databasePath, {
|
|
205
|
+
files, nodes: graph.nodes, edges: graph.edges, unresolved: graph.unresolved, filesMinified, indexedAt,
|
|
206
|
+
});
|
|
207
|
+
// Only after the graph is in place: the cache records the extractions this run actually committed,
|
|
208
|
+
// so a run that failed before writing its graph leaves the previous cache — and the mtimes it was
|
|
209
|
+
// built from — alone instead of vouching for a graph that was never written.
|
|
210
|
+
await writeParseCache(projectRoot, files);
|
|
184
211
|
const languageCounts = new Map();
|
|
185
212
|
for (const file of files)
|
|
186
213
|
languageCounts.set(file.language, (languageCounts.get(file.language) ?? 0) + 1);
|
|
@@ -191,6 +218,8 @@ async function runIndex(projectRoot, config, signal) {
|
|
|
191
218
|
projectRoot,
|
|
192
219
|
filesIndexed: files.length,
|
|
193
220
|
filesSkipped,
|
|
221
|
+
filesMinified,
|
|
222
|
+
filesReused,
|
|
194
223
|
nodeCount: graph.nodes.length,
|
|
195
224
|
edgeCount: graph.edges.length,
|
|
196
225
|
unresolvedCount: graph.unresolved.length,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection of generated (bundled or minified) code, so an index can leave it out.
|
|
3
|
+
*
|
|
4
|
+
* A workspace's build output is not always gitignored: a deployment that ships committed artifacts
|
|
5
|
+
* (`dist/` committed so a server can serve it as-is) hands the walk megabytes of bundler output that
|
|
6
|
+
* is neither source nor hidden from version control. Indexing it is not merely wasted time — those
|
|
7
|
+
* files declare thousands of short, mangled names (`e`, `t`, `n`) that collide with real source
|
|
8
|
+
* symbols, and "one unique workspace-wide name wins" then refuses to resolve every call that the
|
|
9
|
+
* duplicate makes ambiguous.
|
|
10
|
+
*
|
|
11
|
+
* Two signals, both structural rather than name-based on a build tool:
|
|
12
|
+
* - a `.min.` path segment (`bundle.min.js`) — the conventional marker, checked before any read;
|
|
13
|
+
* - an average source line longer than {@link MINIFIED_AVERAGE_LINE_LENGTH} characters, which
|
|
14
|
+
* hand-written code does not reach. Measured on real workspaces: the longest average found in
|
|
15
|
+
* hand-written source is ~70 characters, while bundled output runs from thousands to tens of
|
|
16
|
+
* thousands.
|
|
17
|
+
* @module dsh-plugin-codegraph-tree-sitter/minified
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Average line length at or above which a file is treated as generated output. Chosen with roughly
|
|
21
|
+
* 7× headroom over the widest real source line measured (~70 characters), and far below the narrowest
|
|
22
|
+
* generated sample seen (~5 000).
|
|
23
|
+
*/
|
|
24
|
+
export declare const MINIFIED_AVERAGE_LINE_LENGTH = 500;
|
|
25
|
+
/** A file's size expressed in lines, without allocating one array element per line. */
|
|
26
|
+
export interface TextShape {
|
|
27
|
+
/** Number of newline-separated lines, matching `text.split('\n').length`. */
|
|
28
|
+
readonly lineCount: number;
|
|
29
|
+
/** Mean characters per line, counting the trailing partial line. */
|
|
30
|
+
readonly averageLineLength: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Measure a file's line structure in one pass over its text.
|
|
34
|
+
* @param text - the file's decoded contents.
|
|
35
|
+
* @returns its line count and mean line length. An empty file is one empty line, matching
|
|
36
|
+
* `''.split('\n').length`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function measureText(text: string): TextShape;
|
|
39
|
+
/**
|
|
40
|
+
* Whether a path carries the conventional `.min.` marker, which needs no content read to decide.
|
|
41
|
+
* @param path - project-relative path of the file.
|
|
42
|
+
* @returns whether its own name marks it as minified.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isMinifiedName(path: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether a file is generated output the index should leave out.
|
|
47
|
+
* @param path - project-relative path of the file.
|
|
48
|
+
* @param shape - the file's measured line structure.
|
|
49
|
+
* @returns whether its name or its line shape marks it as generated.
|
|
50
|
+
*/
|
|
51
|
+
export declare function looksMinified(path: string, shape: TextShape): boolean;
|
|
52
|
+
//# sourceMappingURL=minified.d.ts.map
|
package/lib/minified.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection of generated (bundled or minified) code, so an index can leave it out.
|
|
3
|
+
*
|
|
4
|
+
* A workspace's build output is not always gitignored: a deployment that ships committed artifacts
|
|
5
|
+
* (`dist/` committed so a server can serve it as-is) hands the walk megabytes of bundler output that
|
|
6
|
+
* is neither source nor hidden from version control. Indexing it is not merely wasted time — those
|
|
7
|
+
* files declare thousands of short, mangled names (`e`, `t`, `n`) that collide with real source
|
|
8
|
+
* symbols, and "one unique workspace-wide name wins" then refuses to resolve every call that the
|
|
9
|
+
* duplicate makes ambiguous.
|
|
10
|
+
*
|
|
11
|
+
* Two signals, both structural rather than name-based on a build tool:
|
|
12
|
+
* - a `.min.` path segment (`bundle.min.js`) — the conventional marker, checked before any read;
|
|
13
|
+
* - an average source line longer than {@link MINIFIED_AVERAGE_LINE_LENGTH} characters, which
|
|
14
|
+
* hand-written code does not reach. Measured on real workspaces: the longest average found in
|
|
15
|
+
* hand-written source is ~70 characters, while bundled output runs from thousands to tens of
|
|
16
|
+
* thousands.
|
|
17
|
+
* @module dsh-plugin-codegraph-tree-sitter/minified
|
|
18
|
+
*/
|
|
19
|
+
import { posix } from 'node:path';
|
|
20
|
+
/**
|
|
21
|
+
* Average line length at or above which a file is treated as generated output. Chosen with roughly
|
|
22
|
+
* 7× headroom over the widest real source line measured (~70 characters), and far below the narrowest
|
|
23
|
+
* generated sample seen (~5 000).
|
|
24
|
+
*/
|
|
25
|
+
export const MINIFIED_AVERAGE_LINE_LENGTH = 500;
|
|
26
|
+
/**
|
|
27
|
+
* Measure a file's line structure in one pass over its text.
|
|
28
|
+
* @param text - the file's decoded contents.
|
|
29
|
+
* @returns its line count and mean line length. An empty file is one empty line, matching
|
|
30
|
+
* `''.split('\n').length`.
|
|
31
|
+
*/
|
|
32
|
+
export function measureText(text) {
|
|
33
|
+
let newlines = 0;
|
|
34
|
+
for (let index = 0; index < text.length; index++) {
|
|
35
|
+
if (text.charCodeAt(index) === 10)
|
|
36
|
+
newlines++;
|
|
37
|
+
}
|
|
38
|
+
const lineCount = newlines + 1;
|
|
39
|
+
return { lineCount, averageLineLength: text.length / lineCount };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether a path carries the conventional `.min.` marker, which needs no content read to decide.
|
|
43
|
+
* @param path - project-relative path of the file.
|
|
44
|
+
* @returns whether its own name marks it as minified.
|
|
45
|
+
*/
|
|
46
|
+
export function isMinifiedName(path) {
|
|
47
|
+
return posix.basename(path).includes('.min.');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether a file is generated output the index should leave out.
|
|
51
|
+
* @param path - project-relative path of the file.
|
|
52
|
+
* @param shape - the file's measured line structure.
|
|
53
|
+
* @returns whether its name or its line shape marks it as generated.
|
|
54
|
+
*/
|
|
55
|
+
export function looksMinified(path, shape) {
|
|
56
|
+
return isMinifiedName(path) || shape.averageLineLength >= MINIFIED_AVERAGE_LINE_LENGTH;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=minified.js.map
|
package/lib/resolve.d.ts
CHANGED
|
@@ -3,13 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pass 1 collects every definition into a name index. Pass 2 resolves each call site's callee name,
|
|
5
5
|
* in order: (1) the enclosing file imports that name from a relative specifier resolving to a file
|
|
6
|
-
* this run indexed — that file's matching declaration wins; (2) otherwise exactly one definition
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* this run indexed — that file's matching declaration wins; (2) otherwise exactly one definition **in
|
|
7
|
+
* the calling file itself** carries the name — lexical scope settles it before the workspace gets a
|
|
8
|
+
* say, which is what keeps a helper declared in two test files (or a `render` in two components) from
|
|
9
|
+
* being counted as ambiguous; (3) otherwise exactly one definition workspace-wide carries the name —
|
|
10
|
+
* it wins; (4) otherwise the call is recorded in `unresolved` and no `calls` edge is written. An
|
|
11
|
+
* ambiguous edge is worse than a missing one: the model acts on `callers` output, so a confidently
|
|
12
|
+
* wrong caller sends it to edit the wrong file, while a missing caller merely sends it back to text
|
|
13
|
+
* search.
|
|
11
14
|
*
|
|
12
|
-
* Rule
|
|
15
|
+
* Rule 2 is restricted to bare calls. A member call (`x.map()`) names a property of a receiver whose
|
|
16
|
+
* type this resolver does not track, so a same-named local declaration winning it would be a guess
|
|
17
|
+
* rather than a scope resolution — exactly the kind of confidently wrong edge rule 4 exists to avoid.
|
|
18
|
+
*
|
|
19
|
+
* Rule 4's `unresolved` entries are not one uniform signal. Each carries {@link UnresolvedRef.likelyExternal},
|
|
13
20
|
* which is true for a member call (no type information behind its receiver) or a name the file itself
|
|
14
21
|
* imports (a known external origin the specifier just didn't resolve) — both real code that was never a
|
|
15
22
|
* candidate for a workspace edge, as against a bare, undeclared name, which is the actual gap this
|