claude-mycelium 2.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 (207) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/README.md +304 -0
  3. package/dist/coordination/gradient-cache.d.ts +48 -0
  4. package/dist/coordination/gradient-cache.d.ts.map +1 -0
  5. package/dist/coordination/gradient-cache.js +145 -0
  6. package/dist/coordination/gradient-cache.js.map +1 -0
  7. package/dist/coordination/index.d.ts +10 -0
  8. package/dist/coordination/index.d.ts.map +1 -0
  9. package/dist/coordination/index.js +10 -0
  10. package/dist/coordination/index.js.map +1 -0
  11. package/dist/core/agent-executor.d.ts +31 -0
  12. package/dist/core/agent-executor.d.ts.map +1 -0
  13. package/dist/core/agent-executor.js +257 -0
  14. package/dist/core/agent-executor.js.map +1 -0
  15. package/dist/core/change-applier.d.ts +10 -0
  16. package/dist/core/change-applier.d.ts.map +1 -0
  17. package/dist/core/change-applier.js +32 -0
  18. package/dist/core/change-applier.js.map +1 -0
  19. package/dist/core/gradient.d.ts +60 -0
  20. package/dist/core/gradient.d.ts.map +1 -0
  21. package/dist/core/gradient.js +191 -0
  22. package/dist/core/gradient.js.map +1 -0
  23. package/dist/core/index.d.ts +24 -0
  24. package/dist/core/index.d.ts.map +1 -0
  25. package/dist/core/index.js +24 -0
  26. package/dist/core/index.js.map +1 -0
  27. package/dist/core/mode-selector.d.ts +44 -0
  28. package/dist/core/mode-selector.d.ts.map +1 -0
  29. package/dist/core/mode-selector.js +208 -0
  30. package/dist/core/mode-selector.js.map +1 -0
  31. package/dist/core/signals/centrality.d.ts +44 -0
  32. package/dist/core/signals/centrality.d.ts.map +1 -0
  33. package/dist/core/signals/centrality.js +264 -0
  34. package/dist/core/signals/centrality.js.map +1 -0
  35. package/dist/core/signals/churn.d.ts +41 -0
  36. package/dist/core/signals/churn.d.ts.map +1 -0
  37. package/dist/core/signals/churn.js +188 -0
  38. package/dist/core/signals/churn.js.map +1 -0
  39. package/dist/core/signals/complexity.d.ts +29 -0
  40. package/dist/core/signals/complexity.d.ts.map +1 -0
  41. package/dist/core/signals/complexity.js +169 -0
  42. package/dist/core/signals/complexity.js.map +1 -0
  43. package/dist/core/signals/debt.d.ts +27 -0
  44. package/dist/core/signals/debt.d.ts.map +1 -0
  45. package/dist/core/signals/debt.js +80 -0
  46. package/dist/core/signals/debt.js.map +1 -0
  47. package/dist/core/signals/errors.d.ts +32 -0
  48. package/dist/core/signals/errors.d.ts.map +1 -0
  49. package/dist/core/signals/errors.js +73 -0
  50. package/dist/core/signals/errors.js.map +1 -0
  51. package/dist/core/signals/index.d.ts +19 -0
  52. package/dist/core/signals/index.d.ts.map +1 -0
  53. package/dist/core/signals/index.js +19 -0
  54. package/dist/core/signals/index.js.map +1 -0
  55. package/dist/cost/cost-tracker.d.ts +90 -0
  56. package/dist/cost/cost-tracker.d.ts.map +1 -0
  57. package/dist/cost/cost-tracker.js +305 -0
  58. package/dist/cost/cost-tracker.js.map +1 -0
  59. package/dist/cost/index.d.ts +56 -0
  60. package/dist/cost/index.d.ts.map +1 -0
  61. package/dist/cost/index.js +111 -0
  62. package/dist/cost/index.js.map +1 -0
  63. package/dist/index.d.ts +35 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +40 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/llm/anthropic-client.d.ts +52 -0
  68. package/dist/llm/anthropic-client.d.ts.map +1 -0
  69. package/dist/llm/anthropic-client.js +310 -0
  70. package/dist/llm/anthropic-client.js.map +1 -0
  71. package/dist/llm/index.d.ts +27 -0
  72. package/dist/llm/index.d.ts.map +1 -0
  73. package/dist/llm/index.js +34 -0
  74. package/dist/llm/index.js.map +1 -0
  75. package/dist/prompts/complexity-reducer.d.ts +7 -0
  76. package/dist/prompts/complexity-reducer.d.ts.map +1 -0
  77. package/dist/prompts/complexity-reducer.js +55 -0
  78. package/dist/prompts/complexity-reducer.js.map +1 -0
  79. package/dist/prompts/debt-payer.d.ts +7 -0
  80. package/dist/prompts/debt-payer.d.ts.map +1 -0
  81. package/dist/prompts/debt-payer.js +55 -0
  82. package/dist/prompts/debt-payer.js.map +1 -0
  83. package/dist/prompts/error-reducer.d.ts +7 -0
  84. package/dist/prompts/error-reducer.d.ts.map +1 -0
  85. package/dist/prompts/error-reducer.js +54 -0
  86. package/dist/prompts/error-reducer.js.map +1 -0
  87. package/dist/prompts/index.d.ts +22 -0
  88. package/dist/prompts/index.d.ts.map +1 -0
  89. package/dist/prompts/index.js +112 -0
  90. package/dist/prompts/index.js.map +1 -0
  91. package/dist/prompts/stabilizer.d.ts +7 -0
  92. package/dist/prompts/stabilizer.d.ts.map +1 -0
  93. package/dist/prompts/stabilizer.js +55 -0
  94. package/dist/prompts/stabilizer.js.map +1 -0
  95. package/dist/prompts/types.d.ts +14 -0
  96. package/dist/prompts/types.d.ts.map +1 -0
  97. package/dist/prompts/types.js +5 -0
  98. package/dist/prompts/types.js.map +1 -0
  99. package/dist/trace/index.d.ts +51 -0
  100. package/dist/trace/index.d.ts.map +1 -0
  101. package/dist/trace/index.js +60 -0
  102. package/dist/trace/index.js.map +1 -0
  103. package/dist/trace/trace-event.d.ts +72 -0
  104. package/dist/trace/trace-event.d.ts.map +1 -0
  105. package/dist/trace/trace-event.js +244 -0
  106. package/dist/trace/trace-event.js.map +1 -0
  107. package/dist/types/index.d.ts +206 -0
  108. package/dist/types/index.d.ts.map +1 -0
  109. package/dist/types/index.js +6 -0
  110. package/dist/types/index.js.map +1 -0
  111. package/dist/utils/ci-provider.d.ts +43 -0
  112. package/dist/utils/ci-provider.d.ts.map +1 -0
  113. package/dist/utils/ci-provider.js +130 -0
  114. package/dist/utils/ci-provider.js.map +1 -0
  115. package/dist/utils/config.d.ts +31 -0
  116. package/dist/utils/config.d.ts.map +1 -0
  117. package/dist/utils/config.js +85 -0
  118. package/dist/utils/config.js.map +1 -0
  119. package/dist/utils/error-provider.d.ts +51 -0
  120. package/dist/utils/error-provider.d.ts.map +1 -0
  121. package/dist/utils/error-provider.js +123 -0
  122. package/dist/utils/error-provider.js.map +1 -0
  123. package/dist/utils/file-utils.d.ts +18 -0
  124. package/dist/utils/file-utils.d.ts.map +1 -0
  125. package/dist/utils/file-utils.js +95 -0
  126. package/dist/utils/file-utils.js.map +1 -0
  127. package/dist/utils/index.d.ts +10 -0
  128. package/dist/utils/index.d.ts.map +1 -0
  129. package/dist/utils/index.js +10 -0
  130. package/dist/utils/index.js.map +1 -0
  131. package/dist/utils/logger.d.ts +36 -0
  132. package/dist/utils/logger.d.ts.map +1 -0
  133. package/dist/utils/logger.js +74 -0
  134. package/dist/utils/logger.js.map +1 -0
  135. package/docs/IMPLEMENTATION-STATUS.md +199 -0
  136. package/docs/PHASE-0-COMPLETE.md +252 -0
  137. package/docs/PHASE-1-COMPLETE.md +204 -0
  138. package/docs/PHASE-2-COMPLETE.md +233 -0
  139. package/docs/PHASE2_COMPLETION_CHECKLIST.md +290 -0
  140. package/docs/PHASE2_INTEGRATION_SUMMARY.md +255 -0
  141. package/docs/PHASE2_QUICK_REFERENCE.md +365 -0
  142. package/docs/PHASE2_TEST_RESULTS.md +282 -0
  143. package/docs/ROADMAP.md +746 -0
  144. package/docs/SNAPSHOT.md +376 -0
  145. package/docs/adrs/ADR-001-signal-computation.md +76 -0
  146. package/docs/adrs/ADR-002-inhibitor-signals.md +108 -0
  147. package/docs/adrs/ADR-003-llm-integration.md +156 -0
  148. package/docs/adrs/ADR-004-process-architecture.md +175 -0
  149. package/docs/adrs/ADR-005-testing-strategy.md +243 -0
  150. package/docs/pitch.md +94 -0
  151. package/docs/specs/fourth-spec.md +1973 -0
  152. package/docs/specs/initial-spec.md +2096 -0
  153. package/docs/specs/second-spec.md +2690 -0
  154. package/package.json +50 -0
  155. package/src/coordination/gradient-cache.ts +185 -0
  156. package/src/coordination/index.ts +10 -0
  157. package/src/core/agent-executor.ts +327 -0
  158. package/src/core/change-applier.ts +338 -0
  159. package/src/core/gradient.ts +258 -0
  160. package/src/core/index.ts +24 -0
  161. package/src/core/mode-selector.ts +243 -0
  162. package/src/core/signals/centrality.ts +328 -0
  163. package/src/core/signals/churn.ts +239 -0
  164. package/src/core/signals/complexity.ts +206 -0
  165. package/src/core/signals/debt.ts +111 -0
  166. package/src/core/signals/errors.ts +93 -0
  167. package/src/core/signals/index.ts +19 -0
  168. package/src/cost/cost-tracker.ts +410 -0
  169. package/src/cost/index.ts +143 -0
  170. package/src/index.ts +43 -0
  171. package/src/llm/anthropic-client.ts +415 -0
  172. package/src/llm/index.ts +43 -0
  173. package/src/prompts/complexity-reducer.ts +59 -0
  174. package/src/prompts/debt-payer.ts +59 -0
  175. package/src/prompts/error-reducer.ts +58 -0
  176. package/src/prompts/index.ts +128 -0
  177. package/src/prompts/stabilizer.ts +59 -0
  178. package/src/prompts/types.ts +15 -0
  179. package/src/trace/README.md +178 -0
  180. package/src/trace/index.ts +88 -0
  181. package/src/trace/trace-event.ts +324 -0
  182. package/src/types/index.ts +271 -0
  183. package/src/utils/ci-provider.ts +145 -0
  184. package/src/utils/config.ts +95 -0
  185. package/src/utils/error-provider.ts +138 -0
  186. package/src/utils/file-utils.ts +111 -0
  187. package/src/utils/index.ts +10 -0
  188. package/src/utils/logger.ts +94 -0
  189. package/test-8d713cc8-f4b7-403d-8153-57573172b94c.ts +3 -0
  190. package/tests/coordination/gradient-cache.test.ts +270 -0
  191. package/tests/core/agent-executor.test.ts +217 -0
  192. package/tests/core/change-applier.test.ts +336 -0
  193. package/tests/core/gradient.test.ts +263 -0
  194. package/tests/core/mode-selector.test.ts +239 -0
  195. package/tests/core/signals/centrality.test.ts +512 -0
  196. package/tests/core/signals/churn.test.ts +355 -0
  197. package/tests/core/signals/complexity.test.ts +284 -0
  198. package/tests/core/signals/debt.test.ts +437 -0
  199. package/tests/core/signals/errors.test.ts +350 -0
  200. package/tests/cost/cost-tracker.test.ts +475 -0
  201. package/tests/integration/phase2.test.ts +405 -0
  202. package/tests/llm/anthropic-client.test.ts +437 -0
  203. package/tests/prompts/prompts.test.ts +266 -0
  204. package/tests/trace/trace-event.test.ts +666 -0
  205. package/tests/utils/file-utils.test.ts +148 -0
  206. package/tsconfig.json +24 -0
  207. package/vitest.config.ts +28 -0
@@ -0,0 +1,355 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { execSync } from 'child_process';
5
+ import {
6
+ calculateChurn,
7
+ calculateChurnBatch,
8
+ clearChurnCache,
9
+ getChurnCacheStats,
10
+ } from '../../../src/core/signals/churn';
11
+
12
+ const TEST_DIR = '.test-temp-churn';
13
+
14
+ describe('churn signal', () => {
15
+ beforeEach(() => {
16
+ // Clean up any previous test artifacts
17
+ if (fs.existsSync(TEST_DIR)) {
18
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
19
+ }
20
+ fs.mkdirSync(TEST_DIR, { recursive: true });
21
+
22
+ // Clear cache before each test
23
+ clearChurnCache();
24
+ });
25
+
26
+ afterEach(() => {
27
+ if (fs.existsSync(TEST_DIR)) {
28
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
29
+ }
30
+ clearChurnCache();
31
+ });
32
+
33
+ describe('calculateChurn', () => {
34
+ it('returns zero churn for non-git directory', async () => {
35
+ const testFile = path.join(TEST_DIR, 'test.ts');
36
+ fs.writeFileSync(testFile, 'console.log("test");');
37
+
38
+ const result = await calculateChurn(testFile);
39
+
40
+ expect(result.commits).toBe(0);
41
+ expect(result.normalized).toBe(0);
42
+ });
43
+
44
+ it('calculates churn for git repository', async () => {
45
+ // Initialize git repo
46
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
47
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
48
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
49
+
50
+ // Create and commit a file
51
+ const testFile = path.join(TEST_DIR, 'test.ts');
52
+ fs.writeFileSync(testFile, 'console.log("test");');
53
+ execSync('git add test.ts', { cwd: TEST_DIR, stdio: 'ignore' });
54
+ execSync('git commit -m "Initial commit"', { cwd: TEST_DIR, stdio: 'ignore' });
55
+
56
+ const result = await calculateChurn(testFile);
57
+
58
+ expect(result.commits).toBe(1);
59
+ expect(result.normalized).toBeGreaterThanOrEqual(0);
60
+ expect(result.normalized).toBeLessThanOrEqual(1);
61
+ expect(result.cachedAt).toBeInstanceOf(Date);
62
+ });
63
+
64
+ it('normalizes churn correctly with multiple files', async () => {
65
+ // Initialize git repo
66
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
67
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
68
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
69
+
70
+ // Create files with different churn rates
71
+ const file1 = path.join(TEST_DIR, 'high-churn.ts');
72
+ const file2 = path.join(TEST_DIR, 'low-churn.ts');
73
+
74
+ // File 1: 3 commits
75
+ fs.writeFileSync(file1, 'console.log("v1");');
76
+ execSync('git add high-churn.ts', { cwd: TEST_DIR, stdio: 'ignore' });
77
+ execSync('git commit -m "Commit 1"', { cwd: TEST_DIR, stdio: 'ignore' });
78
+
79
+ fs.writeFileSync(file1, 'console.log("v2");');
80
+ execSync('git add high-churn.ts', { cwd: TEST_DIR, stdio: 'ignore' });
81
+ execSync('git commit -m "Commit 2"', { cwd: TEST_DIR, stdio: 'ignore' });
82
+
83
+ fs.writeFileSync(file1, 'console.log("v3");');
84
+ execSync('git add high-churn.ts', { cwd: TEST_DIR, stdio: 'ignore' });
85
+ execSync('git commit -m "Commit 3"', { cwd: TEST_DIR, stdio: 'ignore' });
86
+
87
+ // File 2: 1 commit
88
+ fs.writeFileSync(file2, 'console.log("stable");');
89
+ execSync('git add low-churn.ts', { cwd: TEST_DIR, stdio: 'ignore' });
90
+ execSync('git commit -m "Commit 4"', { cwd: TEST_DIR, stdio: 'ignore' });
91
+
92
+ // Clear cache to force rebuild
93
+ clearChurnCache();
94
+
95
+ const result1 = await calculateChurn(file1);
96
+ const result2 = await calculateChurn(file2);
97
+
98
+ // High churn file should have normalized = 1.0 (max)
99
+ expect(result1.commits).toBe(3);
100
+ expect(result1.normalized).toBe(1.0);
101
+
102
+ // Low churn file should have normalized = 1/3
103
+ expect(result2.commits).toBe(1);
104
+ expect(result2.normalized).toBeCloseTo(1 / 3, 2);
105
+ });
106
+
107
+ it('uses cache for subsequent calls', async () => {
108
+ // Initialize git repo with one commit
109
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
110
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
111
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
112
+
113
+ const testFile = path.join(TEST_DIR, 'test.ts');
114
+ fs.writeFileSync(testFile, 'console.log("test");');
115
+ execSync('git add test.ts', { cwd: TEST_DIR, stdio: 'ignore' });
116
+ execSync('git commit -m "Initial"', { cwd: TEST_DIR, stdio: 'ignore' });
117
+
118
+ // First call
119
+ const result1 = await calculateChurn(testFile);
120
+
121
+ // Second call should use cache (same cachedAt timestamp)
122
+ const result2 = await calculateChurn(testFile);
123
+
124
+ expect(result1.cachedAt).toEqual(result2.cachedAt);
125
+ expect(result1.commits).toBe(result2.commits);
126
+ });
127
+
128
+ it('handles files with no commits', async () => {
129
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
130
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
131
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
132
+
133
+ // Create file but don't commit
134
+ const testFile = path.join(TEST_DIR, 'untracked.ts');
135
+ fs.writeFileSync(testFile, 'console.log("untracked");');
136
+
137
+ // Commit another file to have some git history
138
+ const committedFile = path.join(TEST_DIR, 'committed.ts');
139
+ fs.writeFileSync(committedFile, 'console.log("committed");');
140
+ execSync('git add committed.ts', { cwd: TEST_DIR, stdio: 'ignore' });
141
+ execSync('git commit -m "Commit"', { cwd: TEST_DIR, stdio: 'ignore' });
142
+
143
+ clearChurnCache();
144
+
145
+ const result = await calculateChurn(testFile);
146
+
147
+ expect(result.commits).toBe(0);
148
+ expect(result.normalized).toBe(0);
149
+ });
150
+
151
+ it('respects 30-day lookback window', async () => {
152
+ // This test verifies the implementation uses --since="30 days"
153
+ // We can't easily create old commits in a unit test,
154
+ // so we just verify the behavior with recent commits
155
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
156
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
157
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
158
+
159
+ const testFile = path.join(TEST_DIR, 'test.ts');
160
+ fs.writeFileSync(testFile, 'console.log("test");');
161
+ execSync('git add test.ts', { cwd: TEST_DIR, stdio: 'ignore' });
162
+ execSync('git commit -m "Recent commit"', { cwd: TEST_DIR, stdio: 'ignore' });
163
+
164
+ const result = await calculateChurn(testFile);
165
+
166
+ // Recent commit should be counted
167
+ expect(result.commits).toBeGreaterThan(0);
168
+ });
169
+ });
170
+
171
+ describe('cache behavior', () => {
172
+ it('cache expires after TTL', async () => {
173
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
174
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
175
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
176
+
177
+ const testFile = path.join(TEST_DIR, 'test.ts');
178
+ fs.writeFileSync(testFile, 'console.log("test");');
179
+ execSync('git add test.ts', { cwd: TEST_DIR, stdio: 'ignore' });
180
+ execSync('git commit -m "Commit"', { cwd: TEST_DIR, stdio: 'ignore' });
181
+
182
+ // First call builds cache
183
+ await calculateChurn(testFile);
184
+
185
+ const stats = getChurnCacheStats();
186
+ expect(stats).not.toBeNull();
187
+ expect(stats?.cached).toBe(true);
188
+ expect(stats?.fileCount).toBeGreaterThan(0);
189
+ expect(stats?.expiresIn).toBeGreaterThan(0);
190
+ });
191
+
192
+ it('clearChurnCache removes cached data', async () => {
193
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
194
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
195
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
196
+
197
+ const testFile = path.join(TEST_DIR, 'test.ts');
198
+ fs.writeFileSync(testFile, 'console.log("test");');
199
+ execSync('git add test.ts', { cwd: TEST_DIR, stdio: 'ignore' });
200
+ execSync('git commit -m "Commit"', { cwd: TEST_DIR, stdio: 'ignore' });
201
+
202
+ await calculateChurn(testFile);
203
+
204
+ let stats = getChurnCacheStats();
205
+ expect(stats).not.toBeNull();
206
+
207
+ clearChurnCache();
208
+
209
+ stats = getChurnCacheStats();
210
+ expect(stats).toBeNull();
211
+ });
212
+ });
213
+
214
+ describe('calculateChurnBatch', () => {
215
+ it('calculates churn for multiple files efficiently', async () => {
216
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
217
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
218
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
219
+
220
+ const file1 = path.join(TEST_DIR, 'file1.ts');
221
+ const file2 = path.join(TEST_DIR, 'file2.ts');
222
+ const file3 = path.join(TEST_DIR, 'file3.ts');
223
+
224
+ fs.writeFileSync(file1, 'console.log("file1");');
225
+ fs.writeFileSync(file2, 'console.log("file2");');
226
+ fs.writeFileSync(file3, 'console.log("file3");');
227
+
228
+ execSync('git add .', { cwd: TEST_DIR, stdio: 'ignore' });
229
+ execSync('git commit -m "Add files"', { cwd: TEST_DIR, stdio: 'ignore' });
230
+
231
+ fs.writeFileSync(file1, 'console.log("file1 v2");');
232
+ execSync('git add file1.ts', { cwd: TEST_DIR, stdio: 'ignore' });
233
+ execSync('git commit -m "Update file1"', { cwd: TEST_DIR, stdio: 'ignore' });
234
+
235
+ clearChurnCache();
236
+
237
+ const results = await calculateChurnBatch([file1, file2, file3]);
238
+
239
+ expect(results.size).toBe(3);
240
+ expect(results.get(file1)?.commits).toBe(2);
241
+ expect(results.get(file2)?.commits).toBe(1);
242
+ expect(results.get(file3)?.commits).toBe(1);
243
+
244
+ // File1 should have max normalized value
245
+ expect(results.get(file1)?.normalized).toBe(1.0);
246
+ });
247
+
248
+ it('reuses cache across batch operations', async () => {
249
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
250
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
251
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
252
+
253
+ const file1 = path.join(TEST_DIR, 'file1.ts');
254
+ fs.writeFileSync(file1, 'console.log("file1");');
255
+ execSync('git add file1.ts', { cwd: TEST_DIR, stdio: 'ignore' });
256
+ execSync('git commit -m "Commit"', { cwd: TEST_DIR, stdio: 'ignore' });
257
+
258
+ clearChurnCache();
259
+
260
+ const results = await calculateChurnBatch([file1]);
261
+
262
+ // Cache should be built
263
+ const stats = getChurnCacheStats();
264
+ expect(stats).not.toBeNull();
265
+ expect(stats?.fileCount).toBeGreaterThan(0);
266
+
267
+ // All results should have same cachedAt timestamp
268
+ const cachedAt = results.get(file1)?.cachedAt;
269
+ expect(cachedAt).toBeInstanceOf(Date);
270
+ });
271
+ });
272
+
273
+ describe('edge cases', () => {
274
+ it('handles empty git repository', async () => {
275
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
276
+
277
+ const testFile = path.join(TEST_DIR, 'test.ts');
278
+ fs.writeFileSync(testFile, 'console.log("test");');
279
+
280
+ const result = await calculateChurn(testFile);
281
+
282
+ expect(result.commits).toBe(0);
283
+ expect(result.normalized).toBe(0);
284
+ });
285
+
286
+ it('handles files with special characters in name', async () => {
287
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
288
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
289
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
290
+
291
+ const testFile = path.join(TEST_DIR, 'test-file.component.ts');
292
+ fs.writeFileSync(testFile, 'console.log("test");');
293
+ execSync('git add "test-file.component.ts"', { cwd: TEST_DIR, stdio: 'ignore' });
294
+ execSync('git commit -m "Add file"', { cwd: TEST_DIR, stdio: 'ignore' });
295
+
296
+ clearChurnCache();
297
+
298
+ const result = await calculateChurn(testFile);
299
+
300
+ expect(result.commits).toBe(1);
301
+ expect(result.normalized).toBeGreaterThanOrEqual(0);
302
+ });
303
+
304
+ it('handles large number of commits gracefully', async () => {
305
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
306
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
307
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
308
+
309
+ const testFile = path.join(TEST_DIR, 'active-file.ts');
310
+
311
+ // Create 10 commits (representing high churn)
312
+ for (let i = 1; i <= 10; i++) {
313
+ fs.writeFileSync(testFile, `console.log("version ${i}");`);
314
+ execSync('git add active-file.ts', { cwd: TEST_DIR, stdio: 'ignore' });
315
+ execSync(`git commit -m "Update ${i}"`, { cwd: TEST_DIR, stdio: 'ignore' });
316
+ }
317
+
318
+ clearChurnCache();
319
+
320
+ const result = await calculateChurn(testFile);
321
+
322
+ expect(result.commits).toBe(10);
323
+ expect(result.normalized).toBe(1.0);
324
+ });
325
+ });
326
+
327
+ describe('getChurnCacheStats', () => {
328
+ it('returns null when no cache exists', () => {
329
+ const stats = getChurnCacheStats();
330
+ expect(stats).toBeNull();
331
+ });
332
+
333
+ it('returns cache statistics when cache exists', async () => {
334
+ execSync('git init', { cwd: TEST_DIR, stdio: 'ignore' });
335
+ execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' });
336
+ execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' });
337
+
338
+ const testFile = path.join(TEST_DIR, 'test.ts');
339
+ fs.writeFileSync(testFile, 'console.log("test");');
340
+ execSync('git add test.ts', { cwd: TEST_DIR, stdio: 'ignore' });
341
+ execSync('git commit -m "Commit"', { cwd: TEST_DIR, stdio: 'ignore' });
342
+
343
+ await calculateChurn(testFile);
344
+
345
+ const stats = getChurnCacheStats();
346
+
347
+ expect(stats).not.toBeNull();
348
+ expect(stats?.cached).toBe(true);
349
+ expect(stats?.fileCount).toBeGreaterThan(0);
350
+ expect(stats?.maxChurn).toBeGreaterThan(0);
351
+ expect(stats?.expiresIn).toBeGreaterThan(0);
352
+ expect(stats?.expiresIn).toBeLessThanOrEqual(300); // 5 minutes max
353
+ });
354
+ });
355
+ });
@@ -0,0 +1,284 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { calculateComplexity } from '../../../src/core/signals/complexity';
5
+
6
+ const TEST_DIR = '.test-temp-complexity';
7
+
8
+ describe('complexity signal', () => {
9
+ beforeEach(() => {
10
+ if (!fs.existsSync(TEST_DIR)) {
11
+ fs.mkdirSync(TEST_DIR, { recursive: true });
12
+ }
13
+ });
14
+
15
+ afterEach(() => {
16
+ if (fs.existsSync(TEST_DIR)) {
17
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
18
+ }
19
+ });
20
+
21
+ describe('calculateComplexity', () => {
22
+ it('calculates complexity for simple function', async () => {
23
+ const testFile = path.join(TEST_DIR, 'simple.ts');
24
+ const content = `
25
+ function add(a: number, b: number): number {
26
+ return a + b;
27
+ }
28
+ `.trim();
29
+
30
+ fs.writeFileSync(testFile, content);
31
+
32
+ const result = await calculateComplexity(testFile);
33
+
34
+ expect(result.cyclomaticComplexity).toBe(1); // Base complexity
35
+ expect(result.loc).toBeGreaterThan(0);
36
+ expect(result.normalized).toBeGreaterThanOrEqual(0);
37
+ expect(result.normalized).toBeLessThanOrEqual(1);
38
+ });
39
+
40
+ it('counts if statements', async () => {
41
+ const testFile = path.join(TEST_DIR, 'if.ts');
42
+ const content = `
43
+ function checkValue(x: number): string {
44
+ if (x > 0) {
45
+ return 'positive';
46
+ } else if (x < 0) {
47
+ return 'negative';
48
+ } else {
49
+ return 'zero';
50
+ }
51
+ }
52
+ `.trim();
53
+
54
+ fs.writeFileSync(testFile, content);
55
+
56
+ const result = await calculateComplexity(testFile);
57
+
58
+ // Base (1) + if (1) + else if (1) = 3
59
+ expect(result.cyclomaticComplexity).toBe(3);
60
+ });
61
+
62
+ it('counts loops', async () => {
63
+ const testFile = path.join(TEST_DIR, 'loops.ts');
64
+ const content = `
65
+ function process(items: number[]): void {
66
+ for (let i = 0; i < items.length; i++) {
67
+ console.log(items[i]);
68
+ }
69
+
70
+ while (items.length > 0) {
71
+ items.pop();
72
+ }
73
+
74
+ for (const item of items) {
75
+ console.log(item);
76
+ }
77
+ }
78
+ `.trim();
79
+
80
+ fs.writeFileSync(testFile, content);
81
+
82
+ const result = await calculateComplexity(testFile);
83
+
84
+ // Base (1) + for (1) + while (1) + for-of (1) = 4
85
+ expect(result.cyclomaticComplexity).toBe(4);
86
+ });
87
+
88
+ it('counts switch cases', async () => {
89
+ const testFile = path.join(TEST_DIR, 'switch.ts');
90
+ const content = `
91
+ function getDay(day: number): string {
92
+ switch (day) {
93
+ case 1:
94
+ return 'Monday';
95
+ case 2:
96
+ return 'Tuesday';
97
+ case 3:
98
+ return 'Wednesday';
99
+ default:
100
+ return 'Unknown';
101
+ }
102
+ }
103
+ `.trim();
104
+
105
+ fs.writeFileSync(testFile, content);
106
+
107
+ const result = await calculateComplexity(testFile);
108
+
109
+ // Base (1) + case1 (1) + case2 (1) + case3 (1) = 4
110
+ // (default doesn't count)
111
+ expect(result.cyclomaticComplexity).toBe(4);
112
+ });
113
+
114
+ it('counts logical operators', async () => {
115
+ const testFile = path.join(TEST_DIR, 'logical.ts');
116
+ const content = `
117
+ function validate(x: number, y: number): boolean {
118
+ return x > 0 && y > 0 || x === 0 && y === 0;
119
+ }
120
+ `.trim();
121
+
122
+ fs.writeFileSync(testFile, content);
123
+
124
+ const result = await calculateComplexity(testFile);
125
+
126
+ // Base (1) + && (1) + || (1) + && (1) = 4
127
+ expect(result.cyclomaticComplexity).toBe(4);
128
+ });
129
+
130
+ it('counts try-catch', async () => {
131
+ const testFile = path.join(TEST_DIR, 'trycatch.ts');
132
+ const content = `
133
+ function riskyOperation(): void {
134
+ try {
135
+ throw new Error('test');
136
+ } catch (error) {
137
+ console.error(error);
138
+ }
139
+ }
140
+ `.trim();
141
+
142
+ fs.writeFileSync(testFile, content);
143
+
144
+ const result = await calculateComplexity(testFile);
145
+
146
+ // Base (1) + catch (1) = 2
147
+ expect(result.cyclomaticComplexity).toBe(2);
148
+ });
149
+
150
+ it('counts ternary operators', async () => {
151
+ const testFile = path.join(TEST_DIR, 'ternary.ts');
152
+ const content = `
153
+ function classify(x: number): string {
154
+ return x > 0 ? 'positive' : x < 0 ? 'negative' : 'zero';
155
+ }
156
+ `.trim();
157
+
158
+ fs.writeFileSync(testFile, content);
159
+
160
+ const result = await calculateComplexity(testFile);
161
+
162
+ // Base (1) + ternary (1) + nested ternary (1) = 3
163
+ expect(result.cyclomaticComplexity).toBe(3);
164
+ });
165
+
166
+ it('normalizes complexity correctly', async () => {
167
+ const testFile = path.join(TEST_DIR, 'normalize.ts');
168
+ // Create file with complexity 10, 20 LOC
169
+ // (10 / 20) / 0.5 = 1.0
170
+ const content = `
171
+ function complex(x: number): number {
172
+ if (x > 0) {
173
+ if (x > 10) {
174
+ if (x > 20) {
175
+ return 1;
176
+ }
177
+ }
178
+ }
179
+ if (x < 0) {
180
+ if (x < -10) {
181
+ if (x < -20) {
182
+ return -1;
183
+ }
184
+ }
185
+ }
186
+ return 0;
187
+ }
188
+ `.trim();
189
+
190
+ fs.writeFileSync(testFile, content);
191
+
192
+ const result = await calculateComplexity(testFile);
193
+
194
+ expect(result.normalized).toBeGreaterThan(0);
195
+ expect(result.normalized).toBeLessThanOrEqual(1.0);
196
+ });
197
+
198
+ it('excludes comments from LOC', async () => {
199
+ const testFile = path.join(TEST_DIR, 'comments.ts');
200
+ const content = `
201
+ // This is a comment
202
+ function add(a: number, b: number): number {
203
+ // Another comment
204
+ return a + b; // Inline comment
205
+ }
206
+
207
+ /*
208
+ * Block comment
209
+ * Multiple lines
210
+ */
211
+ `.trim();
212
+
213
+ fs.writeFileSync(testFile, content);
214
+
215
+ const result = await calculateComplexity(testFile);
216
+
217
+ // Should only count actual code lines, not comments
218
+ expect(result.loc).toBeLessThan(content.split('\n').length);
219
+ });
220
+
221
+ it('handles complex real-world function', async () => {
222
+ const testFile = path.join(TEST_DIR, 'realworld.ts');
223
+ const content = `
224
+ function processData(data: any[]): any[] {
225
+ const result = [];
226
+
227
+ for (const item of data) {
228
+ if (item.type === 'user') {
229
+ if (item.active && item.verified) {
230
+ result.push(item);
231
+ }
232
+ } else if (item.type === 'admin') {
233
+ if (item.permissions.includes('write')) {
234
+ result.push(item);
235
+ }
236
+ }
237
+
238
+ try {
239
+ if (item.metadata) {
240
+ item.metadata.processed = true;
241
+ }
242
+ } catch (error) {
243
+ console.error(error);
244
+ }
245
+ }
246
+
247
+ return result.length > 0 ? result : [];
248
+ }
249
+ `.trim();
250
+
251
+ fs.writeFileSync(testFile, content);
252
+
253
+ const result = await calculateComplexity(testFile);
254
+
255
+ // Should have significant complexity
256
+ expect(result.cyclomaticComplexity).toBeGreaterThan(5);
257
+ expect(result.normalized).toBeGreaterThan(0);
258
+ });
259
+
260
+ it('returns zero for non-TypeScript files', async () => {
261
+ const testFile = path.join(TEST_DIR, 'data.json');
262
+ fs.writeFileSync(testFile, '{"key": "value"}');
263
+
264
+ const result = await calculateComplexity(testFile);
265
+
266
+ expect(result.cyclomaticComplexity).toBe(0);
267
+ expect(result.normalized).toBe(0);
268
+ });
269
+
270
+ it('handles syntax errors gracefully', async () => {
271
+ const testFile = path.join(TEST_DIR, 'invalid.ts');
272
+ const content = 'function broken() { this is not valid typescript';
273
+
274
+ fs.writeFileSync(testFile, content);
275
+
276
+ const result = await calculateComplexity(testFile);
277
+
278
+ // Should fallback to zero complexity
279
+ expect(result.cyclomaticComplexity).toBe(0);
280
+ expect(result.normalized).toBe(0);
281
+ expect(result.loc).toBeGreaterThan(0);
282
+ });
283
+ });
284
+ });