dsh-plugin-codegraph-tree-sitter 0.1.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/LICENSE +21 -0
- package/README.md +17 -0
- package/lib/extract.d.ts +70 -0
- package/lib/extract.js +296 -0
- package/lib/git-hooks.d.ts +52 -0
- package/lib/git-hooks.js +75 -0
- package/lib/gitignore.d.ts +58 -0
- package/lib/gitignore.js +107 -0
- package/lib/grammar.d.ts +27 -0
- package/lib/grammar.js +51 -0
- package/lib/index.d.ts +112 -0
- package/lib/index.js +203 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.js +25 -0
- package/lib/languages.d.ts +62 -0
- package/lib/languages.js +107 -0
- package/lib/resolve.d.ts +89 -0
- package/lib/resolve.js +218 -0
- package/lib/schema.d.ts +35 -0
- package/lib/schema.js +126 -0
- package/lib/walk.d.ts +57 -0
- package/lib/walk.js +146 -0
- package/lib/watch-policy.d.ts +49 -0
- package/lib/watch-policy.js +64 -0
- package/lib/watcher.d.ts +95 -0
- package/lib/watcher.js +299 -0
- package/lib/worktree.d.ts +41 -0
- package/lib/worktree.js +63 -0
- package/package.json +64 -0
package/lib/gitignore.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deliberately small subset of `.gitignore` pattern syntax, just enough to keep the indexer's
|
|
3
|
+
* exclusion set in step with what a project's own build tooling already hides from version control —
|
|
4
|
+
* without a new dependency on a full gitignore-matching library.
|
|
5
|
+
*
|
|
6
|
+
* Only the project root's `.gitignore` is read; per-directory `.gitignore` files nested below it are
|
|
7
|
+
* not consulted, matching the walk's own root-relative worldview.
|
|
8
|
+
*
|
|
9
|
+
* Supported:
|
|
10
|
+
* - Blank lines and lines starting with `#` are comments.
|
|
11
|
+
* - A leading `!` negates: a later matching rule re-includes a path an earlier rule excluded.
|
|
12
|
+
* - A pattern containing `/` (leading or internal) is anchored to the project root; one with no `/`
|
|
13
|
+
* at all matches at any depth, by basename.
|
|
14
|
+
* - A trailing `/` restricts the rule to directories.
|
|
15
|
+
* - `*` matches any run of characters other than `/`, within one path segment.
|
|
16
|
+
*
|
|
17
|
+
* Not supported. Each is treated as a literal character (or, for `**`, as two literal `*` globs),
|
|
18
|
+
* which only ever makes a pattern *more* specific than a real gitignore would — it degrades toward
|
|
19
|
+
* "excludes nothing extra" rather than toward "excludes too much":
|
|
20
|
+
* - `**` (explicit multi-segment wildcards — plain unanchored patterns already cross depths)
|
|
21
|
+
* - character classes (`[abc]`), `?`, and backslash escapes
|
|
22
|
+
* - git's rule that a negation cannot resurrect a path inside an already-excluded directory: here,
|
|
23
|
+
* a negation re-includes whatever it matches regardless of ancestor exclusions.
|
|
24
|
+
* @module dsh-plugin-codegraph-tree-sitter/gitignore
|
|
25
|
+
*/
|
|
26
|
+
import { readFile } from 'node:fs/promises';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
/** Regex-escape everything except the one wildcard this subset understands. */
|
|
29
|
+
function toRegExp(pattern, anchored) {
|
|
30
|
+
const escaped = pattern
|
|
31
|
+
.split('*')
|
|
32
|
+
.map(part => part.replace(/[.+^${}()|[\]\\]/g, '\\$&'))
|
|
33
|
+
.join('[^/]*');
|
|
34
|
+
// Anchored: the pattern must match the whole relative path. Unanchored: it must match the whole
|
|
35
|
+
// final path segment, at any depth — either the entire path (top level) or whatever follows the
|
|
36
|
+
// last `/` before it.
|
|
37
|
+
const body = anchored ? escaped : `(?:^|.*/)${escaped}`;
|
|
38
|
+
return new RegExp(`^${body}$`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse `.gitignore` file content into an ordered rule list.
|
|
42
|
+
* @param content - the raw file text.
|
|
43
|
+
* @returns rules in file order. Matching must apply them in order and keep the last match, per
|
|
44
|
+
* git's own precedence — a later line overrides an earlier one it conflicts with.
|
|
45
|
+
*/
|
|
46
|
+
export function parseGitignore(content) {
|
|
47
|
+
const rules = [];
|
|
48
|
+
for (const rawLine of content.split('\n')) {
|
|
49
|
+
const line = rawLine.replace(/\r$/, '');
|
|
50
|
+
if (line.trim() === '' || line.startsWith('#'))
|
|
51
|
+
continue;
|
|
52
|
+
let pattern = line;
|
|
53
|
+
let negated = false;
|
|
54
|
+
if (pattern.startsWith('!')) {
|
|
55
|
+
negated = true;
|
|
56
|
+
pattern = pattern.slice(1);
|
|
57
|
+
}
|
|
58
|
+
let directoryOnly = false;
|
|
59
|
+
if (pattern.endsWith('/')) {
|
|
60
|
+
directoryOnly = true;
|
|
61
|
+
pattern = pattern.slice(0, -1);
|
|
62
|
+
}
|
|
63
|
+
const anchored = pattern.includes('/');
|
|
64
|
+
if (pattern.startsWith('/'))
|
|
65
|
+
pattern = pattern.slice(1);
|
|
66
|
+
// A bare "/", "!", or "!/" names nothing once stripped — skip rather than build a rule that
|
|
67
|
+
// would match every path.
|
|
68
|
+
if (pattern === '')
|
|
69
|
+
continue;
|
|
70
|
+
rules.push({ pattern: toRegExp(pattern, anchored), negated, directoryOnly });
|
|
71
|
+
}
|
|
72
|
+
return rules;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether `relativePath` should be excluded per `rules`. Later rules win over earlier ones, matching
|
|
76
|
+
* git's own precedence: the loop keeps applying matches rather than stopping at the first one.
|
|
77
|
+
* @param rules - parsed rules, in file order.
|
|
78
|
+
* @param relativePath - the path to test, relative to the project root, `/`-separated.
|
|
79
|
+
* @param isDirectory - whether the path names a directory; directory-only rules skip files.
|
|
80
|
+
* @returns whether the path is excluded.
|
|
81
|
+
*/
|
|
82
|
+
export function matchesGitignore(rules, relativePath, isDirectory) {
|
|
83
|
+
let excluded = false;
|
|
84
|
+
for (const rule of rules) {
|
|
85
|
+
if (rule.directoryOnly && !isDirectory)
|
|
86
|
+
continue;
|
|
87
|
+
if (!rule.pattern.test(relativePath))
|
|
88
|
+
continue;
|
|
89
|
+
excluded = !rule.negated;
|
|
90
|
+
}
|
|
91
|
+
return excluded;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Load and parse `<projectRoot>/.gitignore`.
|
|
95
|
+
* @param projectRoot - absolute path of the project root.
|
|
96
|
+
* @returns the parsed rules, or an empty array when there is no root `.gitignore` — a missing file
|
|
97
|
+
* means "nothing extra to exclude," not a failure, so this never throws.
|
|
98
|
+
*/
|
|
99
|
+
export async function loadGitignore(projectRoot) {
|
|
100
|
+
try {
|
|
101
|
+
return parseGitignore(await readFile(join(projectRoot, '.gitignore'), 'utf8'));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=gitignore.js.map
|
package/lib/grammar.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy, process-cached grammar loading. `Parser.init()` boots one Emscripten runtime shared by every
|
|
3
|
+
* grammar; each `Language` loads from its `tree-sitter-wasms` binary only the first time a matching
|
|
4
|
+
* file is seen.
|
|
5
|
+
*
|
|
6
|
+
* Loading is lazy and per-language for a concrete reason: the reference implementation this package
|
|
7
|
+
* targets loads its full grammar set eagerly and must run its bundled Node with `--liftoff-only` to
|
|
8
|
+
* survive V8 running out of WASM code space. This harness cannot pass that flag to the Node process
|
|
9
|
+
* that runs it, so it must never load a grammar the workspace does not use.
|
|
10
|
+
* @module dsh-plugin-codegraph-tree-sitter/grammar
|
|
11
|
+
*/
|
|
12
|
+
import { Language, Parser } from 'web-tree-sitter';
|
|
13
|
+
import type { LanguageSpec } from './languages.ts';
|
|
14
|
+
/**
|
|
15
|
+
* The parsed grammar for one language, loading and caching it on first use.
|
|
16
|
+
* @param spec - the language's extraction table entry, naming its wasm file.
|
|
17
|
+
* @returns the loaded grammar, shared by every caller for the process lifetime.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadGrammar(spec: LanguageSpec): Promise<Language>;
|
|
20
|
+
/**
|
|
21
|
+
* A parser configured for one language, built fresh per call because `Parser` carries mutable parse
|
|
22
|
+
* state a concurrent caller must not share.
|
|
23
|
+
* @param spec - the language's extraction table entry.
|
|
24
|
+
* @returns a parser ready to parse source in this language.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createParser(spec: LanguageSpec): Promise<Parser>;
|
|
27
|
+
//# sourceMappingURL=grammar.d.ts.map
|
package/lib/grammar.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy, process-cached grammar loading. `Parser.init()` boots one Emscripten runtime shared by every
|
|
3
|
+
* grammar; each `Language` loads from its `tree-sitter-wasms` binary only the first time a matching
|
|
4
|
+
* file is seen.
|
|
5
|
+
*
|
|
6
|
+
* Loading is lazy and per-language for a concrete reason: the reference implementation this package
|
|
7
|
+
* targets loads its full grammar set eagerly and must run its bundled Node with `--liftoff-only` to
|
|
8
|
+
* survive V8 running out of WASM code space. This harness cannot pass that flag to the Node process
|
|
9
|
+
* that runs it, so it must never load a grammar the workspace does not use.
|
|
10
|
+
* @module dsh-plugin-codegraph-tree-sitter/grammar
|
|
11
|
+
*/
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { Language, Parser } from 'web-tree-sitter';
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
let initialized;
|
|
16
|
+
/** Boot the shared Emscripten runtime exactly once per process. */
|
|
17
|
+
function ensureInitialized() {
|
|
18
|
+
initialized ??= Parser.init();
|
|
19
|
+
return initialized;
|
|
20
|
+
}
|
|
21
|
+
const loaded = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* The parsed grammar for one language, loading and caching it on first use.
|
|
24
|
+
* @param spec - the language's extraction table entry, naming its wasm file.
|
|
25
|
+
* @returns the loaded grammar, shared by every caller for the process lifetime.
|
|
26
|
+
*/
|
|
27
|
+
export async function loadGrammar(spec) {
|
|
28
|
+
let pending = loaded.get(spec.language);
|
|
29
|
+
if (pending === undefined) {
|
|
30
|
+
pending = (async () => {
|
|
31
|
+
await ensureInitialized();
|
|
32
|
+
const wasmPath = require.resolve(`tree-sitter-wasms/out/${spec.wasmFile}`);
|
|
33
|
+
return Language.load(wasmPath);
|
|
34
|
+
})();
|
|
35
|
+
loaded.set(spec.language, pending);
|
|
36
|
+
}
|
|
37
|
+
return pending;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A parser configured for one language, built fresh per call because `Parser` carries mutable parse
|
|
41
|
+
* state a concurrent caller must not share.
|
|
42
|
+
* @param spec - the language's extraction table entry.
|
|
43
|
+
* @returns a parser ready to parse source in this language.
|
|
44
|
+
*/
|
|
45
|
+
export async function createParser(spec) {
|
|
46
|
+
const language = await loadGrammar(spec);
|
|
47
|
+
const parser = new Parser();
|
|
48
|
+
parser.setLanguage(language);
|
|
49
|
+
return parser;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=grammar.js.map
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-built code-graph indexer for `ctx.codegraph`. Parses a workspace with `web-tree-sitter` and
|
|
3
|
+
* `tree-sitter-wasms` and writes schema version 4 to `<projectRoot>/.codegraph/codegraph.db` — the
|
|
4
|
+
* same path and format `dsh-plugin-codegraph-sqlite` reads and the external `codegraph` CLI
|
|
5
|
+
* writes, so a workspace this package indexes becomes queryable through the existing store with no
|
|
6
|
+
* second graph format to disagree with the first.
|
|
7
|
+
*
|
|
8
|
+
* Registers **only an indexer**, never a store: `ctx.codegraph.index()` runs this package, and
|
|
9
|
+
* `ctx.codegraph.query()` is answered by whichever store claims the root afterward. Indexing never
|
|
10
|
+
* runs implicitly from a query — it is a caller-initiated, potentially multi-minute operation, kept
|
|
11
|
+
* out of the seam's read-only query path on purpose.
|
|
12
|
+
*
|
|
13
|
+
* Namespace plugin (named exports, no default export). Grammars load lazily, one per language, on
|
|
14
|
+
* first sight of a matching file, and stay cached for the process — never eagerly for every grammar
|
|
15
|
+
* this package ships with.
|
|
16
|
+
* @module dsh-plugin-codegraph-tree-sitter
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
import z from '@deepseek-ai/schemastery';
|
|
20
|
+
export { LANGUAGE_TABLE, languageFor } from './languages.ts';
|
|
21
|
+
export type { DefinitionRule, ImportRule, LanguageSpec } from './languages.ts';
|
|
22
|
+
export { extractFile } from './extract.ts';
|
|
23
|
+
export type { FileExtraction, RawCall, RawDefinition, RawImport } from './extract.ts';
|
|
24
|
+
export { loadGitignore, matchesGitignore, parseGitignore } from './gitignore.ts';
|
|
25
|
+
export type { GitignoreRule } from './gitignore.ts';
|
|
26
|
+
export { resolveWorkspace } from './resolve.ts';
|
|
27
|
+
export type { ExtractedFile, GraphEdge, GraphNode, ResolvedGraph, UnresolvedRef } from './resolve.ts';
|
|
28
|
+
export { SCHEMA_VERSION, writeGraph } from './schema.ts';
|
|
29
|
+
export { isExcluded, walkAndExtract } from './walk.ts';
|
|
30
|
+
export type { WalkConfig, WalkResult } from './walk.ts';
|
|
31
|
+
export { createWatcher, nodeWatch } from './watcher.ts';
|
|
32
|
+
export type { DegradeReason, WatchConfig, WatchHandle, WatchPrimitive, Watcher } from './watcher.ts';
|
|
33
|
+
export { decideWatch, readProcVersion } from './watch-policy.ts';
|
|
34
|
+
export type { WatchDecision, WatchPolicyInput } from './watch-policy.ts';
|
|
35
|
+
export { detectWorktree, execGit } from './worktree.ts';
|
|
36
|
+
export type { GitExec, WorktreeInfo } from './worktree.ts';
|
|
37
|
+
export { HOOK_MARKER_BEGIN, HOOK_MARKER_END, installGitHooks, uninstallGitHooks } from './git-hooks.ts';
|
|
38
|
+
export type { GitHooksOptions } from './git-hooks.ts';
|
|
39
|
+
/** Cordis plugin name for loader diagnostics. */
|
|
40
|
+
export declare const name = "codegraph-tree-sitter";
|
|
41
|
+
/** Services required by this plugin. */
|
|
42
|
+
export declare const inject: string[];
|
|
43
|
+
/** Path of the on-disk graph this package writes, relative to a project root. */
|
|
44
|
+
export declare const DATABASE_RELATIVE_PATH = ".codegraph/codegraph.db";
|
|
45
|
+
/** Default branded identity this indexer reserves on the seam. */
|
|
46
|
+
export declare const DEFAULT_INDEXER_ID = "codegraph-tree-sitter";
|
|
47
|
+
/** Directory segments never descended into by default. */
|
|
48
|
+
export declare const DEFAULT_EXCLUDE: string[];
|
|
49
|
+
/** Default ceiling on a single file's size before it is skipped. */
|
|
50
|
+
export declare const DEFAULT_MAX_FILE_BYTES = 2000000;
|
|
51
|
+
/** Default ceiling on how many files one run indexes. */
|
|
52
|
+
export declare const DEFAULT_MAX_FILES = 50000;
|
|
53
|
+
/** Default number of files parsed concurrently. */
|
|
54
|
+
export declare const DEFAULT_CONCURRENCY = 4;
|
|
55
|
+
/** Default quiet period after the last change before a watch-triggered refresh runs. */
|
|
56
|
+
export declare const DEFAULT_WATCH_DEBOUNCE_MS = 2000;
|
|
57
|
+
/** Default cap on directories watched individually, on platforms without a recursive `fs.watch`. */
|
|
58
|
+
export declare const DEFAULT_MAX_WATCHED_DIRECTORIES = 4000;
|
|
59
|
+
/** Plugin configuration: the indexer's identity and every deployment-varying bound on a run. */
|
|
60
|
+
export interface Config {
|
|
61
|
+
/**
|
|
62
|
+
* Branded identity to reserve on `ctx.codegraph`. Give each instance its own id when mounting more
|
|
63
|
+
* than one, so a duplicate registration fails at load instead of shadowing the first indexer.
|
|
64
|
+
*/
|
|
65
|
+
indexerId?: string;
|
|
66
|
+
/** Restrict indexing to these seam language labels when set (default: every grammar this package ships). */
|
|
67
|
+
languages?: string[];
|
|
68
|
+
/** Directory segment names never descended into (default: node_modules, dist, build, coverage, .git). */
|
|
69
|
+
exclude?: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Also exclude whatever the project root's `.gitignore` names, unioned with `exclude` (default
|
|
72
|
+
* true). Build tooling routinely writes compiled output to a gitignored directory outside the
|
|
73
|
+
* default exclude list (`lib`, `out`, ...); indexing that output alongside its own source hands the
|
|
74
|
+
* resolver two same-named declarations of one symbol and makes "the one unique workspace-wide name
|
|
75
|
+
* wins" pick between them arbitrarily. Only a practical subset of gitignore syntax is understood —
|
|
76
|
+
* see `./gitignore.ts`. A project with no root `.gitignore` is unaffected either way.
|
|
77
|
+
*/
|
|
78
|
+
respectGitignore?: boolean;
|
|
79
|
+
/** Files larger than this are skipped and counted in the report, never parsed (default 2000000). */
|
|
80
|
+
maxFileBytes?: number;
|
|
81
|
+
/** The walk stops discovering new files once this many have been found (default 50000). */
|
|
82
|
+
maxFiles?: number;
|
|
83
|
+
/** Files parsed concurrently (default 4). */
|
|
84
|
+
concurrency?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Watch the workspace for file changes and refresh the index automatically after a successful
|
|
87
|
+
* `index()` call establishes a baseline (default false). A caller that never sets this sees no
|
|
88
|
+
* change from before this option existed — indexing stays purely explicit.
|
|
89
|
+
*
|
|
90
|
+
* Overridden off by default on a WSL2 kernel watching a path mounted in from the Windows host
|
|
91
|
+
* (`/mnt/<drive>/...`), since inotify does not reliably deliver events over that 9P mount — see
|
|
92
|
+
* `watch-policy.ts`. Set `CODEGRAPH_FORCE_WATCH=1` to watch there anyway, or `CODEGRAPH_NO_WATCH=1`
|
|
93
|
+
* to force watching off everywhere regardless of this option.
|
|
94
|
+
*/
|
|
95
|
+
watch?: boolean;
|
|
96
|
+
/** Quiet period after the last change before a watch-triggered refresh runs, in ms (default 2000). */
|
|
97
|
+
watchDebounceMs?: number;
|
|
98
|
+
/**
|
|
99
|
+
* Hard cap on directories watched individually (default 4000). Only relevant on platforms without a
|
|
100
|
+
* recursive `fs.watch` (Linux); exceeding it degrades that root's watcher rather than covering only
|
|
101
|
+
* part of the tree.
|
|
102
|
+
*/
|
|
103
|
+
maxWatchedDirectories?: number;
|
|
104
|
+
}
|
|
105
|
+
export declare const Config: z<Config>;
|
|
106
|
+
/**
|
|
107
|
+
* Register the tree-sitter indexer.
|
|
108
|
+
* @param ctx - the plugin context (must inject `codegraph`).
|
|
109
|
+
* @param config - the resolved plugin configuration.
|
|
110
|
+
*/
|
|
111
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
112
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-built code-graph indexer for `ctx.codegraph`. Parses a workspace with `web-tree-sitter` and
|
|
3
|
+
* `tree-sitter-wasms` and writes schema version 4 to `<projectRoot>/.codegraph/codegraph.db` — the
|
|
4
|
+
* same path and format `dsh-plugin-codegraph-sqlite` reads and the external `codegraph` CLI
|
|
5
|
+
* writes, so a workspace this package indexes becomes queryable through the existing store with no
|
|
6
|
+
* second graph format to disagree with the first.
|
|
7
|
+
*
|
|
8
|
+
* Registers **only an indexer**, never a store: `ctx.codegraph.index()` runs this package, and
|
|
9
|
+
* `ctx.codegraph.query()` is answered by whichever store claims the root afterward. Indexing never
|
|
10
|
+
* runs implicitly from a query — it is a caller-initiated, potentially multi-minute operation, kept
|
|
11
|
+
* out of the seam's read-only query path on purpose.
|
|
12
|
+
*
|
|
13
|
+
* Namespace plugin (named exports, no default export). Grammars load lazily, one per language, on
|
|
14
|
+
* first sight of a matching file, and stay cached for the process — never eagerly for every grammar
|
|
15
|
+
* this package ships with.
|
|
16
|
+
* @module dsh-plugin-codegraph-tree-sitter
|
|
17
|
+
*/
|
|
18
|
+
import { stat } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import z from '@deepseek-ai/schemastery';
|
|
21
|
+
import { CodegraphIndexerId } from 'dsh-plugin-codegraph-service';
|
|
22
|
+
import { LANGUAGE_TABLE } from "./languages.js";
|
|
23
|
+
import { walkAndExtract } from "./walk.js";
|
|
24
|
+
import { resolveWorkspace } from "./resolve.js";
|
|
25
|
+
import { writeGraph } from "./schema.js";
|
|
26
|
+
import { createWatcher } from "./watcher.js";
|
|
27
|
+
import { decideWatch, readProcVersion } from "./watch-policy.js";
|
|
28
|
+
export { LANGUAGE_TABLE, languageFor } from "./languages.js";
|
|
29
|
+
export { extractFile } from "./extract.js";
|
|
30
|
+
export { loadGitignore, matchesGitignore, parseGitignore } from "./gitignore.js";
|
|
31
|
+
export { resolveWorkspace } from "./resolve.js";
|
|
32
|
+
export { SCHEMA_VERSION, writeGraph } from "./schema.js";
|
|
33
|
+
export { isExcluded, walkAndExtract } from "./walk.js";
|
|
34
|
+
export { createWatcher, nodeWatch } from "./watcher.js";
|
|
35
|
+
export { decideWatch, readProcVersion } from "./watch-policy.js";
|
|
36
|
+
export { detectWorktree, execGit } from "./worktree.js";
|
|
37
|
+
export { HOOK_MARKER_BEGIN, HOOK_MARKER_END, installGitHooks, uninstallGitHooks } from "./git-hooks.js";
|
|
38
|
+
/** Cordis plugin name for loader diagnostics. */
|
|
39
|
+
export const name = 'codegraph-tree-sitter';
|
|
40
|
+
/** Services required by this plugin. */
|
|
41
|
+
export const inject = ['codegraph'];
|
|
42
|
+
/** Path of the on-disk graph this package writes, relative to a project root. */
|
|
43
|
+
export const DATABASE_RELATIVE_PATH = '.codegraph/codegraph.db';
|
|
44
|
+
/** Default branded identity this indexer reserves on the seam. */
|
|
45
|
+
export const DEFAULT_INDEXER_ID = 'codegraph-tree-sitter';
|
|
46
|
+
/** Directory segments never descended into by default. */
|
|
47
|
+
export const DEFAULT_EXCLUDE = ['node_modules', 'dist', 'build', 'coverage', '.git'];
|
|
48
|
+
/** Default ceiling on a single file's size before it is skipped. */
|
|
49
|
+
export const DEFAULT_MAX_FILE_BYTES = 2_000_000;
|
|
50
|
+
/** Default ceiling on how many files one run indexes. */
|
|
51
|
+
export const DEFAULT_MAX_FILES = 50_000;
|
|
52
|
+
/** Default number of files parsed concurrently. */
|
|
53
|
+
export const DEFAULT_CONCURRENCY = 4;
|
|
54
|
+
/** Default quiet period after the last change before a watch-triggered refresh runs. */
|
|
55
|
+
export const DEFAULT_WATCH_DEBOUNCE_MS = 2_000;
|
|
56
|
+
/** Default cap on directories watched individually, on platforms without a recursive `fs.watch`. */
|
|
57
|
+
export const DEFAULT_MAX_WATCHED_DIRECTORIES = 4_000;
|
|
58
|
+
export const Config = z.object({
|
|
59
|
+
indexerId: z.string().default(DEFAULT_INDEXER_ID),
|
|
60
|
+
languages: z.array(z.string()).default([...new Set(LANGUAGE_TABLE.map(spec => spec.language))]),
|
|
61
|
+
exclude: z.array(z.string()).default(DEFAULT_EXCLUDE),
|
|
62
|
+
respectGitignore: z.boolean().default(true),
|
|
63
|
+
maxFileBytes: z.number().default(DEFAULT_MAX_FILE_BYTES),
|
|
64
|
+
maxFiles: z.number().default(DEFAULT_MAX_FILES),
|
|
65
|
+
concurrency: z.number().default(DEFAULT_CONCURRENCY),
|
|
66
|
+
watch: z.boolean().default(false),
|
|
67
|
+
watchDebounceMs: z.number().default(DEFAULT_WATCH_DEBOUNCE_MS),
|
|
68
|
+
maxWatchedDirectories: z.number().default(DEFAULT_MAX_WATCHED_DIRECTORIES),
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* Register the tree-sitter indexer.
|
|
72
|
+
* @param ctx - the plugin context (must inject `codegraph`).
|
|
73
|
+
* @param config - the resolved plugin configuration.
|
|
74
|
+
*/
|
|
75
|
+
export function apply(ctx, config) {
|
|
76
|
+
const resolved = config;
|
|
77
|
+
assertPositiveInteger('maxFileBytes', resolved.maxFileBytes);
|
|
78
|
+
assertPositiveInteger('maxFiles', resolved.maxFiles);
|
|
79
|
+
assertPositiveInteger('concurrency', resolved.concurrency);
|
|
80
|
+
assertPositiveInteger('watchDebounceMs', resolved.watchDebounceMs);
|
|
81
|
+
assertPositiveInteger('maxWatchedDirectories', resolved.maxWatchedDirectories);
|
|
82
|
+
// Keyed by project root, private to this plugin instance: never registered on `ctx`, so there is no
|
|
83
|
+
// cross-plugin relation here for an invariant to describe — just process-local bookkeeping this
|
|
84
|
+
// effect's cleanup below tears down on unload.
|
|
85
|
+
const watchers = new Map();
|
|
86
|
+
/** Start (or leave running) file watching for `projectRoot`, once it has a baseline to refresh. */
|
|
87
|
+
function ensureWatching(projectRoot) {
|
|
88
|
+
if (watchers.has(projectRoot))
|
|
89
|
+
return;
|
|
90
|
+
const decision = decideWatch({
|
|
91
|
+
configuredWatch: resolved.watch,
|
|
92
|
+
root: projectRoot,
|
|
93
|
+
env: process.env,
|
|
94
|
+
platform: process.platform,
|
|
95
|
+
procVersion: readProcVersion,
|
|
96
|
+
});
|
|
97
|
+
if (!decision.enabled)
|
|
98
|
+
return;
|
|
99
|
+
const watcher = createWatcher({
|
|
100
|
+
root: projectRoot,
|
|
101
|
+
exclude: resolved.exclude,
|
|
102
|
+
respectGitignore: resolved.respectGitignore,
|
|
103
|
+
debounceMs: resolved.watchDebounceMs,
|
|
104
|
+
maxWatchedDirectories: resolved.maxWatchedDirectories,
|
|
105
|
+
sync: () => runSync(projectRoot, resolved),
|
|
106
|
+
// No diagnostics service is wired into this package's dependencies; this is the floor for
|
|
107
|
+
// "degradation must be visible" until one is, per NOTES.local.md.
|
|
108
|
+
onDegraded: (reason) => {
|
|
109
|
+
console.error(`codegraph-tree-sitter: file watching for "${projectRoot}" stopped permanently (${reason.code}); ` +
|
|
110
|
+
'the index will no longer refresh on its own — run codegraph_index manually to update it.');
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
watchers.set(projectRoot, watcher);
|
|
114
|
+
watcher.start();
|
|
115
|
+
}
|
|
116
|
+
const indexer = {
|
|
117
|
+
id: CodegraphIndexerId(resolved.indexerId),
|
|
118
|
+
async canIndex(projectRoot) {
|
|
119
|
+
try {
|
|
120
|
+
return (await stat(projectRoot)).isDirectory();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Only the existence probe runs in the try. Any rejection — missing path, or no permission
|
|
124
|
+
// to look — means this indexer cannot build a graph here, which is the answer the seam asked
|
|
125
|
+
// for rather than a failure to report.
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
async index(projectRoot, signal) {
|
|
130
|
+
const report = await runIndex(projectRoot, resolved, signal);
|
|
131
|
+
// Watching starts only after a successful index establishes the baseline it refreshes — never
|
|
132
|
+
// from a canIndex() probe or a failed run.
|
|
133
|
+
ensureWatching(projectRoot);
|
|
134
|
+
return report;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
ctx.effect(function* () {
|
|
138
|
+
yield () => {
|
|
139
|
+
for (const watcher of watchers.values())
|
|
140
|
+
watcher.stop();
|
|
141
|
+
watchers.clear();
|
|
142
|
+
};
|
|
143
|
+
yield ctx.codegraph.registerIndexer(indexer);
|
|
144
|
+
}, 'codegraph-tree-sitter');
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Run one watch-triggered reindex. Always a full rebuild in this version — `runIndex` has no notion of
|
|
148
|
+
* a file subset — so the watcher's own collected paths are not consulted here.
|
|
149
|
+
* @param projectRoot - absolute path of the workspace to refresh.
|
|
150
|
+
* @param config - the resolved plugin configuration.
|
|
151
|
+
* @returns how many files the rebuild indexed, and how long it took.
|
|
152
|
+
*/
|
|
153
|
+
async function runSync(projectRoot, config) {
|
|
154
|
+
const startedAt = Date.now();
|
|
155
|
+
const report = await runIndex(projectRoot, config);
|
|
156
|
+
return { filesChanged: report.filesIndexed, durationMs: Date.now() - startedAt };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* One indexing run: walk, parse, resolve, and write.
|
|
160
|
+
* @param projectRoot - absolute path of the workspace to index.
|
|
161
|
+
* @param config - the resolved plugin configuration.
|
|
162
|
+
* @param signal - aborts the run.
|
|
163
|
+
* @returns the run's report.
|
|
164
|
+
*/
|
|
165
|
+
async function runIndex(projectRoot, config, signal) {
|
|
166
|
+
const { files, filesSkipped } = await walkAndExtract(projectRoot, {
|
|
167
|
+
exclude: config.exclude,
|
|
168
|
+
respectGitignore: config.respectGitignore,
|
|
169
|
+
maxFileBytes: config.maxFileBytes,
|
|
170
|
+
maxFiles: config.maxFiles,
|
|
171
|
+
concurrency: config.concurrency,
|
|
172
|
+
languages: config.languages,
|
|
173
|
+
}, signal);
|
|
174
|
+
signal?.throwIfAborted();
|
|
175
|
+
const indexedAt = Date.now();
|
|
176
|
+
const graph = resolveWorkspace(files, indexedAt);
|
|
177
|
+
signal?.throwIfAborted();
|
|
178
|
+
const databasePath = join(projectRoot, DATABASE_RELATIVE_PATH);
|
|
179
|
+
await writeGraph(databasePath, { files, nodes: graph.nodes, edges: graph.edges, unresolved: graph.unresolved, indexedAt });
|
|
180
|
+
const languageCounts = new Map();
|
|
181
|
+
for (const file of files)
|
|
182
|
+
languageCounts.set(file.language, (languageCounts.get(file.language) ?? 0) + 1);
|
|
183
|
+
const languages = [...languageCounts.entries()]
|
|
184
|
+
.map(([language, fileCount]) => ({ language, fileCount }))
|
|
185
|
+
.sort((left, right) => right.fileCount - left.fileCount || left.language.localeCompare(right.language));
|
|
186
|
+
return {
|
|
187
|
+
projectRoot,
|
|
188
|
+
filesIndexed: files.length,
|
|
189
|
+
filesSkipped,
|
|
190
|
+
nodeCount: graph.nodes.length,
|
|
191
|
+
edgeCount: graph.edges.length,
|
|
192
|
+
unresolvedCount: graph.unresolved.length,
|
|
193
|
+
unresolvedLikelyInternalCount: graph.unresolved.filter(ref => !ref.likelyExternal).length,
|
|
194
|
+
languages,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** Reject a non-positive-integer config value at load, so misconfiguration fails loud. */
|
|
198
|
+
function assertPositiveInteger(field, value) {
|
|
199
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
200
|
+
throw new Error(`codegraph-tree-sitter: ${field} must be a positive integer`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-plugin-codegraph-tree-sitter`.
|
|
3
|
+
* @module dsh-plugin-codegraph-tree-sitter/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "codegraph-tree-sitter-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-plugin-codegraph-tree-sitter`.
|
|
3
|
+
* @module dsh-plugin-codegraph-tree-sitter/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = 'dsh-plugin-codegraph-tree-sitter';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'codegraph-tree-sitter-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* No runtime invariant: an indexing run is a one-shot, caller-awaited operation, and the optional file
|
|
12
|
+
* watcher this package can start keeps its per-root state (`Watcher` instances, debounce timers,
|
|
13
|
+
* pending-path sets) private to the plugin's own closure — never registered on `ctx` or exposed to
|
|
14
|
+
* another plugin. There is no cross-plugin relation here for an invariant to describe, only
|
|
15
|
+
* process-local bookkeeping the plugin's own `ctx.effect` cleanup already tears down on unload.
|
|
16
|
+
*/
|
|
17
|
+
const install = () => { };
|
|
18
|
+
/**
|
|
19
|
+
* Register this package's invariant companion.
|
|
20
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
21
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
22
|
+
*/
|
|
23
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
24
|
+
/* jscpd:ignore-end */
|
|
25
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-language extraction tables: which tree-sitter node types are definitions, which are call
|
|
3
|
+
* sites, and which anchor an import, for every grammar this package loads today.
|
|
4
|
+
*
|
|
5
|
+
* The table is deliberately small. It covers the definition/call/import core the seam's operations
|
|
6
|
+
* depend on — not every declaration shape a language can produce — because the alternative is porting
|
|
7
|
+
* the reference indexer's language-specific resolution, which the proposal this package implements
|
|
8
|
+
* defers until the core proves itself against real workspaces.
|
|
9
|
+
* @module dsh-plugin-codegraph-tree-sitter/languages
|
|
10
|
+
*/
|
|
11
|
+
/** One tree-sitter node type that introduces a declaration, mapped to the seam's `NODE_KINDS`. */
|
|
12
|
+
export interface DefinitionRule {
|
|
13
|
+
/** The tree-sitter node type this rule matches, e.g. `function_declaration`. */
|
|
14
|
+
readonly nodeType: string;
|
|
15
|
+
/** The seam node kind to record, e.g. `function`. See `NODE_KINDS` in `dsh-plugin-codegraph-service`. */
|
|
16
|
+
readonly kind: string;
|
|
17
|
+
/** The field holding the declared name; `childForFieldName(nameField)` must be an identifier. */
|
|
18
|
+
readonly nameField: string;
|
|
19
|
+
/**
|
|
20
|
+
* Present only when this rule is conditional on the shape of a value it binds — a
|
|
21
|
+
* `variable_declarator` is a function definition when its `value` is a function expression, and is
|
|
22
|
+
* otherwise not extracted at all. Field and matching types travel together so a rule can never
|
|
23
|
+
* declare one without the other.
|
|
24
|
+
*/
|
|
25
|
+
readonly value?: {
|
|
26
|
+
/** Field naming the value assigned to the binding. */
|
|
27
|
+
readonly field: string;
|
|
28
|
+
/** Node types the field must resolve to for this rule to match. */
|
|
29
|
+
readonly types: readonly string[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** One tree-sitter node type recording a relative-import binding, per language family. */
|
|
33
|
+
export interface ImportRule {
|
|
34
|
+
/** The tree-sitter node type anchoring one import statement, e.g. `import_statement`. */
|
|
35
|
+
readonly statementType: string;
|
|
36
|
+
}
|
|
37
|
+
/** One language grammar this package can load and extract from. */
|
|
38
|
+
export interface LanguageSpec {
|
|
39
|
+
/** The seam's language label. See `LANGUAGES` in `dsh-plugin-codegraph-service`. */
|
|
40
|
+
readonly language: string;
|
|
41
|
+
/** File extensions routed to this grammar, each including the leading dot. */
|
|
42
|
+
readonly extensions: readonly string[];
|
|
43
|
+
/** The `tree-sitter-wasms` package-relative wasm file this grammar loads from. */
|
|
44
|
+
readonly wasmFile: string;
|
|
45
|
+
/** Definition rules tried in order for every named node the walk visits. */
|
|
46
|
+
readonly definitions: readonly DefinitionRule[];
|
|
47
|
+
/** Node types whose {@link callFunctionField} names the callee. */
|
|
48
|
+
readonly callTypes: readonly string[];
|
|
49
|
+
/** Field on a call node holding the callee expression. */
|
|
50
|
+
readonly callFunctionField: string;
|
|
51
|
+
/** Node types anchoring one import statement. */
|
|
52
|
+
readonly importTypes: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
/** Every grammar this package loads, keyed by the seam language label it produces. */
|
|
55
|
+
export declare const LANGUAGE_TABLE: readonly LanguageSpec[];
|
|
56
|
+
/**
|
|
57
|
+
* The grammar for a file extension, or `undefined` when this package indexes nothing for it.
|
|
58
|
+
* @param extension - the file extension, including the leading dot, e.g. `.ts`.
|
|
59
|
+
* @returns the matching language's extraction table entry, or `undefined`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function languageFor(extension: string): LanguageSpec | undefined;
|
|
62
|
+
//# sourceMappingURL=languages.d.ts.map
|