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.
- package/.claude/settings.local.json +14 -0
- package/README.md +304 -0
- package/dist/coordination/gradient-cache.d.ts +48 -0
- package/dist/coordination/gradient-cache.d.ts.map +1 -0
- package/dist/coordination/gradient-cache.js +145 -0
- package/dist/coordination/gradient-cache.js.map +1 -0
- package/dist/coordination/index.d.ts +10 -0
- package/dist/coordination/index.d.ts.map +1 -0
- package/dist/coordination/index.js +10 -0
- package/dist/coordination/index.js.map +1 -0
- package/dist/core/agent-executor.d.ts +31 -0
- package/dist/core/agent-executor.d.ts.map +1 -0
- package/dist/core/agent-executor.js +257 -0
- package/dist/core/agent-executor.js.map +1 -0
- package/dist/core/change-applier.d.ts +10 -0
- package/dist/core/change-applier.d.ts.map +1 -0
- package/dist/core/change-applier.js +32 -0
- package/dist/core/change-applier.js.map +1 -0
- package/dist/core/gradient.d.ts +60 -0
- package/dist/core/gradient.d.ts.map +1 -0
- package/dist/core/gradient.js +191 -0
- package/dist/core/gradient.js.map +1 -0
- package/dist/core/index.d.ts +24 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +24 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/mode-selector.d.ts +44 -0
- package/dist/core/mode-selector.d.ts.map +1 -0
- package/dist/core/mode-selector.js +208 -0
- package/dist/core/mode-selector.js.map +1 -0
- package/dist/core/signals/centrality.d.ts +44 -0
- package/dist/core/signals/centrality.d.ts.map +1 -0
- package/dist/core/signals/centrality.js +264 -0
- package/dist/core/signals/centrality.js.map +1 -0
- package/dist/core/signals/churn.d.ts +41 -0
- package/dist/core/signals/churn.d.ts.map +1 -0
- package/dist/core/signals/churn.js +188 -0
- package/dist/core/signals/churn.js.map +1 -0
- package/dist/core/signals/complexity.d.ts +29 -0
- package/dist/core/signals/complexity.d.ts.map +1 -0
- package/dist/core/signals/complexity.js +169 -0
- package/dist/core/signals/complexity.js.map +1 -0
- package/dist/core/signals/debt.d.ts +27 -0
- package/dist/core/signals/debt.d.ts.map +1 -0
- package/dist/core/signals/debt.js +80 -0
- package/dist/core/signals/debt.js.map +1 -0
- package/dist/core/signals/errors.d.ts +32 -0
- package/dist/core/signals/errors.d.ts.map +1 -0
- package/dist/core/signals/errors.js +73 -0
- package/dist/core/signals/errors.js.map +1 -0
- package/dist/core/signals/index.d.ts +19 -0
- package/dist/core/signals/index.d.ts.map +1 -0
- package/dist/core/signals/index.js +19 -0
- package/dist/core/signals/index.js.map +1 -0
- package/dist/cost/cost-tracker.d.ts +90 -0
- package/dist/cost/cost-tracker.d.ts.map +1 -0
- package/dist/cost/cost-tracker.js +305 -0
- package/dist/cost/cost-tracker.js.map +1 -0
- package/dist/cost/index.d.ts +56 -0
- package/dist/cost/index.d.ts.map +1 -0
- package/dist/cost/index.js +111 -0
- package/dist/cost/index.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/anthropic-client.d.ts +52 -0
- package/dist/llm/anthropic-client.d.ts.map +1 -0
- package/dist/llm/anthropic-client.js +310 -0
- package/dist/llm/anthropic-client.js.map +1 -0
- package/dist/llm/index.d.ts +27 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +34 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/prompts/complexity-reducer.d.ts +7 -0
- package/dist/prompts/complexity-reducer.d.ts.map +1 -0
- package/dist/prompts/complexity-reducer.js +55 -0
- package/dist/prompts/complexity-reducer.js.map +1 -0
- package/dist/prompts/debt-payer.d.ts +7 -0
- package/dist/prompts/debt-payer.d.ts.map +1 -0
- package/dist/prompts/debt-payer.js +55 -0
- package/dist/prompts/debt-payer.js.map +1 -0
- package/dist/prompts/error-reducer.d.ts +7 -0
- package/dist/prompts/error-reducer.d.ts.map +1 -0
- package/dist/prompts/error-reducer.js +54 -0
- package/dist/prompts/error-reducer.js.map +1 -0
- package/dist/prompts/index.d.ts +22 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +112 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/stabilizer.d.ts +7 -0
- package/dist/prompts/stabilizer.d.ts.map +1 -0
- package/dist/prompts/stabilizer.js +55 -0
- package/dist/prompts/stabilizer.js.map +1 -0
- package/dist/prompts/types.d.ts +14 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/prompts/types.js +5 -0
- package/dist/prompts/types.js.map +1 -0
- package/dist/trace/index.d.ts +51 -0
- package/dist/trace/index.d.ts.map +1 -0
- package/dist/trace/index.js +60 -0
- package/dist/trace/index.js.map +1 -0
- package/dist/trace/trace-event.d.ts +72 -0
- package/dist/trace/trace-event.d.ts.map +1 -0
- package/dist/trace/trace-event.js +244 -0
- package/dist/trace/trace-event.js.map +1 -0
- package/dist/types/index.d.ts +206 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/ci-provider.d.ts +43 -0
- package/dist/utils/ci-provider.d.ts.map +1 -0
- package/dist/utils/ci-provider.js +130 -0
- package/dist/utils/ci-provider.js.map +1 -0
- package/dist/utils/config.d.ts +31 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +85 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/error-provider.d.ts +51 -0
- package/dist/utils/error-provider.d.ts.map +1 -0
- package/dist/utils/error-provider.js +123 -0
- package/dist/utils/error-provider.js.map +1 -0
- package/dist/utils/file-utils.d.ts +18 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +95 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +74 -0
- package/dist/utils/logger.js.map +1 -0
- package/docs/IMPLEMENTATION-STATUS.md +199 -0
- package/docs/PHASE-0-COMPLETE.md +252 -0
- package/docs/PHASE-1-COMPLETE.md +204 -0
- package/docs/PHASE-2-COMPLETE.md +233 -0
- package/docs/PHASE2_COMPLETION_CHECKLIST.md +290 -0
- package/docs/PHASE2_INTEGRATION_SUMMARY.md +255 -0
- package/docs/PHASE2_QUICK_REFERENCE.md +365 -0
- package/docs/PHASE2_TEST_RESULTS.md +282 -0
- package/docs/ROADMAP.md +746 -0
- package/docs/SNAPSHOT.md +376 -0
- package/docs/adrs/ADR-001-signal-computation.md +76 -0
- package/docs/adrs/ADR-002-inhibitor-signals.md +108 -0
- package/docs/adrs/ADR-003-llm-integration.md +156 -0
- package/docs/adrs/ADR-004-process-architecture.md +175 -0
- package/docs/adrs/ADR-005-testing-strategy.md +243 -0
- package/docs/pitch.md +94 -0
- package/docs/specs/fourth-spec.md +1973 -0
- package/docs/specs/initial-spec.md +2096 -0
- package/docs/specs/second-spec.md +2690 -0
- package/package.json +50 -0
- package/src/coordination/gradient-cache.ts +185 -0
- package/src/coordination/index.ts +10 -0
- package/src/core/agent-executor.ts +327 -0
- package/src/core/change-applier.ts +338 -0
- package/src/core/gradient.ts +258 -0
- package/src/core/index.ts +24 -0
- package/src/core/mode-selector.ts +243 -0
- package/src/core/signals/centrality.ts +328 -0
- package/src/core/signals/churn.ts +239 -0
- package/src/core/signals/complexity.ts +206 -0
- package/src/core/signals/debt.ts +111 -0
- package/src/core/signals/errors.ts +93 -0
- package/src/core/signals/index.ts +19 -0
- package/src/cost/cost-tracker.ts +410 -0
- package/src/cost/index.ts +143 -0
- package/src/index.ts +43 -0
- package/src/llm/anthropic-client.ts +415 -0
- package/src/llm/index.ts +43 -0
- package/src/prompts/complexity-reducer.ts +59 -0
- package/src/prompts/debt-payer.ts +59 -0
- package/src/prompts/error-reducer.ts +58 -0
- package/src/prompts/index.ts +128 -0
- package/src/prompts/stabilizer.ts +59 -0
- package/src/prompts/types.ts +15 -0
- package/src/trace/README.md +178 -0
- package/src/trace/index.ts +88 -0
- package/src/trace/trace-event.ts +324 -0
- package/src/types/index.ts +271 -0
- package/src/utils/ci-provider.ts +145 -0
- package/src/utils/config.ts +95 -0
- package/src/utils/error-provider.ts +138 -0
- package/src/utils/file-utils.ts +111 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/logger.ts +94 -0
- package/test-8d713cc8-f4b7-403d-8153-57573172b94c.ts +3 -0
- package/tests/coordination/gradient-cache.test.ts +270 -0
- package/tests/core/agent-executor.test.ts +217 -0
- package/tests/core/change-applier.test.ts +336 -0
- package/tests/core/gradient.test.ts +263 -0
- package/tests/core/mode-selector.test.ts +239 -0
- package/tests/core/signals/centrality.test.ts +512 -0
- package/tests/core/signals/churn.test.ts +355 -0
- package/tests/core/signals/complexity.test.ts +284 -0
- package/tests/core/signals/debt.test.ts +437 -0
- package/tests/core/signals/errors.test.ts +350 -0
- package/tests/cost/cost-tracker.test.ts +475 -0
- package/tests/integration/phase2.test.ts +405 -0
- package/tests/llm/anthropic-client.test.ts +437 -0
- package/tests/prompts/prompts.test.ts +266 -0
- package/tests/trace/trace-event.test.ts +666 -0
- package/tests/utils/file-utils.test.ts +148 -0
- package/tsconfig.json +24 -0
- 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
|
+
});
|