@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
@@ -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
- });
@@ -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
- }