@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.
- package/CHANGELOG.md +72 -126
- package/README.md +290 -102
- package/dist/cli.js +41 -1
- package/dist/cli.js.map +1 -1
- package/dist/edges.js +22 -11
- package/dist/edges.js.map +1 -1
- package/dist/indexer.js +3 -3
- package/dist/indexer.js.map +1 -1
- package/dist/init.d.ts +28 -0
- package/dist/init.js +112 -0
- package/dist/init.js.map +1 -0
- package/dist/interactions.d.ts +5 -4
- package/dist/interactions.js +0 -0
- package/dist/interactions.js.map +1 -1
- package/dist/mcp.js +119 -90
- package/dist/mcp.js.map +1 -1
- package/dist/repo-resolve.d.ts +47 -0
- package/dist/repo-resolve.js +195 -0
- package/dist/repo-resolve.js.map +1 -0
- package/dist/storage.js +10 -1
- package/dist/storage.js.map +1 -1
- package/dist/symbols.js +26 -2
- package/dist/symbols.js.map +1 -1
- package/dist/validation.js +30 -4
- package/dist/validation.js.map +1 -1
- package/dist/validators.d.ts +1 -5
- package/dist/validators.js +0 -11
- package/dist/validators.js.map +1 -1
- package/dist/watcher.d.ts +10 -0
- package/dist/watcher.js +70 -7
- package/dist/watcher.js.map +1 -1
- package/examples/README.md +105 -0
- package/examples/claude-code/README.md +125 -0
- package/examples/claude-code/claude-routing.md +102 -0
- package/examples/claude-code/claude_config.json +8 -0
- package/examples/cline/.clinerules +39 -0
- package/examples/cline/README.md +104 -0
- package/examples/cline/cline_mcp_settings.json +10 -0
- package/examples/continue/.continuerules +39 -0
- package/examples/continue/README.md +98 -0
- package/examples/continue/config.json +13 -0
- package/examples/cursor/.cursorrules +39 -0
- package/examples/cursor/README.md +98 -0
- package/examples/cursor/mcp.json +11 -0
- package/examples/windsurf/.windsurfrules +39 -0
- package/examples/windsurf/README.md +85 -0
- package/examples/windsurf/mcp_config.json +8 -0
- package/package.json +14 -4
- package/.editorconfig +0 -15
- package/.github/CODEOWNERS +0 -22
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
- package/.github/ISSUE_TEMPLATE/config.yml +0 -5
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
- package/.github/dependabot.yml +0 -15
- package/.github/workflows/ci.yml +0 -62
- package/.github/workflows/release.yml +0 -50
- package/.prettierignore +0 -19
- package/.prettierrc.json +0 -20
- package/CODE_OF_CONDUCT.md +0 -83
- package/CONTRIBUTING.md +0 -111
- package/bench/README.md +0 -544
- package/bench/results/agent-tier-2026-05-22.md +0 -28
- package/bench/results/agent-tier-summary.md +0 -44
- package/bench/results/baseline-tier-2026-05-22.md +0 -23
- package/bench/results/baseline.json +0 -810
- package/bench/results/baseline.md +0 -28
- package/bench/run-agent-tier-automated.ts +0 -234
- package/bench/run-agent-tier.md +0 -125
- package/bench/run-baseline-tier.ts +0 -200
- package/bench/run.ts +0 -210
- package/bench/runner-baseline.ts +0 -177
- package/bench/runner-graphpilot.ts +0 -131
- package/bench/score-agent-tier.ts +0 -191
- package/bench/score.ts +0 -59
- package/bench/tasks.ts +0 -236
- package/dist/provenance.d.ts +0 -74
- package/dist/provenance.js +0 -95
- package/dist/provenance.js.map +0 -1
- package/docs/architecture.md +0 -311
- package/docs/limitations.md +0 -156
- package/docs/mcp-setup.md +0 -231
- package/docs/quickstart.md +0 -202
- package/eslint.config.js +0 -148
- package/lefthook.yml +0 -81
- package/pnpm-workspace.yaml +0 -6
- package/scripts/smoke-stdio.mjs +0 -97
- package/src/cli.ts +0 -171
- package/src/edges.ts +0 -202
- package/src/git.ts +0 -255
- package/src/graph-schema.ts +0 -229
- package/src/impact.ts +0 -218
- package/src/indexer.ts +0 -152
- package/src/interactions.ts +0 -0
- package/src/mcp.ts +0 -652
- package/src/parser.ts +0 -138
- package/src/provenance.ts +0 -115
- package/src/query.ts +0 -148
- package/src/redact.ts +0 -122
- package/src/storage.ts +0 -115
- package/src/symbols.ts +0 -173
- package/src/validation.ts +0 -69
- package/src/validators.ts +0 -253
- package/src/watcher.ts +0 -383
- package/tests/edges.test.ts +0 -175
- package/tests/fixtures/sample.ts +0 -32
- package/tests/git.test.ts +0 -303
- package/tests/graph-schema.test.ts +0 -321
- package/tests/impact.test.ts +0 -454
- package/tests/interactions.test.ts +0 -180
- package/tests/lint-policy.test.ts +0 -106
- package/tests/mcp-stdio.test.ts +0 -171
- package/tests/mcp.test.ts +0 -335
- package/tests/parser.test.ts +0 -31
- package/tests/provenance.test.ts +0 -132
- package/tests/query.test.ts +0 -160
- package/tests/redact.test.ts +0 -167
- package/tests/security.test.ts +0 -144
- package/tests/symbols.test.ts +0 -78
- package/tests/validators.test.ts +0 -193
- package/tests/watcher.test.ts +0 -250
- package/tsconfig.json +0 -18
package/tests/validators.test.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
validateGpIndex,
|
|
4
|
-
validateGpRecall,
|
|
5
|
-
validateGpCallers,
|
|
6
|
-
validateGpImpact,
|
|
7
|
-
validateGpStats,
|
|
8
|
-
} from '../src/validators.js';
|
|
9
|
-
|
|
10
|
-
describe('validateGpStats', () => {
|
|
11
|
-
it('accepts empty object', () => {
|
|
12
|
-
expect(validateGpStats({})).toEqual({ ok: true, value: { path: undefined } });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('accepts a path', () => {
|
|
16
|
-
expect(validateGpStats({ path: '/x' })).toEqual({
|
|
17
|
-
ok: true,
|
|
18
|
-
value: { path: '/x' },
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('rejects non-object', () => {
|
|
23
|
-
expect(validateGpStats('nope').ok).toBe(false);
|
|
24
|
-
expect(validateGpStats(42).ok).toBe(false);
|
|
25
|
-
expect(validateGpStats([]).ok).toBe(false);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('rejects extra keys', () => {
|
|
29
|
-
const r = validateGpStats({ path: '/x', sneaky: true });
|
|
30
|
-
expect(r.ok).toBe(false);
|
|
31
|
-
if (!r.ok) expect(r.error).toMatch(/sneaky/);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('rejects non-string path', () => {
|
|
35
|
-
const r = validateGpStats({ path: 42 });
|
|
36
|
-
expect(r.ok).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('validateGpIndex', () => {
|
|
41
|
-
it('accepts empty (path defaults to cwd later)', () => {
|
|
42
|
-
expect(validateGpIndex({}).ok).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
it('accepts path', () => {
|
|
45
|
-
const r = validateGpIndex({ path: '/repo' });
|
|
46
|
-
expect(r.ok).toBe(true);
|
|
47
|
-
if (r.ok) expect(r.value.path).toBe('/repo');
|
|
48
|
-
});
|
|
49
|
-
it('rejects extra keys', () => {
|
|
50
|
-
expect(validateGpIndex({ path: '/x', force: true }).ok).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('validateGpRecall', () => {
|
|
55
|
-
it('requires a non-empty query', () => {
|
|
56
|
-
expect(validateGpRecall({}).ok).toBe(false);
|
|
57
|
-
expect(validateGpRecall({ query: '' }).ok).toBe(false);
|
|
58
|
-
expect(validateGpRecall({ query: ' ' }).ok).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('accepts minimal valid input', () => {
|
|
62
|
-
const r = validateGpRecall({ query: 'parseToken' });
|
|
63
|
-
expect(r.ok).toBe(true);
|
|
64
|
-
if (r.ok) {
|
|
65
|
-
expect(r.value.query).toBe('parseToken');
|
|
66
|
-
expect(r.value.limit).toBeUndefined();
|
|
67
|
-
expect(r.value.substring).toBeUndefined();
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('accepts full input', () => {
|
|
72
|
-
const r = validateGpRecall({
|
|
73
|
-
query: 'parse',
|
|
74
|
-
limit: 25,
|
|
75
|
-
substring: true,
|
|
76
|
-
path: '/x',
|
|
77
|
-
});
|
|
78
|
-
expect(r.ok).toBe(true);
|
|
79
|
-
if (r.ok) {
|
|
80
|
-
expect(r.value.limit).toBe(25);
|
|
81
|
-
expect(r.value.substring).toBe(true);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('caps limit range', () => {
|
|
86
|
-
expect(validateGpRecall({ query: 'x', limit: 0 }).ok).toBe(false);
|
|
87
|
-
expect(validateGpRecall({ query: 'x', limit: 51 }).ok).toBe(false);
|
|
88
|
-
expect(validateGpRecall({ query: 'x', limit: 1.5 }).ok).toBe(false);
|
|
89
|
-
expect(validateGpRecall({ query: 'x', limit: -1 }).ok).toBe(false);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('rejects extra keys', () => {
|
|
93
|
-
expect(validateGpRecall({ query: 'x', shellInjection: 'oops' }).ok).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('rejects wrong types', () => {
|
|
97
|
-
expect(validateGpRecall({ query: 42 }).ok).toBe(false);
|
|
98
|
-
expect(validateGpRecall({ query: 'x', substring: 'yes' }).ok).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('caps query length', () => {
|
|
102
|
-
const r = validateGpRecall({ query: 'a'.repeat(500) });
|
|
103
|
-
expect(r.ok).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('validateGpCallers', () => {
|
|
108
|
-
it('requires a non-empty symbol', () => {
|
|
109
|
-
expect(validateGpCallers({}).ok).toBe(false);
|
|
110
|
-
expect(validateGpCallers({ symbol: '' }).ok).toBe(false);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('accepts minimal valid input', () => {
|
|
114
|
-
const r = validateGpCallers({ symbol: 'parseToken' });
|
|
115
|
-
expect(r.ok).toBe(true);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('enforces direction enum', () => {
|
|
119
|
-
expect(validateGpCallers({ symbol: 'x', direction: 'callers' }).ok).toBe(true);
|
|
120
|
-
expect(validateGpCallers({ symbol: 'x', direction: 'callees' }).ok).toBe(true);
|
|
121
|
-
expect(validateGpCallers({ symbol: 'x', direction: 'both' }).ok).toBe(false);
|
|
122
|
-
expect(validateGpCallers({ symbol: 'x', direction: 42 }).ok).toBe(false);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('caps limit at 100', () => {
|
|
126
|
-
expect(validateGpCallers({ symbol: 'x', limit: 100 }).ok).toBe(true);
|
|
127
|
-
expect(validateGpCallers({ symbol: 'x', limit: 101 }).ok).toBe(false);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('rejects extra keys', () => {
|
|
131
|
-
expect(validateGpCallers({ symbol: 'x', sql: 'drop table' }).ok).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('validateGpImpact', () => {
|
|
136
|
-
it('requires a non-empty symbol', () => {
|
|
137
|
-
expect(validateGpImpact({}).ok).toBe(false);
|
|
138
|
-
expect(validateGpImpact({ symbol: '' }).ok).toBe(false);
|
|
139
|
-
expect(validateGpImpact({ symbol: ' ' }).ok).toBe(false);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('accepts a minimal valid input', () => {
|
|
143
|
-
const r = validateGpImpact({ symbol: 'parseToken' });
|
|
144
|
-
expect(r.ok).toBe(true);
|
|
145
|
-
if (r.ok) {
|
|
146
|
-
expect(r.value.symbol).toBe('parseToken');
|
|
147
|
-
expect(r.value.depth).toBeUndefined();
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('accepts depth in range 1..5', () => {
|
|
152
|
-
expect(validateGpImpact({ symbol: 'x', depth: 1 }).ok).toBe(true);
|
|
153
|
-
expect(validateGpImpact({ symbol: 'x', depth: 3 }).ok).toBe(true);
|
|
154
|
-
expect(validateGpImpact({ symbol: 'x', depth: 5 }).ok).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('rejects depth out of range', () => {
|
|
158
|
-
expect(validateGpImpact({ symbol: 'x', depth: 0 }).ok).toBe(false);
|
|
159
|
-
expect(validateGpImpact({ symbol: 'x', depth: 6 }).ok).toBe(false);
|
|
160
|
-
expect(validateGpImpact({ symbol: 'x', depth: -1 }).ok).toBe(false);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('rejects non-integer depth', () => {
|
|
164
|
-
expect(validateGpImpact({ symbol: 'x', depth: 2.5 }).ok).toBe(false);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('accepts path', () => {
|
|
168
|
-
const r = validateGpImpact({ symbol: 'x', path: '/tmp/repo' });
|
|
169
|
-
expect(r.ok).toBe(true);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('rejects extra keys', () => {
|
|
173
|
-
expect(validateGpImpact({ symbol: 'x', surprise: 'hello' }).ok).toBe(false);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('rejects wrong types', () => {
|
|
177
|
-
expect(validateGpImpact({ symbol: 42 }).ok).toBe(false);
|
|
178
|
-
expect(validateGpImpact({ symbol: 'x', depth: '3' }).ok).toBe(false);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('accepts a non-empty `since` (commit/branch/tag)', () => {
|
|
182
|
-
const r = validateGpImpact({ symbol: 'x', since: 'main' });
|
|
183
|
-
expect(r.ok).toBe(true);
|
|
184
|
-
if (r.ok) expect(r.value.since).toBe('main');
|
|
185
|
-
expect(validateGpImpact({ symbol: 'x', since: 'abc1234' }).ok).toBe(true);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('rejects empty / whitespace / wrong-typed `since`', () => {
|
|
189
|
-
expect(validateGpImpact({ symbol: 'x', since: '' }).ok).toBe(false);
|
|
190
|
-
expect(validateGpImpact({ symbol: 'x', since: ' ' }).ok).toBe(false);
|
|
191
|
-
expect(validateGpImpact({ symbol: 'x', since: 42 }).ok).toBe(false);
|
|
192
|
-
});
|
|
193
|
-
});
|
package/tests/watcher.test.ts
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync, unlinkSync, existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir, homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { GraphWatcher } from '../src/watcher.js';
|
|
6
|
-
import { repoDir, loadGraph } from '../src/storage.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Tests drive applyUpdate / applyDeletion directly. We do NOT spin chokidar
|
|
10
|
-
* up in tests — it's timing-dependent and racy on CI. The chokidar wiring is
|
|
11
|
-
* exercised manually with `graphpilot watch <path>` and observed in dev.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
let workDir: string;
|
|
15
|
-
|
|
16
|
-
function silentLog(): (s: string) => void {
|
|
17
|
-
return () => undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
workDir = mkdtempSync(join(tmpdir(), 'graphpilot-watch-'));
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
if (workDir && existsSync(workDir)) rmSync(workDir, { recursive: true, force: true });
|
|
26
|
-
// Clean the index dir this workDir would have produced
|
|
27
|
-
const dir = repoDir(workDir);
|
|
28
|
-
if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('GraphWatcher — initial state', () => {
|
|
32
|
-
it('starts with empty graph when no index exists yet', () => {
|
|
33
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
34
|
-
expect(w.currentGraph.symbols.length).toBe(0);
|
|
35
|
-
expect(w.currentGraph.edges.length).toBe(0);
|
|
36
|
-
expect(w.currentGraph.rootPath).toBe(workDir);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('loads existing graph from disk if present', async () => {
|
|
40
|
-
writeFileSync(join(workDir, 'hello.ts'), 'export function hello() { return 1; }\n');
|
|
41
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
42
|
-
await w.fullReindex();
|
|
43
|
-
expect(w.currentGraph.symbols.length).toBeGreaterThan(0);
|
|
44
|
-
|
|
45
|
-
// New watcher should pick the existing graph from disk.
|
|
46
|
-
const w2 = new GraphWatcher(workDir, { log: silentLog() });
|
|
47
|
-
expect(w2.currentGraph.symbols.length).toBe(w.currentGraph.symbols.length);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('refuses to construct on a dangerous root path', () => {
|
|
51
|
-
expect(() => new GraphWatcher(homedir(), { log: silentLog() })).toThrow(
|
|
52
|
-
/home directory|system path/i,
|
|
53
|
-
);
|
|
54
|
-
expect(() => new GraphWatcher('/', { log: silentLog() })).toThrow(/system path/i);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('GraphWatcher — applyUpdate (single-file change)', () => {
|
|
59
|
-
it('adds symbols when a previously-unseen file appears', async () => {
|
|
60
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
61
|
-
await w.fullReindex(); // empty graph
|
|
62
|
-
|
|
63
|
-
const newFile = join(workDir, 'new.ts');
|
|
64
|
-
writeFileSync(newFile, 'export function appeared() { return 42; }\n');
|
|
65
|
-
const r = await w.applyUpdate(newFile, 'add');
|
|
66
|
-
|
|
67
|
-
expect(r).not.toBeNull();
|
|
68
|
-
expect(r!.symbolsAfter).toBeGreaterThan(r!.symbolsBefore);
|
|
69
|
-
const names = w.currentGraph.symbols.map((s) => s.name);
|
|
70
|
-
expect(names).toContain('appeared');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("replaces a file's symbols when it changes (rename function)", async () => {
|
|
74
|
-
const file = join(workDir, 'a.ts');
|
|
75
|
-
writeFileSync(file, 'export function oldName() { return 1; }\n');
|
|
76
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
77
|
-
await w.fullReindex();
|
|
78
|
-
expect(w.currentGraph.symbols.some((s) => s.name === 'oldName')).toBe(true);
|
|
79
|
-
|
|
80
|
-
// Rewrite the file with a different function name
|
|
81
|
-
writeFileSync(file, 'export function newName() { return 2; }\n');
|
|
82
|
-
await w.applyUpdate(file, 'change');
|
|
83
|
-
|
|
84
|
-
const names = w.currentGraph.symbols.map((s) => s.name);
|
|
85
|
-
expect(names).toContain('newName');
|
|
86
|
-
expect(names).not.toContain('oldName');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('updates edges when a caller appears that resolves a previously-unresolved name', async () => {
|
|
90
|
-
// Start with a self-contained caller that calls a not-yet-existing function
|
|
91
|
-
writeFileSync(join(workDir, 'consumer.ts'), `function consume() { helper(); return 1; }\n`);
|
|
92
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
93
|
-
await w.fullReindex();
|
|
94
|
-
const unresolvedBefore = w.currentGraph.edges.filter(
|
|
95
|
-
(e) => e.toName === 'helper' && e.toId === null,
|
|
96
|
-
);
|
|
97
|
-
expect(unresolvedBefore.length).toBe(1);
|
|
98
|
-
|
|
99
|
-
// Add the helper — the edge should newly resolve
|
|
100
|
-
const helperFile = join(workDir, 'helper.ts');
|
|
101
|
-
writeFileSync(helperFile, 'export function helper() { return 7; }\n');
|
|
102
|
-
await w.applyUpdate(helperFile, 'add');
|
|
103
|
-
|
|
104
|
-
const resolved = w.currentGraph.edges.find((e) => e.toName === 'helper' && e.toId !== null);
|
|
105
|
-
expect(resolved).toBeDefined();
|
|
106
|
-
expect(resolved!.toId).toMatch(/helper/);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('drops edges for a removed caller', async () => {
|
|
110
|
-
writeFileSync(join(workDir, 'target.ts'), 'export function target() { return 1; }\n');
|
|
111
|
-
writeFileSync(
|
|
112
|
-
join(workDir, 'caller.ts'),
|
|
113
|
-
'import { target } from "./target";\nfunction call() { return target(); }\n',
|
|
114
|
-
);
|
|
115
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
116
|
-
await w.fullReindex();
|
|
117
|
-
const edgesBefore = w.currentGraph.edges.length;
|
|
118
|
-
expect(edgesBefore).toBeGreaterThan(0);
|
|
119
|
-
|
|
120
|
-
// Remove the caller's body — should drop the edge
|
|
121
|
-
const callerFile = join(workDir, 'caller.ts');
|
|
122
|
-
writeFileSync(callerFile, 'import { target } from "./target";\n');
|
|
123
|
-
await w.applyUpdate(callerFile, 'change');
|
|
124
|
-
|
|
125
|
-
const callTargetEdges = w.currentGraph.edges.filter((e) => e.toName === 'target');
|
|
126
|
-
expect(callTargetEdges.length).toBe(0);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('ignores non-watchable files', async () => {
|
|
130
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
131
|
-
await w.fullReindex();
|
|
132
|
-
const txtFile = join(workDir, 'note.txt');
|
|
133
|
-
writeFileSync(txtFile, 'just text');
|
|
134
|
-
const r = await w.applyUpdate(txtFile, 'add');
|
|
135
|
-
expect(r).toBeNull();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('ignores files outside the watched root', async () => {
|
|
139
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
140
|
-
await w.fullReindex();
|
|
141
|
-
// A path outside the root
|
|
142
|
-
const outside = '/tmp/some-other-file.ts';
|
|
143
|
-
const r = await w.applyUpdate(outside, 'change');
|
|
144
|
-
expect(r).toBeNull();
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('GraphWatcher — applyDeletion', () => {
|
|
149
|
-
it('removes all symbols + edges that came from the deleted file', async () => {
|
|
150
|
-
const a = join(workDir, 'a.ts');
|
|
151
|
-
const b = join(workDir, 'b.ts');
|
|
152
|
-
writeFileSync(a, 'export function fromA() { return 1; }\n');
|
|
153
|
-
writeFileSync(b, 'import { fromA } from "./a";\nexport function fromB() { return fromA(); }\n');
|
|
154
|
-
|
|
155
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
156
|
-
await w.fullReindex();
|
|
157
|
-
expect(w.currentGraph.symbols.some((s) => s.name === 'fromA')).toBe(true);
|
|
158
|
-
|
|
159
|
-
unlinkSync(a);
|
|
160
|
-
const r = await w.applyDeletion(a);
|
|
161
|
-
expect(r).not.toBeNull();
|
|
162
|
-
expect(w.currentGraph.symbols.some((s) => s.name === 'fromA')).toBe(false);
|
|
163
|
-
// The fromA-call edge in b.ts is now unresolved
|
|
164
|
-
const stillThere = w.currentGraph.edges.find((e) => e.toName === 'fromA');
|
|
165
|
-
expect(stillThere?.toId).toBeNull();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('is a no-op if the file was not in the index', async () => {
|
|
169
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
170
|
-
await w.fullReindex();
|
|
171
|
-
const r = await w.applyDeletion(join(workDir, 'never-existed.ts'));
|
|
172
|
-
expect(r).toBeNull();
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('GraphWatcher — on-disk side effects', () => {
|
|
177
|
-
it('persists each applyUpdate atomically (graph.json never partial)', async () => {
|
|
178
|
-
writeFileSync(join(workDir, 'x.ts'), 'export function x() {}\n');
|
|
179
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
180
|
-
await w.fullReindex();
|
|
181
|
-
|
|
182
|
-
// After a change, on-disk graph.json must be parseable JSON matching
|
|
183
|
-
// what's in memory. (Atomic .tmp + rename in storage.saveGraph.)
|
|
184
|
-
writeFileSync(join(workDir, 'x.ts'), 'export function x() { return 9; }\n');
|
|
185
|
-
await w.applyUpdate(join(workDir, 'x.ts'), 'change');
|
|
186
|
-
|
|
187
|
-
const onDisk = loadGraph(workDir);
|
|
188
|
-
expect(onDisk).not.toBeNull();
|
|
189
|
-
expect(onDisk!.symbolCount).toBe(w.currentGraph.symbols.length);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('recomputes filesIndexed from surviving symbols', async () => {
|
|
193
|
-
writeFileSync(join(workDir, 'one.ts'), 'export function one() {}\n');
|
|
194
|
-
writeFileSync(join(workDir, 'two.ts'), 'export function two() {}\n');
|
|
195
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
196
|
-
await w.fullReindex();
|
|
197
|
-
expect(w.currentGraph.filesIndexed).toBe(2);
|
|
198
|
-
|
|
199
|
-
unlinkSync(join(workDir, 'one.ts'));
|
|
200
|
-
await w.applyDeletion(join(workDir, 'one.ts'));
|
|
201
|
-
expect(w.currentGraph.filesIndexed).toBe(1);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('serializes concurrent updates via the chain (no torn graph)', async () => {
|
|
205
|
-
// Drive multiple applyUpdates in parallel — the watcher should still end
|
|
206
|
-
// up consistent. (The internal chain only protects chokidar-triggered
|
|
207
|
-
// events, but applyUpdate is awaitable from the outside; here we test
|
|
208
|
-
// that two awaits in sequence produce a deterministic outcome.)
|
|
209
|
-
const a = join(workDir, 'a.ts');
|
|
210
|
-
const b = join(workDir, 'b.ts');
|
|
211
|
-
writeFileSync(a, 'export function a1() {}\n');
|
|
212
|
-
writeFileSync(b, 'export function b1() {}\n');
|
|
213
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
214
|
-
await w.fullReindex();
|
|
215
|
-
|
|
216
|
-
writeFileSync(a, 'export function a2() {}\n');
|
|
217
|
-
writeFileSync(b, 'export function b2() {}\n');
|
|
218
|
-
await Promise.all([w.applyUpdate(a, 'change'), w.applyUpdate(b, 'change')]);
|
|
219
|
-
|
|
220
|
-
const names = w.currentGraph.symbols.map((s) => s.name).sort();
|
|
221
|
-
expect(names).toEqual(['a2', 'b2']);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe('GraphWatcher — diagnostic output', () => {
|
|
226
|
-
it('writes one diagnostic line per applyUpdate', async () => {
|
|
227
|
-
writeFileSync(join(workDir, 'x.ts'), 'export function x() {}\n');
|
|
228
|
-
const lines: string[] = [];
|
|
229
|
-
const w = new GraphWatcher(workDir, { log: (s) => lines.push(s) });
|
|
230
|
-
// fullReindex is silent by design; only event-driven updates log.
|
|
231
|
-
await w.fullReindex();
|
|
232
|
-
const before = lines.length;
|
|
233
|
-
writeFileSync(join(workDir, 'x.ts'), 'export function x() { return 1; }\n');
|
|
234
|
-
await w.applyUpdate(join(workDir, 'x.ts'), 'change');
|
|
235
|
-
expect(lines.length).toBe(before + 1);
|
|
236
|
-
expect(lines[lines.length - 1]).toMatch(/x\.ts/);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('on-disk graph contains the new symbols after change', async () => {
|
|
240
|
-
writeFileSync(join(workDir, 'x.ts'), 'export function oldOne() {}\n');
|
|
241
|
-
const w = new GraphWatcher(workDir, { log: silentLog() });
|
|
242
|
-
await w.fullReindex();
|
|
243
|
-
writeFileSync(join(workDir, 'x.ts'), 'export function newOne() {}\n');
|
|
244
|
-
await w.applyUpdate(join(workDir, 'x.ts'), 'change');
|
|
245
|
-
|
|
246
|
-
const raw = readFileSync(join(repoDir(workDir), 'graph.json'), 'utf8');
|
|
247
|
-
expect(raw).toContain('newOne');
|
|
248
|
-
expect(raw).not.toContain('oldOne');
|
|
249
|
-
});
|
|
250
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"declaration": true,
|
|
13
|
-
"sourceMap": true,
|
|
14
|
-
"resolveJsonModule": true
|
|
15
|
-
},
|
|
16
|
-
"include": ["src/**/*"],
|
|
17
|
-
"exclude": ["node_modules", "dist", "tests"]
|
|
18
|
-
}
|