@graphpilot-oss/graphpilot 0.0.1 → 1.0.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +72 -126
  2. package/README.md +290 -102
  3. package/dist/cli.js +41 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/edges.js +22 -11
  6. package/dist/edges.js.map +1 -1
  7. package/dist/indexer.js +3 -3
  8. package/dist/indexer.js.map +1 -1
  9. package/dist/init.d.ts +28 -0
  10. package/dist/init.js +112 -0
  11. package/dist/init.js.map +1 -0
  12. package/dist/interactions.d.ts +5 -4
  13. package/dist/interactions.js +0 -0
  14. package/dist/interactions.js.map +1 -1
  15. package/dist/mcp.js +119 -90
  16. package/dist/mcp.js.map +1 -1
  17. package/dist/repo-resolve.d.ts +47 -0
  18. package/dist/repo-resolve.js +195 -0
  19. package/dist/repo-resolve.js.map +1 -0
  20. package/dist/storage.js +10 -1
  21. package/dist/storage.js.map +1 -1
  22. package/dist/symbols.js +26 -2
  23. package/dist/symbols.js.map +1 -1
  24. package/dist/validation.js +30 -4
  25. package/dist/validation.js.map +1 -1
  26. package/dist/validators.d.ts +1 -5
  27. package/dist/validators.js +0 -11
  28. package/dist/validators.js.map +1 -1
  29. package/dist/watcher.d.ts +10 -0
  30. package/dist/watcher.js +70 -7
  31. package/dist/watcher.js.map +1 -1
  32. package/examples/README.md +105 -0
  33. package/examples/claude-code/README.md +125 -0
  34. package/examples/claude-code/claude-routing.md +102 -0
  35. package/examples/claude-code/claude_config.json +8 -0
  36. package/examples/cline/.clinerules +39 -0
  37. package/examples/cline/README.md +104 -0
  38. package/examples/cline/cline_mcp_settings.json +10 -0
  39. package/examples/continue/.continuerules +39 -0
  40. package/examples/continue/README.md +98 -0
  41. package/examples/continue/config.json +13 -0
  42. package/examples/cursor/.cursorrules +39 -0
  43. package/examples/cursor/README.md +98 -0
  44. package/examples/cursor/mcp.json +11 -0
  45. package/examples/windsurf/.windsurfrules +39 -0
  46. package/examples/windsurf/README.md +85 -0
  47. package/examples/windsurf/mcp_config.json +8 -0
  48. package/package.json +14 -4
  49. package/.editorconfig +0 -15
  50. package/.github/CODEOWNERS +0 -22
  51. package/.github/FUNDING.yml +0 -1
  52. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  53. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  54. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
  55. package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
  56. package/.github/dependabot.yml +0 -15
  57. package/.github/workflows/ci.yml +0 -62
  58. package/.github/workflows/release.yml +0 -50
  59. package/.prettierignore +0 -19
  60. package/.prettierrc.json +0 -20
  61. package/CODE_OF_CONDUCT.md +0 -83
  62. package/CONTRIBUTING.md +0 -111
  63. package/bench/README.md +0 -544
  64. package/bench/results/agent-tier-2026-05-22.md +0 -28
  65. package/bench/results/agent-tier-summary.md +0 -44
  66. package/bench/results/baseline-tier-2026-05-22.md +0 -23
  67. package/bench/results/baseline.json +0 -810
  68. package/bench/results/baseline.md +0 -28
  69. package/bench/run-agent-tier-automated.ts +0 -234
  70. package/bench/run-agent-tier.md +0 -125
  71. package/bench/run-baseline-tier.ts +0 -200
  72. package/bench/run.ts +0 -210
  73. package/bench/runner-baseline.ts +0 -177
  74. package/bench/runner-graphpilot.ts +0 -131
  75. package/bench/score-agent-tier.ts +0 -191
  76. package/bench/score.ts +0 -59
  77. package/bench/tasks.ts +0 -236
  78. package/dist/provenance.d.ts +0 -74
  79. package/dist/provenance.js +0 -95
  80. package/dist/provenance.js.map +0 -1
  81. package/docs/architecture.md +0 -311
  82. package/docs/limitations.md +0 -156
  83. package/docs/mcp-setup.md +0 -231
  84. package/docs/quickstart.md +0 -202
  85. package/eslint.config.js +0 -148
  86. package/lefthook.yml +0 -81
  87. package/pnpm-workspace.yaml +0 -6
  88. package/scripts/smoke-stdio.mjs +0 -97
  89. package/src/cli.ts +0 -171
  90. package/src/edges.ts +0 -202
  91. package/src/git.ts +0 -255
  92. package/src/graph-schema.ts +0 -229
  93. package/src/impact.ts +0 -218
  94. package/src/indexer.ts +0 -152
  95. package/src/interactions.ts +0 -0
  96. package/src/mcp.ts +0 -652
  97. package/src/parser.ts +0 -138
  98. package/src/provenance.ts +0 -115
  99. package/src/query.ts +0 -148
  100. package/src/redact.ts +0 -122
  101. package/src/storage.ts +0 -115
  102. package/src/symbols.ts +0 -173
  103. package/src/validation.ts +0 -69
  104. package/src/validators.ts +0 -253
  105. package/src/watcher.ts +0 -383
  106. package/tests/edges.test.ts +0 -175
  107. package/tests/fixtures/sample.ts +0 -32
  108. package/tests/git.test.ts +0 -303
  109. package/tests/graph-schema.test.ts +0 -321
  110. package/tests/impact.test.ts +0 -454
  111. package/tests/interactions.test.ts +0 -180
  112. package/tests/lint-policy.test.ts +0 -106
  113. package/tests/mcp-stdio.test.ts +0 -171
  114. package/tests/mcp.test.ts +0 -335
  115. package/tests/parser.test.ts +0 -31
  116. package/tests/provenance.test.ts +0 -132
  117. package/tests/query.test.ts +0 -160
  118. package/tests/redact.test.ts +0 -167
  119. package/tests/security.test.ts +0 -144
  120. package/tests/symbols.test.ts +0 -78
  121. package/tests/validators.test.ts +0 -193
  122. package/tests/watcher.test.ts +0 -250
  123. package/tsconfig.json +0 -18
package/src/watcher.ts DELETED
@@ -1,383 +0,0 @@
1
- /**
2
- * Watch mode (A2) — keep the on-disk graph fresh as the user edits.
3
- *
4
- * Algorithm per file event:
5
- * 1. Remove all symbols + raw calls that came from this file
6
- * 2. Re-parse the file, extract its symbols + raw calls
7
- * 3. Re-resolve all raw calls against the full symbol table
8
- * (cheap: under 50ms for a 2k-symbol repo)
9
- * 4. Atomic save via storage.saveGraph (.tmp + rename)
10
- *
11
- * No schema change: rawCalls are reconstructed from existing edges on
12
- * startup (every CallEdge carries fromId/toName/file/line/column, which
13
- * is exactly the RawCall shape).
14
- *
15
- * Diagnostic lines go to stderr — never stdout — so stdin/stdout stays
16
- * clean for any agent that's also reading the graph over MCP.
17
- */
18
-
19
- import chokidar, { type FSWatcher } from 'chokidar';
20
- import { realpathSync } from 'node:fs';
21
- import { resolve, relative } from 'node:path';
22
- import { parseFile } from './parser.js';
23
- import { extractSymbols, type SymbolRecord } from './symbols.js';
24
- import { extractRawCalls, resolveCallEdges, type RawCall, type CallEdge } from './edges.js';
25
- import { saveGraph, loadGraph, repoIdFor, type Graph } from './storage.js';
26
- import { readGitInfo } from './git.js';
27
- import { indexDirectory } from './indexer.js';
28
- import { validateRootPath, MAX_FILES_PER_INDEX } from './validation.js';
29
-
30
- /** Per-event delta the watcher reports back to the caller (and to stderr). */
31
- export interface UpdateResult {
32
- file: string;
33
- kind: 'add' | 'change' | 'delete';
34
- symbolsBefore: number;
35
- symbolsAfter: number;
36
- edgesBefore: number;
37
- edgesAfter: number;
38
- durationMs: number;
39
- }
40
-
41
- export interface WatcherOptions {
42
- /** Debounce window for editor "save in 3 syscalls" patterns. Default 100 ms. */
43
- awaitStabilityMs?: number;
44
- /** Logger for human-readable progress lines. Default: process.stderr. */
45
- log?: (line: string) => void;
46
- }
47
-
48
- const DEFAULT_IGNORE = [
49
- /(^|[/\\])\.git([/\\]|$)/,
50
- /(^|[/\\])node_modules([/\\]|$)/,
51
- /(^|[/\\])dist([/\\]|$)/,
52
- /(^|[/\\])build([/\\]|$)/,
53
- /(^|[/\\])coverage([/\\]|$)/,
54
- /(^|[/\\])\.next([/\\]|$)/,
55
- /(^|[/\\])\.nuxt([/\\]|$)/,
56
- /(^|[/\\])\.cache([/\\]|$)/,
57
- /(^|[/\\])out([/\\]|$)/,
58
- /\.d\.ts$/,
59
- ];
60
-
61
- const WATCHED_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
62
-
63
- function isWatchableFile(absPath: string): boolean {
64
- const dot = absPath.lastIndexOf('.');
65
- if (dot === -1) return false;
66
- return WATCHED_EXT.has(absPath.slice(dot).toLowerCase());
67
- }
68
-
69
- function defaultLogger(line: string): void {
70
- process.stderr.write(`[graphpilot:watch] ${line}\n`);
71
- }
72
-
73
- /**
74
- * Stateful watcher. Owns the in-memory Graph + raw calls and reconciles
75
- * the on-disk graph.json on every event.
76
- *
77
- * Methods come in two flavours:
78
- * - lifecycle: start / stop, drive chokidar
79
- * - applyUpdate / applyDeletion: synchronous-style helpers that tests
80
- * can call directly, bypassing chokidar (which is racy in tests)
81
- */
82
- export class GraphWatcher {
83
- readonly absRoot: string;
84
- private graph: Graph;
85
- private rawCalls: RawCall[];
86
- private watcher: FSWatcher | null = null;
87
- private readonly log: (line: string) => void;
88
- private readonly awaitStabilityMs: number;
89
- /**
90
- * Serializes the update queue so chokidar bursts (multiple `change`
91
- * events in 200ms) don't race each other into a torn graph.
92
- */
93
- private chain: Promise<void> = Promise.resolve();
94
-
95
- constructor(rawRoot: string, opts: WatcherOptions = {}) {
96
- this.absRoot = resolve(rawRoot);
97
- this.log = opts.log ?? defaultLogger;
98
- this.awaitStabilityMs = opts.awaitStabilityMs ?? 100;
99
-
100
- const refusal = validateRootPath(this.absRoot);
101
- if (refusal) throw new Error(refusal);
102
-
103
- // Defence-in-depth realpath check (T2): make sure the user-given path
104
- // resolves to a real directory. We do NOT overwrite this.absRoot with
105
- // the canonical form — that would break the path → repoId hash and
106
- // cause the watcher to read+write a different graph.json than `index`.
107
- realpathSync(this.absRoot);
108
-
109
- const loaded = loadGraph(this.absRoot);
110
- if (loaded) {
111
- this.graph = loaded;
112
- this.rawCalls = this.deriveRawCalls(loaded.edges);
113
- } else {
114
- // No existing index — caller is expected to `start()`, which will
115
- // build one. We initialize empty here so applyUpdate is safe to call
116
- // pre-start in tests.
117
- this.graph = {
118
- version: 1,
119
- repoId: repoIdFor(this.absRoot),
120
- rootPath: this.absRoot,
121
- indexedAt: new Date().toISOString(),
122
- filesIndexed: 0,
123
- symbolCount: 0,
124
- edgeCount: 0,
125
- symbols: [],
126
- edges: [],
127
- };
128
- this.rawCalls = [];
129
- }
130
- }
131
-
132
- /** Build a one-shot full index if no graph exists, then start the watcher. */
133
- async start(): Promise<void> {
134
- if (this.graph.symbols.length === 0) {
135
- this.log(`No existing index. Running full index of ${this.absRoot} ...`);
136
- await this.fullReindex();
137
- }
138
-
139
- this.log(
140
- `Watching ${this.absRoot} ` +
141
- `(${this.graph.symbols.length} symbols, ${this.graph.edges.length} calls, ` +
142
- `${this.graph.filesIndexed} files). Edit a file to see updates.`,
143
- );
144
-
145
- this.watcher = chokidar.watch(this.absRoot, {
146
- ignored: DEFAULT_IGNORE,
147
- ignoreInitial: true, // we already have a graph
148
- persistent: true,
149
- followSymlinks: false,
150
- awaitWriteFinish: {
151
- stabilityThreshold: this.awaitStabilityMs,
152
- pollInterval: 50,
153
- },
154
- });
155
-
156
- this.watcher.on('add', (abs: string) => this.enqueue(() => this.handleEvent(abs, 'add')));
157
- this.watcher.on('change', (abs: string) => this.enqueue(() => this.handleEvent(abs, 'change')));
158
- this.watcher.on('unlink', (abs: string) => this.enqueue(() => this.handleDeletion(abs)));
159
- this.watcher.on('error', (err: unknown) => this.log(`watcher error: ${String(err)}`));
160
- }
161
-
162
- async stop(): Promise<void> {
163
- if (this.watcher) {
164
- await this.watcher.close();
165
- this.watcher = null;
166
- }
167
- await this.chain;
168
- this.log('Stopped.');
169
- }
170
-
171
- /**
172
- * Process a single file update. Public so tests can drive it directly
173
- * without spawning chokidar.
174
- */
175
- async applyUpdate(
176
- absFilePath: string,
177
- kind: 'add' | 'change' = 'change',
178
- ): Promise<UpdateResult | null> {
179
- if (!isWatchableFile(absFilePath)) return null;
180
- if (!absFilePath.startsWith(this.absRoot)) return null;
181
-
182
- const start = Date.now();
183
- const rel = relative(this.absRoot, absFilePath);
184
- const symbolsBefore = this.graph.symbols.length;
185
- const edgesBefore = this.graph.edges.length;
186
-
187
- // 1. Remove existing symbols + raw calls from this file
188
- const keptSymbols = this.graph.symbols.filter((s) => s.file !== rel);
189
- const keptRawCalls = this.rawCalls.filter((c) => c.file !== rel);
190
-
191
- // 2. Parse the new content + extract
192
- const parsed = parseFile(absFilePath);
193
- if (!parsed) {
194
- // Could not parse (too large, unknown ext, gone, etc.) — treat as a
195
- // deletion of that file's contribution. Keeps the index honest.
196
- this.commitState(keptSymbols, keptRawCalls);
197
- const result = this.finalize(rel, kind, symbolsBefore, edgesBefore, start);
198
- this.log(
199
- `${rel} unparseable; dropped ${symbolsBefore - keptSymbols.length} symbols (${result.durationMs}ms).`,
200
- );
201
- return result;
202
- }
203
-
204
- const fileSymbols = extractSymbols(parsed);
205
- const fileCalls = extractRawCalls(parsed, fileSymbols);
206
-
207
- // 3. Normalize paths + rewrite ids to relative form (same as indexer.ts)
208
- const idRewrites = new Map<string, string>();
209
- for (const s of fileSymbols) {
210
- const oldId = s.id;
211
- s.file = rel;
212
- s.id = oldId.replace(absFilePath, rel);
213
- idRewrites.set(oldId, s.id);
214
- }
215
- for (const c of fileCalls) {
216
- c.file = rel;
217
- c.fromId = idRewrites.get(c.fromId) ?? c.fromId;
218
- }
219
-
220
- const newSymbols = [...keptSymbols, ...fileSymbols];
221
- const newRawCalls = [...keptRawCalls, ...fileCalls];
222
-
223
- // 4. Re-resolve every edge against the new symbol table. Cheap.
224
- this.commitState(newSymbols, newRawCalls);
225
-
226
- const result = this.finalize(rel, kind, symbolsBefore, edgesBefore, start);
227
- this.log(
228
- `${rel}: ${this.deltaStr(symbolsBefore, result.symbolsAfter)} symbols, ` +
229
- `${this.deltaStr(edgesBefore, result.edgesAfter)} calls (${result.durationMs}ms).`,
230
- );
231
- return result;
232
- }
233
-
234
- /** Drop everything that came from a deleted file. */
235
- async applyDeletion(absFilePath: string): Promise<UpdateResult | null> {
236
- if (!absFilePath.startsWith(this.absRoot)) return null;
237
-
238
- const start = Date.now();
239
- const rel = relative(this.absRoot, absFilePath);
240
- const symbolsBefore = this.graph.symbols.length;
241
- const edgesBefore = this.graph.edges.length;
242
-
243
- const keptSymbols = this.graph.symbols.filter((s) => s.file !== rel);
244
- const keptRawCalls = this.rawCalls.filter((c) => c.file !== rel);
245
-
246
- // No change? File wasn't in the index, ignore.
247
- if (
248
- keptSymbols.length === this.graph.symbols.length &&
249
- keptRawCalls.length === this.rawCalls.length
250
- ) {
251
- return null;
252
- }
253
-
254
- this.commitState(keptSymbols, keptRawCalls);
255
- const result = this.finalize(rel, 'delete', symbolsBefore, edgesBefore, start);
256
- this.log(
257
- `${rel} deleted: ${this.deltaStr(symbolsBefore, result.symbolsAfter)} symbols, ` +
258
- `${this.deltaStr(edgesBefore, result.edgesAfter)} calls (${result.durationMs}ms).`,
259
- );
260
- return result;
261
- }
262
-
263
- /** Force a full re-index from scratch. Used on startup if no graph exists. */
264
- async fullReindex(): Promise<void> {
265
- const result = await indexDirectory(this.absRoot);
266
- if (result.symbols.length > MAX_FILES_PER_INDEX) {
267
- throw new Error('Refusing to watch: index would exceed MAX_FILES_PER_INDEX.');
268
- }
269
- this.graph = {
270
- version: 1,
271
- repoId: repoIdFor(this.absRoot),
272
- rootPath: this.absRoot,
273
- indexedAt: new Date().toISOString(),
274
- filesIndexed: result.filesIndexed,
275
- symbolCount: result.symbols.length,
276
- edgeCount: result.edges.length,
277
- symbols: result.symbols,
278
- edges: result.edges,
279
- indexedSha: result.git.sha,
280
- indexedBranch: result.git.branch,
281
- };
282
- this.rawCalls = this.deriveRawCalls(result.edges);
283
- saveGraph(this.graph);
284
- }
285
-
286
- /** Read-only accessor, mainly for tests + diagnostics. */
287
- get currentGraph(): Graph {
288
- return this.graph;
289
- }
290
-
291
- // -------------------------------------------------------------------------
292
- // internals
293
- // -------------------------------------------------------------------------
294
-
295
- /**
296
- * Chain updates so chokidar's burst events don't run in parallel. Errors
297
- * are logged but never abort the chain — the next event still runs.
298
- */
299
- private enqueue(work: () => Promise<void>): void {
300
- this.chain = this.chain.then(
301
- () =>
302
- work().catch((err) => {
303
- this.log(`error: ${err instanceof Error ? err.message : String(err)}`);
304
- }),
305
- () => undefined, // can't reach because catch above never rejects
306
- );
307
- }
308
-
309
- private async handleEvent(abs: string, kind: 'add' | 'change'): Promise<void> {
310
- if (!isWatchableFile(abs)) return;
311
- await this.applyUpdate(abs, kind);
312
- }
313
-
314
- private async handleDeletion(abs: string): Promise<void> {
315
- await this.applyDeletion(abs);
316
- }
317
-
318
- /**
319
- * Apply a new (symbols, rawCalls) state: re-resolve edges, update the
320
- * graph object, save atomically.
321
- */
322
- private commitState(symbols: SymbolRecord[], rawCalls: RawCall[]): void {
323
- const edges = resolveCallEdges(rawCalls, symbols);
324
-
325
- // Recompute filesIndexed from surviving symbols' files.
326
- const files = new Set<string>();
327
- for (const s of symbols) files.add(s.file);
328
-
329
- // Re-read git info on every commit — branch / sha can change between
330
- // edits (e.g. user did `git checkout` mid-session) and we want the
331
- // graph stamped with the *current* state. Cheap: a few fs reads.
332
- const git = readGitInfo(this.absRoot);
333
-
334
- this.graph = {
335
- ...this.graph,
336
- indexedAt: new Date().toISOString(),
337
- filesIndexed: files.size,
338
- symbolCount: symbols.length,
339
- edgeCount: edges.length,
340
- symbols,
341
- edges,
342
- indexedSha: git.sha,
343
- indexedBranch: git.branch,
344
- };
345
- this.rawCalls = rawCalls;
346
- saveGraph(this.graph);
347
- }
348
-
349
- private finalize(
350
- file: string,
351
- kind: UpdateResult['kind'],
352
- symbolsBefore: number,
353
- edgesBefore: number,
354
- start: number,
355
- ): UpdateResult {
356
- return {
357
- file,
358
- kind,
359
- symbolsBefore,
360
- symbolsAfter: this.graph.symbols.length,
361
- edgesBefore,
362
- edgesAfter: this.graph.edges.length,
363
- durationMs: Date.now() - start,
364
- };
365
- }
366
-
367
- private deriveRawCalls(edges: CallEdge[]): RawCall[] {
368
- return edges.map((e) => ({
369
- fromId: e.fromId,
370
- toName: e.toName,
371
- file: e.file,
372
- line: e.line,
373
- column: e.column,
374
- }));
375
- }
376
-
377
- private deltaStr(before: number, after: number): string {
378
- const delta = after - before;
379
- if (delta === 0) return `${after} (=)`;
380
- const sign = delta > 0 ? '+' : '';
381
- return `${after} (${sign}${delta})`;
382
- }
383
- }
@@ -1,175 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { writeFileSync, mkdtempSync, rmSync, existsSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join, dirname } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { parseFile } from '../src/parser.js';
7
- import { extractSymbols } from '../src/symbols.js';
8
- import { extractRawCalls, resolveCallEdges } from '../src/edges.js';
9
- import { indexDirectory } from '../src/indexer.js';
10
-
11
- const here = dirname(fileURLToPath(import.meta.url));
12
- const fixture = (name: string) => join(here, 'fixtures', name);
13
-
14
- // ---------------------------------------------------------------------------
15
- // extractRawCalls (per-file)
16
- // ---------------------------------------------------------------------------
17
-
18
- describe('extractRawCalls', () => {
19
- it('finds in-file call: authenticate → parseToken', () => {
20
- const parsed = parseFile(fixture('sample.ts'))!;
21
- const syms = extractSymbols(parsed);
22
- const calls = extractRawCalls(parsed, syms);
23
-
24
- const authToParseToken = calls.find(
25
- (c) => c.toName === 'parseToken' && c.fromId.includes('AuthService.authenticate'),
26
- );
27
- expect(authToParseToken).toBeDefined();
28
- });
29
-
30
- it('finds method call: parseToken → trim (unresolvable but captured)', () => {
31
- const parsed = parseFile(fixture('sample.ts'))!;
32
- const syms = extractSymbols(parsed);
33
- const calls = extractRawCalls(parsed, syms);
34
-
35
- const trimCall = calls.find((c) => c.toName === 'trim');
36
- expect(trimCall).toBeDefined();
37
- expect(trimCall!.fromId).toContain('parseToken');
38
- });
39
-
40
- it('does not falsely emit calls for property accesses', () => {
41
- // validateJwt body is `return jwt.length > 0;` — `.length` is a property,
42
- // not a call. We should NOT emit it as a call.
43
- const parsed = parseFile(fixture('sample.ts'))!;
44
- const syms = extractSymbols(parsed);
45
- const calls = extractRawCalls(parsed, syms);
46
-
47
- expect(calls.find((c) => c.toName === 'length')).toBeUndefined();
48
- });
49
-
50
- it('returns line+column for every call site', () => {
51
- const parsed = parseFile(fixture('sample.ts'))!;
52
- const syms = extractSymbols(parsed);
53
- const calls = extractRawCalls(parsed, syms);
54
-
55
- for (const c of calls) {
56
- expect(c.line).toBeGreaterThan(0);
57
- expect(c.column).toBeGreaterThan(0);
58
- expect(c.fromId).toMatch(/.+#.+@\d+/);
59
- }
60
- });
61
- });
62
-
63
- // ---------------------------------------------------------------------------
64
- // resolveCallEdges (post-pass)
65
- // ---------------------------------------------------------------------------
66
-
67
- describe('resolveCallEdges', () => {
68
- it('resolves callee in same file', () => {
69
- const parsed = parseFile(fixture('sample.ts'))!;
70
- const syms = extractSymbols(parsed);
71
- const raw = extractRawCalls(parsed, syms);
72
- const edges = resolveCallEdges(raw, syms);
73
-
74
- const e = edges.find((x) => x.toName === 'parseToken');
75
- expect(e?.toId).not.toBeNull();
76
- expect(e?.toId).toContain('parseToken');
77
- });
78
-
79
- it('marks unknown calls as unresolved (toId: null)', () => {
80
- const parsed = parseFile(fixture('sample.ts'))!;
81
- const syms = extractSymbols(parsed);
82
- const raw = extractRawCalls(parsed, syms);
83
- const edges = resolveCallEdges(raw, syms);
84
-
85
- const e = edges.find((x) => x.toName === 'trim');
86
- expect(e?.toId).toBeNull();
87
- expect(e?.toName).toBe('trim');
88
- });
89
-
90
- it('prefers same-file candidate when multiple symbols share a name', () => {
91
- const fileASym = {
92
- id: 'a.ts#foo@1',
93
- name: 'foo',
94
- kind: 'function' as const,
95
- file: 'a.ts',
96
- line: 1,
97
- column: 1,
98
- endLine: 3,
99
- signature: 'function foo()',
100
- exported: false,
101
- };
102
- const fileBSym = {
103
- ...fileASym,
104
- id: 'b.ts#foo@1',
105
- file: 'b.ts',
106
- };
107
- const raw = [{ fromId: 'b.ts#bar@5', toName: 'foo', file: 'b.ts', line: 6, column: 10 }];
108
- const edges = resolveCallEdges(raw, [fileASym, fileBSym]);
109
- expect(edges[0].toId).toBe('b.ts#foo@1');
110
- });
111
- });
112
-
113
- // ---------------------------------------------------------------------------
114
- // Integration: cross-file resolution end-to-end
115
- // ---------------------------------------------------------------------------
116
-
117
- describe('indexDirectory: cross-file edges', () => {
118
- let workDir: string;
119
-
120
- beforeEach(() => {
121
- workDir = mkdtempSync(join(tmpdir(), 'graphpilot-edges-'));
122
- });
123
-
124
- afterEach(() => {
125
- if (existsSync(workDir)) rmSync(workDir, { recursive: true, force: true });
126
- });
127
-
128
- it('resolves a call across two files', async () => {
129
- writeFileSync(join(workDir, 'a.ts'), `export function helper(): number { return 42; }\n`);
130
- writeFileSync(
131
- join(workDir, 'b.ts'),
132
- `import { helper } from './a';\nexport function main(): number { return helper(); }\n`,
133
- );
134
-
135
- const result = await indexDirectory(workDir);
136
-
137
- expect(result.symbols.map((s) => s.name).sort()).toEqual(['helper', 'main']);
138
-
139
- const mainToHelper = result.edges.find((e) => e.toName === 'helper' && e.file === 'b.ts');
140
- expect(mainToHelper).toBeDefined();
141
- expect(mainToHelper!.toId).not.toBeNull();
142
- expect(mainToHelper!.toId).toContain('a.ts#helper');
143
- });
144
-
145
- it('emits zero edges when no calls exist', async () => {
146
- writeFileSync(join(workDir, 'pure.ts'), `export const x = 1;\nexport type Y = string;\n`);
147
- const result = await indexDirectory(workDir);
148
- expect(result.edges.length).toBe(0);
149
- });
150
-
151
- it('does not attribute nested-arrow calls to the outer function', async () => {
152
- writeFileSync(
153
- join(workDir, 'nested.ts'),
154
- `export function outer(): void {\n` +
155
- ` const inner = () => { doThing(); };\n` +
156
- ` inner();\n` +
157
- `}\n` +
158
- `function doThing(): void {}\n`,
159
- );
160
-
161
- const result = await indexDirectory(workDir);
162
-
163
- // outer should only call `inner` (not `doThing`, which is called by the arrow).
164
- const outerSym = result.symbols.find((s) => s.name === 'outer')!;
165
- const outerCalls = result.edges.filter((e) => e.fromId === outerSym.id);
166
- const outerCalleeNames = outerCalls.map((e) => e.toName).sort();
167
- expect(outerCalleeNames).toEqual(['inner']);
168
-
169
- // The arrow itself is a SymbolRecord (assigned to `inner`); it should be
170
- // the one calling `doThing`.
171
- const innerSym = result.symbols.find((s) => s.name === 'inner')!;
172
- const innerCalls = result.edges.filter((e) => e.fromId === innerSym.id);
173
- expect(innerCalls.map((e) => e.toName)).toContain('doThing');
174
- });
175
- });
@@ -1,32 +0,0 @@
1
- export function parseToken(token: string): string {
2
- return token.trim();
3
- }
4
-
5
- export const validateJwt = (jwt: string): boolean => {
6
- return jwt.length > 0;
7
- };
8
-
9
- const internalHelper = function (x: number) {
10
- return x * 2;
11
- };
12
-
13
- export class AuthService {
14
- authenticate(user: string): boolean {
15
- return parseToken(user).length > 0;
16
- }
17
-
18
- private async fetchUser(id: string): Promise<string> {
19
- return id;
20
- }
21
- }
22
-
23
- export interface Repository {
24
- save(): void;
25
- }
26
-
27
- export type UserId = string;
28
-
29
- enum Role {
30
- Admin,
31
- Member,
32
- }