@optave/codegraph 3.9.4 → 3.9.6
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/README.md +10 -10
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +3 -2
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/rules/csharp.d.ts.map +1 -1
- package/dist/ast-analysis/rules/csharp.js +8 -1
- package/dist/ast-analysis/rules/csharp.js.map +1 -1
- package/dist/ast-analysis/rules/go.d.ts.map +1 -1
- package/dist/ast-analysis/rules/go.js +4 -1
- package/dist/ast-analysis/rules/go.js.map +1 -1
- package/dist/ast-analysis/rules/index.d.ts +6 -0
- package/dist/ast-analysis/rules/index.d.ts.map +1 -1
- package/dist/ast-analysis/rules/index.js +151 -4
- package/dist/ast-analysis/rules/index.js.map +1 -1
- package/dist/ast-analysis/rules/java.d.ts.map +1 -1
- package/dist/ast-analysis/rules/java.js +5 -1
- package/dist/ast-analysis/rules/java.js.map +1 -1
- package/dist/ast-analysis/rules/php.d.ts.map +1 -1
- package/dist/ast-analysis/rules/php.js +6 -1
- package/dist/ast-analysis/rules/php.js.map +1 -1
- package/dist/ast-analysis/rules/python.d.ts.map +1 -1
- package/dist/ast-analysis/rules/python.js +5 -1
- package/dist/ast-analysis/rules/python.js.map +1 -1
- package/dist/ast-analysis/rules/ruby.d.ts.map +1 -1
- package/dist/ast-analysis/rules/ruby.js +4 -1
- package/dist/ast-analysis/rules/ruby.js.map +1 -1
- package/dist/ast-analysis/rules/rust.d.ts.map +1 -1
- package/dist/ast-analysis/rules/rust.js +5 -1
- package/dist/ast-analysis/rules/rust.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts +2 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +129 -37
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +2 -0
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli.js +24 -1
- package/dist/cli.js.map +1 -1
- package/dist/domain/graph/builder/context.d.ts +2 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +13 -2
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +30 -4
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +141 -3
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.js +58 -26
- package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +54 -45
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +17 -0
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/journal.d.ts +15 -0
- package/dist/domain/graph/journal.d.ts.map +1 -1
- package/dist/domain/graph/journal.js +283 -28
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts +17 -0
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +23 -7
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +53 -4
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +278 -80
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/generator.d.ts.map +1 -1
- package/dist/domain/search/generator.js +28 -2
- package/dist/domain/search/generator.js.map +1 -1
- package/dist/domain/search/models.js +1 -1
- package/dist/domain/wasm-worker-entry.d.ts +24 -0
- package/dist/domain/wasm-worker-entry.d.ts.map +1 -0
- package/dist/domain/wasm-worker-entry.js +644 -0
- package/dist/domain/wasm-worker-entry.js.map +1 -0
- package/dist/domain/wasm-worker-pool.d.ts +59 -0
- package/dist/domain/wasm-worker-pool.d.ts.map +1 -0
- package/dist/domain/wasm-worker-pool.js +312 -0
- package/dist/domain/wasm-worker-pool.js.map +1 -0
- package/dist/domain/wasm-worker-protocol.d.ts +65 -0
- package/dist/domain/wasm-worker-protocol.d.ts.map +1 -0
- package/dist/domain/wasm-worker-protocol.js +13 -0
- package/dist/domain/wasm-worker-protocol.js.map +1 -0
- package/dist/extractors/javascript.js +146 -2
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/ast.d.ts.map +1 -1
- package/dist/features/ast.js +11 -9
- package/dist/features/ast.js.map +1 -1
- package/dist/features/boundaries.d.ts +2 -2
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +2 -31
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/snapshot.d.ts.map +1 -1
- package/dist/features/snapshot.js +99 -13
- package/dist/features/snapshot.js.map +1 -1
- package/dist/graph/algorithms/louvain.d.ts.map +1 -1
- package/dist/graph/algorithms/louvain.js +2 -4
- package/dist/graph/algorithms/louvain.js.map +1 -1
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +12 -2
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/shared/globs.d.ts +40 -0
- package/dist/shared/globs.d.ts.map +1 -0
- package/dist/shared/globs.js +126 -0
- package/dist/shared/globs.js.map +1 -0
- package/dist/types.d.ts +26 -1
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +11 -1
- package/src/ast-analysis/rules/csharp.ts +8 -1
- package/src/ast-analysis/rules/go.ts +4 -1
- package/src/ast-analysis/rules/index.ts +181 -4
- package/src/ast-analysis/rules/java.ts +5 -1
- package/src/ast-analysis/rules/php.ts +6 -1
- package/src/ast-analysis/rules/python.ts +5 -1
- package/src/ast-analysis/rules/ruby.ts +4 -1
- package/src/ast-analysis/rules/rust.ts +5 -1
- package/src/ast-analysis/visitors/ast-store-visitor.ts +129 -34
- package/src/cli/commands/watch.ts +2 -0
- package/src/cli.ts +31 -8
- package/src/domain/graph/builder/context.ts +2 -0
- package/src/domain/graph/builder/helpers.ts +53 -3
- package/src/domain/graph/builder/pipeline.ts +162 -3
- package/src/domain/graph/builder/stages/collect-files.ts +56 -26
- package/src/domain/graph/builder/stages/detect-changes.ts +57 -49
- package/src/domain/graph/builder/stages/finalize.ts +16 -0
- package/src/domain/graph/journal.ts +284 -27
- package/src/domain/graph/watcher.ts +29 -9
- package/src/domain/parser.ts +288 -73
- package/src/domain/search/generator.ts +34 -2
- package/src/domain/search/models.ts +1 -1
- package/src/domain/wasm-worker-entry.ts +798 -0
- package/src/domain/wasm-worker-pool.ts +330 -0
- package/src/domain/wasm-worker-protocol.ts +81 -0
- package/src/extractors/javascript.ts +149 -2
- package/src/features/ast.ts +22 -9
- package/src/features/boundaries.ts +2 -27
- package/src/features/snapshot.ts +93 -14
- package/src/graph/algorithms/louvain.ts +2 -4
- package/src/infrastructure/config.ts +12 -2
- package/src/shared/globs.ts +121 -0
- package/src/types.ts +26 -1
package/src/features/snapshot.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { getDatabase } from '../db/better-sqlite3.js';
|
|
@@ -37,24 +38,77 @@ export function snapshotSave(
|
|
|
37
38
|
const dir = snapshotsDir(dbPath);
|
|
38
39
|
const dest = path.join(dir, `${name}.db`);
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
fs.unlinkSync(dest);
|
|
45
|
-
debug(`Deleted existing snapshot: ${dest}`);
|
|
41
|
+
// Cheap fail-fast for the common non-force case; the authoritative check
|
|
42
|
+
// below uses an atomic linkSync that closes the TOCTOU window.
|
|
43
|
+
if (!options.force && fs.existsSync(dest)) {
|
|
44
|
+
throw new ConfigError(`Snapshot "${name}" already exists. Use --force to overwrite.`);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
fs.mkdirSync(dir, { recursive: true });
|
|
49
48
|
|
|
49
|
+
// VACUUM INTO a unique temp path on the same filesystem, then atomically
|
|
50
|
+
// place it at the destination. This closes the TOCTOU window between
|
|
51
|
+
// existsSync/unlinkSync/VACUUM INTO where two concurrent saves could
|
|
52
|
+
// observe a missing file or interleave their VACUUM writes.
|
|
53
|
+
//
|
|
54
|
+
// Unique temp name: process.pid is shared across worker_threads in the
|
|
55
|
+
// same process, so we add random bytes to keep concurrent callers in any
|
|
56
|
+
// thread from colliding on the temp path.
|
|
57
|
+
const tmp = path.join(
|
|
58
|
+
dir,
|
|
59
|
+
`.${name}.db.tmp-${process.pid}-${Date.now()}-${randomBytes(6).toString('hex')}`,
|
|
60
|
+
);
|
|
61
|
+
try {
|
|
62
|
+
fs.unlinkSync(tmp);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
const Database = getDatabase();
|
|
51
68
|
const db = new Database(dbPath, { readonly: true });
|
|
52
69
|
try {
|
|
53
|
-
db.exec(`VACUUM INTO '${
|
|
70
|
+
db.exec(`VACUUM INTO '${tmp.replace(/'/g, "''")}'`);
|
|
54
71
|
} finally {
|
|
55
72
|
db.close();
|
|
56
73
|
}
|
|
57
74
|
|
|
75
|
+
try {
|
|
76
|
+
if (options.force) {
|
|
77
|
+
// renameSync overwrites atomically — the correct semantics for --force.
|
|
78
|
+
fs.renameSync(tmp, dest);
|
|
79
|
+
} else {
|
|
80
|
+
// Non-force path: linkSync fails atomically with EEXIST if dest exists,
|
|
81
|
+
// closing the TOCTOU window between existsSync above and the final
|
|
82
|
+
// placement. We then unlink the temp file; on POSIX and NTFS, link
|
|
83
|
+
// creates a second reference so tmp can safely be removed.
|
|
84
|
+
try {
|
|
85
|
+
fs.linkSync(tmp, dest);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
88
|
+
throw new ConfigError(`Snapshot "${name}" already exists. Use --force to overwrite.`);
|
|
89
|
+
}
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
fs.unlinkSync(tmp);
|
|
94
|
+
} catch (cleanupErr) {
|
|
95
|
+
// Best-effort — dest is already in place, so a leftover tmp file is
|
|
96
|
+
// harmless. Log at debug so repeated failures surface during
|
|
97
|
+
// troubleshooting without noising up normal operation.
|
|
98
|
+
debug(`snapshotSave: failed to remove temp file ${tmp}: ${cleanupErr}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
try {
|
|
103
|
+
fs.unlinkSync(tmp);
|
|
104
|
+
} catch (cleanupErr) {
|
|
105
|
+
if ((cleanupErr as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
106
|
+
debug(`snapshotSave: failed to remove temp file ${tmp}: ${cleanupErr}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
|
|
58
112
|
const stat = fs.statSync(dest);
|
|
59
113
|
debug(`Snapshot saved: ${dest} (${stat.size} bytes)`);
|
|
60
114
|
return { name, path: dest, size: stat.size };
|
|
@@ -74,16 +128,38 @@ export function snapshotRestore(name: string, options: SnapshotDbPathOptions = {
|
|
|
74
128
|
throw new DbError(`Snapshot "${name}" not found at ${src}`, { file: src });
|
|
75
129
|
}
|
|
76
130
|
|
|
77
|
-
// Remove WAL/SHM
|
|
131
|
+
// Remove WAL/SHM sidecars first so the old journal can't be replayed over
|
|
132
|
+
// the restored DB. unlink then check ENOENT — avoids the existsSync/unlinkSync
|
|
133
|
+
// race another process could wedge into.
|
|
78
134
|
for (const suffix of ['-wal', '-shm']) {
|
|
79
135
|
const sidecar = dbPath + suffix;
|
|
80
|
-
|
|
136
|
+
try {
|
|
81
137
|
fs.unlinkSync(sidecar);
|
|
82
138
|
debug(`Removed sidecar: ${sidecar}`);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Copy to a temp path next to the DB, then rename atomically. Readers that
|
|
145
|
+
// open dbPath during restore see either the pre-restore or post-restore
|
|
146
|
+
// file, never a partially-written one.
|
|
147
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
148
|
+
const tmp = `${dbPath}.restore-tmp-${process.pid}-${Date.now()}-${randomBytes(6).toString('hex')}`;
|
|
149
|
+
try {
|
|
150
|
+
fs.copyFileSync(src, tmp);
|
|
151
|
+
fs.renameSync(tmp, dbPath);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
try {
|
|
154
|
+
fs.unlinkSync(tmp);
|
|
155
|
+
} catch (cleanupErr) {
|
|
156
|
+
if ((cleanupErr as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
157
|
+
debug(`snapshotRestore: failed to remove temp file ${tmp}: ${cleanupErr}`);
|
|
158
|
+
}
|
|
83
159
|
}
|
|
160
|
+
throw err;
|
|
84
161
|
}
|
|
85
162
|
|
|
86
|
-
fs.copyFileSync(src, dbPath);
|
|
87
163
|
debug(`Restored snapshot "${name}" → ${dbPath}`);
|
|
88
164
|
}
|
|
89
165
|
|
|
@@ -122,10 +198,13 @@ export function snapshotDelete(name: string, options: SnapshotDbPathOptions = {}
|
|
|
122
198
|
const dir = snapshotsDir(dbPath);
|
|
123
199
|
const target = path.join(dir, `${name}.db`);
|
|
124
200
|
|
|
125
|
-
|
|
126
|
-
|
|
201
|
+
try {
|
|
202
|
+
fs.unlinkSync(target);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
205
|
+
throw new DbError(`Snapshot "${name}" not found at ${target}`, { file: target });
|
|
206
|
+
}
|
|
207
|
+
throw err;
|
|
127
208
|
}
|
|
128
|
-
|
|
129
|
-
fs.unlinkSync(target);
|
|
130
209
|
debug(`Deleted snapshot: ${target}`);
|
|
131
210
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* JS fallback: Leiden algorithm via `detectClusters` (always undirected, `directed: false`).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { debug } from '../../infrastructure/logger.js';
|
|
10
10
|
import { loadNative } from '../../infrastructure/native.js';
|
|
11
11
|
import type { CodeGraph } from '../model.js';
|
|
12
12
|
import type { DetectClustersResult } from './leiden/index.js';
|
|
@@ -36,10 +36,8 @@ export function louvainCommunities(graph: CodeGraph, opts: LouvainOptions = {}):
|
|
|
36
36
|
|
|
37
37
|
const native = loadNative();
|
|
38
38
|
if (native?.louvainCommunities) {
|
|
39
|
-
// maxLevels, maxLocalPasses, and refinementTheta are Leiden-specific tuning knobs
|
|
40
|
-
// not supported by the Rust Louvain implementation. Warn callers who set them.
|
|
41
39
|
if (opts.maxLevels != null || opts.maxLocalPasses != null || opts.refinementTheta != null) {
|
|
42
|
-
|
|
40
|
+
debug(
|
|
43
41
|
'louvainCommunities: maxLevels/maxLocalPasses/refinementTheta are ignored by the native Rust path',
|
|
44
42
|
);
|
|
45
43
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { toErrorMessage } from '../shared/errors.js';
|
|
4
|
+
import { ConfigError, toErrorMessage } from '../shared/errors.js';
|
|
5
5
|
import type { CodegraphConfig } from '../types.js';
|
|
6
6
|
import { debug, warn } from './logger.js';
|
|
7
7
|
|
|
@@ -179,6 +179,7 @@ export function loadConfig(cwd?: string): CodegraphConfig {
|
|
|
179
179
|
_configCache.set(cwd, structuredClone(result));
|
|
180
180
|
return result;
|
|
181
181
|
} catch (err: unknown) {
|
|
182
|
+
if (err instanceof ConfigError) throw err;
|
|
182
183
|
debug(`Failed to parse config ${filePath}: ${toErrorMessage(err)}`);
|
|
183
184
|
}
|
|
184
185
|
}
|
|
@@ -215,7 +216,16 @@ export function applyEnvOverrides(config: CodegraphConfig): CodegraphConfig {
|
|
|
215
216
|
|
|
216
217
|
export function resolveSecrets(config: CodegraphConfig): CodegraphConfig {
|
|
217
218
|
const cmd = config.llm.apiKeyCommand;
|
|
218
|
-
if (
|
|
219
|
+
if (cmd == null) return config;
|
|
220
|
+
if (typeof cmd !== 'string') {
|
|
221
|
+
const actual = Array.isArray(cmd) ? 'array' : typeof cmd;
|
|
222
|
+
throw new ConfigError(
|
|
223
|
+
`llm.apiKeyCommand must be a string (received ${actual}). ` +
|
|
224
|
+
'The command is split on whitespace and executed without a shell. ' +
|
|
225
|
+
'Example: "apiKeyCommand": "op read op://vault/openai/api-key"',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (cmd.trim() === '') return config;
|
|
219
229
|
|
|
220
230
|
const parts = cmd.trim().split(/\s+/);
|
|
221
231
|
const [executable, ...args] = parts;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob → RegExp conversion utilities.
|
|
3
|
+
*
|
|
4
|
+
* Shared by boundary rules (`features/boundaries.ts`) and the file-collection
|
|
5
|
+
* include/exclude filters (`domain/graph/builder/helpers.ts`). Keeping a single
|
|
6
|
+
* implementation ensures users get consistent glob semantics everywhere.
|
|
7
|
+
*
|
|
8
|
+
* Supported syntax:
|
|
9
|
+
* - `**` matches any sequence of characters including `/`
|
|
10
|
+
* - `*` matches any sequence of characters except `/`
|
|
11
|
+
* - `?` matches a single non-slash character
|
|
12
|
+
* - other regex metacharacters are escaped literally
|
|
13
|
+
*
|
|
14
|
+
* Paths must use forward slashes (callers normalize before testing).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compile a glob pattern into a `RegExp` anchored with `^…$`.
|
|
19
|
+
*/
|
|
20
|
+
export function globToRegex(pattern: string): RegExp {
|
|
21
|
+
let re = '';
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < pattern.length) {
|
|
24
|
+
const ch = pattern[i] as string;
|
|
25
|
+
if (ch === '*' && pattern[i + 1] === '*') {
|
|
26
|
+
i += 2;
|
|
27
|
+
if (pattern[i] === '/') {
|
|
28
|
+
// `**/` matches zero or more full path segments, preserving the
|
|
29
|
+
// directory boundary before the next segment. Without this, patterns
|
|
30
|
+
// like `**/foo.ts` would compile to `^.*foo\.ts$` and match
|
|
31
|
+
// `barfoo.ts`, diverging from Rust `globset` semantics.
|
|
32
|
+
re += '(?:[^/]+/)*';
|
|
33
|
+
i++;
|
|
34
|
+
} else {
|
|
35
|
+
// Bare `**` (e.g. `dir/**`, or trailing) matches anything.
|
|
36
|
+
re += '.*';
|
|
37
|
+
}
|
|
38
|
+
} else if (ch === '*') {
|
|
39
|
+
re += '[^/]*';
|
|
40
|
+
i++;
|
|
41
|
+
} else if (ch === '?') {
|
|
42
|
+
re += '[^/]';
|
|
43
|
+
i++;
|
|
44
|
+
} else if (/[.+^${}()|[\]\\]/.test(ch)) {
|
|
45
|
+
re += `\\${ch}`;
|
|
46
|
+
i++;
|
|
47
|
+
} else {
|
|
48
|
+
re += ch;
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return new RegExp(`^${re}$`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const EMPTY_REGEX_LIST: readonly RegExp[] = Object.freeze([]) as readonly RegExp[];
|
|
56
|
+
|
|
57
|
+
// Compile results are cached by pattern content so a long-running host
|
|
58
|
+
// (watch mode, MCP server) doesn't recompile on every buildGraph call.
|
|
59
|
+
// Capped to avoid unbounded growth when callers pass many distinct lists.
|
|
60
|
+
const COMPILE_CACHE_MAX = 32;
|
|
61
|
+
const compileCache = new Map<string, readonly RegExp[]>();
|
|
62
|
+
|
|
63
|
+
function buildCacheKey(patterns: readonly string[]): string {
|
|
64
|
+
// JSON.stringify avoids ambiguity when patterns legitimately contain any
|
|
65
|
+
// single character (including control characters or separators a caller
|
|
66
|
+
// might choose): ["a", "bc"] → '["a","bc"]' vs ["ab", "c"] → '["ab","c"]'.
|
|
67
|
+
return JSON.stringify(patterns);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Compile a list of glob patterns. Invalid / empty patterns are skipped.
|
|
72
|
+
*
|
|
73
|
+
* Results are memoized per pattern-content so repeated `buildGraph` calls
|
|
74
|
+
* with the same include/exclude lists reuse the compiled regexes. The
|
|
75
|
+
* returned array is shared across callers and must not be mutated.
|
|
76
|
+
*/
|
|
77
|
+
export function compileGlobs(patterns: readonly string[] | undefined): readonly RegExp[] {
|
|
78
|
+
if (!patterns || patterns.length === 0) return EMPTY_REGEX_LIST;
|
|
79
|
+
const key = buildCacheKey(patterns);
|
|
80
|
+
const cached = compileCache.get(key);
|
|
81
|
+
if (cached) return cached;
|
|
82
|
+
const out: RegExp[] = [];
|
|
83
|
+
for (const p of patterns) {
|
|
84
|
+
if (typeof p !== 'string' || p.length === 0) continue;
|
|
85
|
+
try {
|
|
86
|
+
out.push(globToRegex(p));
|
|
87
|
+
} catch {
|
|
88
|
+
// Ignore malformed patterns rather than failing the whole build.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const frozen = Object.freeze(out) as readonly RegExp[];
|
|
92
|
+
if (compileCache.size >= COMPILE_CACHE_MAX) {
|
|
93
|
+
// FIFO eviction — Map iterates insertion order. Config pattern sets
|
|
94
|
+
// are small and stable, so a simple cap is sufficient.
|
|
95
|
+
const first = compileCache.keys().next().value;
|
|
96
|
+
if (first !== undefined) compileCache.delete(first);
|
|
97
|
+
}
|
|
98
|
+
compileCache.set(key, frozen);
|
|
99
|
+
return frozen;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Clear the compiled-glob cache. Intended for long-running hosts that
|
|
104
|
+
* need to reload config (e.g. watch mode after `.codegraphrc.json` edits)
|
|
105
|
+
* and for test isolation.
|
|
106
|
+
*/
|
|
107
|
+
export function clearGlobCache(): void {
|
|
108
|
+
compileCache.clear();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `true` when at least one compiled pattern matches the given path.
|
|
113
|
+
*
|
|
114
|
+
* The path must already be normalized to forward slashes.
|
|
115
|
+
*/
|
|
116
|
+
export function matchesAny(regexes: readonly RegExp[], path: string): boolean {
|
|
117
|
+
for (const re of regexes) {
|
|
118
|
+
if (re.test(path)) return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1033,7 +1033,23 @@ export interface PipelineContext {
|
|
|
1033
1033
|
lineCountMap: Map<string, number>;
|
|
1034
1034
|
|
|
1035
1035
|
// Phase timing
|
|
1036
|
-
timing:
|
|
1036
|
+
timing: {
|
|
1037
|
+
setupMs?: number;
|
|
1038
|
+
collectMs?: number;
|
|
1039
|
+
detectMs?: number;
|
|
1040
|
+
parseMs?: number;
|
|
1041
|
+
insertMs?: number;
|
|
1042
|
+
resolveMs?: number;
|
|
1043
|
+
edgesMs?: number;
|
|
1044
|
+
structureMs?: number;
|
|
1045
|
+
rolesMs?: number;
|
|
1046
|
+
astMs?: number;
|
|
1047
|
+
complexityMs?: number;
|
|
1048
|
+
cfgMs?: number;
|
|
1049
|
+
dataflowMs?: number;
|
|
1050
|
+
finalizeMs?: number;
|
|
1051
|
+
[key: string]: number | undefined;
|
|
1052
|
+
};
|
|
1037
1053
|
buildStart: number;
|
|
1038
1054
|
}
|
|
1039
1055
|
|
|
@@ -1053,6 +1069,8 @@ export interface BuildGraphOpts {
|
|
|
1053
1069
|
export interface BuildResult {
|
|
1054
1070
|
phases: {
|
|
1055
1071
|
setupMs: number;
|
|
1072
|
+
collectMs: number;
|
|
1073
|
+
detectMs: number;
|
|
1056
1074
|
parseMs: number;
|
|
1057
1075
|
insertMs: number;
|
|
1058
1076
|
resolveMs: number;
|
|
@@ -1104,6 +1122,13 @@ export interface CodegraphConfig {
|
|
|
1104
1122
|
model: string | null;
|
|
1105
1123
|
baseUrl: string | null;
|
|
1106
1124
|
apiKey: string | null;
|
|
1125
|
+
/**
|
|
1126
|
+
* Command that prints the API key to stdout. Must be a single string —
|
|
1127
|
+
* it is split on whitespace and executed via `execFileSync` with no shell,
|
|
1128
|
+
* so shell features like `$(...)`, pipes, globs, or variable expansion are
|
|
1129
|
+
* not supported. Example: `"op read op://vault/openai/api-key"`. Non-string
|
|
1130
|
+
* values are rejected with a `ConfigError` at load time.
|
|
1131
|
+
*/
|
|
1107
1132
|
apiKeyCommand: string | null;
|
|
1108
1133
|
};
|
|
1109
1134
|
|