@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.
Files changed (146) hide show
  1. package/README.md +10 -10
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +3 -2
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/csharp.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/csharp.js +8 -1
  7. package/dist/ast-analysis/rules/csharp.js.map +1 -1
  8. package/dist/ast-analysis/rules/go.d.ts.map +1 -1
  9. package/dist/ast-analysis/rules/go.js +4 -1
  10. package/dist/ast-analysis/rules/go.js.map +1 -1
  11. package/dist/ast-analysis/rules/index.d.ts +6 -0
  12. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  13. package/dist/ast-analysis/rules/index.js +151 -4
  14. package/dist/ast-analysis/rules/index.js.map +1 -1
  15. package/dist/ast-analysis/rules/java.d.ts.map +1 -1
  16. package/dist/ast-analysis/rules/java.js +5 -1
  17. package/dist/ast-analysis/rules/java.js.map +1 -1
  18. package/dist/ast-analysis/rules/php.d.ts.map +1 -1
  19. package/dist/ast-analysis/rules/php.js +6 -1
  20. package/dist/ast-analysis/rules/php.js.map +1 -1
  21. package/dist/ast-analysis/rules/python.d.ts.map +1 -1
  22. package/dist/ast-analysis/rules/python.js +5 -1
  23. package/dist/ast-analysis/rules/python.js.map +1 -1
  24. package/dist/ast-analysis/rules/ruby.d.ts.map +1 -1
  25. package/dist/ast-analysis/rules/ruby.js +4 -1
  26. package/dist/ast-analysis/rules/ruby.js.map +1 -1
  27. package/dist/ast-analysis/rules/rust.d.ts.map +1 -1
  28. package/dist/ast-analysis/rules/rust.js +5 -1
  29. package/dist/ast-analysis/rules/rust.js.map +1 -1
  30. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts +2 -1
  31. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  32. package/dist/ast-analysis/visitors/ast-store-visitor.js +129 -37
  33. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  34. package/dist/cli/commands/watch.d.ts.map +1 -1
  35. package/dist/cli/commands/watch.js +2 -0
  36. package/dist/cli/commands/watch.js.map +1 -1
  37. package/dist/cli.js +24 -1
  38. package/dist/cli.js.map +1 -1
  39. package/dist/domain/graph/builder/context.d.ts +2 -0
  40. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/context.js.map +1 -1
  42. package/dist/domain/graph/builder/helpers.d.ts +13 -2
  43. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/helpers.js +30 -4
  45. package/dist/domain/graph/builder/helpers.js.map +1 -1
  46. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/pipeline.js +141 -3
  48. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  49. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/collect-files.js +58 -26
  51. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/detect-changes.js +54 -45
  54. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/stages/finalize.js +17 -0
  57. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  58. package/dist/domain/graph/journal.d.ts +15 -0
  59. package/dist/domain/graph/journal.d.ts.map +1 -1
  60. package/dist/domain/graph/journal.js +283 -28
  61. package/dist/domain/graph/journal.js.map +1 -1
  62. package/dist/domain/graph/watcher.d.ts +17 -0
  63. package/dist/domain/graph/watcher.d.ts.map +1 -1
  64. package/dist/domain/graph/watcher.js +23 -7
  65. package/dist/domain/graph/watcher.js.map +1 -1
  66. package/dist/domain/parser.d.ts +53 -4
  67. package/dist/domain/parser.d.ts.map +1 -1
  68. package/dist/domain/parser.js +278 -80
  69. package/dist/domain/parser.js.map +1 -1
  70. package/dist/domain/search/generator.d.ts.map +1 -1
  71. package/dist/domain/search/generator.js +28 -2
  72. package/dist/domain/search/generator.js.map +1 -1
  73. package/dist/domain/search/models.js +1 -1
  74. package/dist/domain/wasm-worker-entry.d.ts +24 -0
  75. package/dist/domain/wasm-worker-entry.d.ts.map +1 -0
  76. package/dist/domain/wasm-worker-entry.js +644 -0
  77. package/dist/domain/wasm-worker-entry.js.map +1 -0
  78. package/dist/domain/wasm-worker-pool.d.ts +59 -0
  79. package/dist/domain/wasm-worker-pool.d.ts.map +1 -0
  80. package/dist/domain/wasm-worker-pool.js +312 -0
  81. package/dist/domain/wasm-worker-pool.js.map +1 -0
  82. package/dist/domain/wasm-worker-protocol.d.ts +65 -0
  83. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -0
  84. package/dist/domain/wasm-worker-protocol.js +13 -0
  85. package/dist/domain/wasm-worker-protocol.js.map +1 -0
  86. package/dist/extractors/javascript.js +146 -2
  87. package/dist/extractors/javascript.js.map +1 -1
  88. package/dist/features/ast.d.ts.map +1 -1
  89. package/dist/features/ast.js +11 -9
  90. package/dist/features/ast.js.map +1 -1
  91. package/dist/features/boundaries.d.ts +2 -2
  92. package/dist/features/boundaries.d.ts.map +1 -1
  93. package/dist/features/boundaries.js +2 -31
  94. package/dist/features/boundaries.js.map +1 -1
  95. package/dist/features/snapshot.d.ts.map +1 -1
  96. package/dist/features/snapshot.js +99 -13
  97. package/dist/features/snapshot.js.map +1 -1
  98. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  99. package/dist/graph/algorithms/louvain.js +2 -4
  100. package/dist/graph/algorithms/louvain.js.map +1 -1
  101. package/dist/infrastructure/config.d.ts.map +1 -1
  102. package/dist/infrastructure/config.js +12 -2
  103. package/dist/infrastructure/config.js.map +1 -1
  104. package/dist/shared/globs.d.ts +40 -0
  105. package/dist/shared/globs.d.ts.map +1 -0
  106. package/dist/shared/globs.js +126 -0
  107. package/dist/shared/globs.js.map +1 -0
  108. package/dist/types.d.ts +26 -1
  109. package/dist/types.d.ts.map +1 -1
  110. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  111. package/grammars/tree-sitter-erlang.wasm +0 -0
  112. package/package.json +7 -7
  113. package/src/ast-analysis/engine.ts +11 -1
  114. package/src/ast-analysis/rules/csharp.ts +8 -1
  115. package/src/ast-analysis/rules/go.ts +4 -1
  116. package/src/ast-analysis/rules/index.ts +181 -4
  117. package/src/ast-analysis/rules/java.ts +5 -1
  118. package/src/ast-analysis/rules/php.ts +6 -1
  119. package/src/ast-analysis/rules/python.ts +5 -1
  120. package/src/ast-analysis/rules/ruby.ts +4 -1
  121. package/src/ast-analysis/rules/rust.ts +5 -1
  122. package/src/ast-analysis/visitors/ast-store-visitor.ts +129 -34
  123. package/src/cli/commands/watch.ts +2 -0
  124. package/src/cli.ts +31 -8
  125. package/src/domain/graph/builder/context.ts +2 -0
  126. package/src/domain/graph/builder/helpers.ts +53 -3
  127. package/src/domain/graph/builder/pipeline.ts +162 -3
  128. package/src/domain/graph/builder/stages/collect-files.ts +56 -26
  129. package/src/domain/graph/builder/stages/detect-changes.ts +57 -49
  130. package/src/domain/graph/builder/stages/finalize.ts +16 -0
  131. package/src/domain/graph/journal.ts +284 -27
  132. package/src/domain/graph/watcher.ts +29 -9
  133. package/src/domain/parser.ts +288 -73
  134. package/src/domain/search/generator.ts +34 -2
  135. package/src/domain/search/models.ts +1 -1
  136. package/src/domain/wasm-worker-entry.ts +798 -0
  137. package/src/domain/wasm-worker-pool.ts +330 -0
  138. package/src/domain/wasm-worker-protocol.ts +81 -0
  139. package/src/extractors/javascript.ts +149 -2
  140. package/src/features/ast.ts +22 -9
  141. package/src/features/boundaries.ts +2 -27
  142. package/src/features/snapshot.ts +93 -14
  143. package/src/graph/algorithms/louvain.ts +2 -4
  144. package/src/infrastructure/config.ts +12 -2
  145. package/src/shared/globs.ts +121 -0
  146. package/src/types.ts +26 -1
@@ -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
- if (fs.existsSync(dest)) {
41
- if (!options.force) {
42
- throw new ConfigError(`Snapshot "${name}" already exists. Use --force to overwrite.`);
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 '${dest.replace(/'/g, "''")}'`);
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 sidecar files for a clean restore
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
- if (fs.existsSync(sidecar)) {
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
- if (!fs.existsSync(target)) {
126
- throw new DbError(`Snapshot "${name}" not found at ${target}`, { file: target });
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 { warn } from '../../infrastructure/logger.js';
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
- warn(
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 (typeof cmd !== 'string' || cmd.trim() === '') return config;
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: Record<string, number>;
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