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,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode Selection
|
|
3
|
+
* Per initial-spec §3
|
|
4
|
+
*
|
|
5
|
+
* Selects agent mode based on file's dominant signal.
|
|
6
|
+
* Each mode focuses on a specific type of improvement.
|
|
7
|
+
*
|
|
8
|
+
* Modes:
|
|
9
|
+
* - error_reducer: Fix bugs and add error handling
|
|
10
|
+
* - complexity_reducer: Simplify and refactor
|
|
11
|
+
* - debt_payer: Fix lint issues and improve types
|
|
12
|
+
* - stabilizer: Reduce churn with tests and docs
|
|
13
|
+
* - explorer: Challenge assumptions (Phase 4)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { logDebug, logWarn } from '../utils/logger.js';
|
|
17
|
+
import type { Mode } from '../types/index.js';
|
|
18
|
+
import type { GradientScore } from './gradient.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Minimum signal value to be considered "dominant"
|
|
22
|
+
* Per initial-spec §3, signals must exceed 0.3 threshold
|
|
23
|
+
*/
|
|
24
|
+
const DOMINANCE_THRESHOLD = 0.3;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Select mode based on gradient score
|
|
28
|
+
*/
|
|
29
|
+
export function selectMode(gradient: GradientScore): Mode {
|
|
30
|
+
const { dominantSignal } = gradient;
|
|
31
|
+
|
|
32
|
+
// Check if dominant signal exceeds threshold
|
|
33
|
+
if (dominantSignal.value < DOMINANCE_THRESHOLD) {
|
|
34
|
+
// No signal is dominant - default to debt_payer
|
|
35
|
+
logDebug('No dominant signal, defaulting to debt_payer', {
|
|
36
|
+
file: gradient.file,
|
|
37
|
+
maxSignal: dominantSignal.value.toFixed(3),
|
|
38
|
+
});
|
|
39
|
+
return 'debt_payer';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Map dominant signal to mode
|
|
43
|
+
const modeMap: Record<string, Mode> = {
|
|
44
|
+
error_rate: 'error_reducer',
|
|
45
|
+
complexity: 'complexity_reducer',
|
|
46
|
+
debt: 'debt_payer',
|
|
47
|
+
churn: 'stabilizer',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const mode = modeMap[dominantSignal.name];
|
|
51
|
+
|
|
52
|
+
logDebug('Mode selected', {
|
|
53
|
+
file: gradient.file,
|
|
54
|
+
mode,
|
|
55
|
+
signal: dominantSignal.name,
|
|
56
|
+
value: dominantSignal.value.toFixed(3),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return mode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get mode-specific instructions for the agent
|
|
64
|
+
* These guide the LLM on what to focus on
|
|
65
|
+
*/
|
|
66
|
+
export function getModeInstructions(mode: Mode): string {
|
|
67
|
+
const instructions: Record<Mode, string> = {
|
|
68
|
+
error_reducer: `## Error Reducer Mode
|
|
69
|
+
|
|
70
|
+
Your focus: Fix bugs and improve error handling.
|
|
71
|
+
|
|
72
|
+
Allowed actions:
|
|
73
|
+
- Fix logic errors and type errors
|
|
74
|
+
- Add try/catch blocks for error handling
|
|
75
|
+
- Improve error messages and logging
|
|
76
|
+
- Add input validation
|
|
77
|
+
- Fix off-by-one errors and edge cases
|
|
78
|
+
|
|
79
|
+
Avoid:
|
|
80
|
+
- Large refactors (use complexity_reducer for that)
|
|
81
|
+
- Fixing lint warnings (use debt_payer for that)
|
|
82
|
+
- Major architectural changes`,
|
|
83
|
+
|
|
84
|
+
complexity_reducer: `## Complexity Reducer Mode
|
|
85
|
+
|
|
86
|
+
Your focus: Simplify and refactor complex code.
|
|
87
|
+
|
|
88
|
+
Allowed actions:
|
|
89
|
+
- Extract functions to reduce nesting
|
|
90
|
+
- Split large functions into smaller ones
|
|
91
|
+
- Simplify conditional logic
|
|
92
|
+
- Remove duplicate code
|
|
93
|
+
- Improve variable naming for clarity
|
|
94
|
+
|
|
95
|
+
Avoid:
|
|
96
|
+
- Changing functionality (keep behavior identical)
|
|
97
|
+
- Fixing bugs (use error_reducer for that)
|
|
98
|
+
- Removing tests or validation`,
|
|
99
|
+
|
|
100
|
+
debt_payer: `## Debt Payer Mode
|
|
101
|
+
|
|
102
|
+
Your focus: Fix lint issues and improve code quality.
|
|
103
|
+
|
|
104
|
+
Allowed actions:
|
|
105
|
+
- Fix ESLint errors and warnings
|
|
106
|
+
- Add missing type annotations
|
|
107
|
+
- Fix naming conventions
|
|
108
|
+
- Remove unused imports and variables
|
|
109
|
+
- Improve code formatting
|
|
110
|
+
|
|
111
|
+
Avoid:
|
|
112
|
+
- Large refactors (use complexity_reducer for that)
|
|
113
|
+
- Fixing logic bugs (use error_reducer for that)
|
|
114
|
+
- Major functionality changes`,
|
|
115
|
+
|
|
116
|
+
stabilizer: `## Stabilizer Mode
|
|
117
|
+
|
|
118
|
+
Your focus: Reduce churn by improving stability.
|
|
119
|
+
|
|
120
|
+
Allowed actions:
|
|
121
|
+
- Add unit tests for untested code
|
|
122
|
+
- Improve API interfaces to reduce breaking changes
|
|
123
|
+
- Add documentation and comments
|
|
124
|
+
- Extract constants from magic numbers
|
|
125
|
+
- Improve encapsulation
|
|
126
|
+
|
|
127
|
+
Avoid:
|
|
128
|
+
- Large refactors
|
|
129
|
+
- Changing public APIs without good reason
|
|
130
|
+
- Removing tests`,
|
|
131
|
+
|
|
132
|
+
explorer: `## Explorer Mode
|
|
133
|
+
|
|
134
|
+
Your focus: Challenge assumptions and find better approaches.
|
|
135
|
+
|
|
136
|
+
You have more freedom than other modes, but still bounded:
|
|
137
|
+
- Can modify up to 50 lines
|
|
138
|
+
- Must stay in one file
|
|
139
|
+
- Cannot delete tests
|
|
140
|
+
- Cannot remove validation
|
|
141
|
+
- Cannot modify public APIs
|
|
142
|
+
- Must pass CI
|
|
143
|
+
|
|
144
|
+
This mode is for trying novel approaches when conventional modes have failed.`,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return instructions[mode] || instructions.debt_payer;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get mode description for UI/logging
|
|
152
|
+
*/
|
|
153
|
+
export function getModeDescription(mode: Mode): string {
|
|
154
|
+
const descriptions: Record<Mode, string> = {
|
|
155
|
+
error_reducer: 'Fix bugs and add error handling',
|
|
156
|
+
complexity_reducer: 'Simplify and refactor code',
|
|
157
|
+
debt_payer: 'Fix lint issues and improve types',
|
|
158
|
+
stabilizer: 'Add tests and reduce churn',
|
|
159
|
+
explorer: 'Try novel approaches (bounded)',
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return descriptions[mode] || 'Unknown mode';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if a mode is allowed for a file
|
|
167
|
+
* Some modes may be restricted based on file state
|
|
168
|
+
*/
|
|
169
|
+
export function isModeAllowed(
|
|
170
|
+
mode: Mode,
|
|
171
|
+
filePath: string,
|
|
172
|
+
isQuarantined: boolean = false
|
|
173
|
+
): boolean {
|
|
174
|
+
// Only explorer can work on quarantined files
|
|
175
|
+
if (isQuarantined && mode !== 'explorer') {
|
|
176
|
+
logWarn('Mode not allowed on quarantined file', {
|
|
177
|
+
file: filePath,
|
|
178
|
+
mode,
|
|
179
|
+
reason: 'file is quarantined',
|
|
180
|
+
});
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// All modes allowed for normal files
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Suggest modes based on signal values
|
|
190
|
+
* Returns ranked list of modes that might be helpful
|
|
191
|
+
*/
|
|
192
|
+
export function suggestModes(gradient: GradientScore): Array<{
|
|
193
|
+
mode: Mode;
|
|
194
|
+
reason: string;
|
|
195
|
+
priority: number;
|
|
196
|
+
}> {
|
|
197
|
+
const suggestions: Array<{ mode: Mode; reason: string; priority: number }> = [];
|
|
198
|
+
const { signals } = gradient;
|
|
199
|
+
|
|
200
|
+
// Check each signal
|
|
201
|
+
if (signals.error_rate >= DOMINANCE_THRESHOLD) {
|
|
202
|
+
suggestions.push({
|
|
203
|
+
mode: 'error_reducer',
|
|
204
|
+
reason: `High error rate: ${signals.error_rate.toFixed(2)}`,
|
|
205
|
+
priority: signals.error_rate,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (signals.complexity >= DOMINANCE_THRESHOLD) {
|
|
210
|
+
suggestions.push({
|
|
211
|
+
mode: 'complexity_reducer',
|
|
212
|
+
reason: `High complexity: ${signals.complexity.toFixed(2)}`,
|
|
213
|
+
priority: signals.complexity,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (signals.debt >= DOMINANCE_THRESHOLD) {
|
|
218
|
+
suggestions.push({
|
|
219
|
+
mode: 'debt_payer',
|
|
220
|
+
reason: `High technical debt: ${signals.debt.toFixed(2)}`,
|
|
221
|
+
priority: signals.debt,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (signals.churn >= DOMINANCE_THRESHOLD) {
|
|
226
|
+
suggestions.push({
|
|
227
|
+
mode: 'stabilizer',
|
|
228
|
+
reason: `High churn: ${signals.churn.toFixed(2)}`,
|
|
229
|
+
priority: signals.churn,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Sort by priority (highest signal first)
|
|
234
|
+
suggestions.sort((a, b) => b.priority - a.priority);
|
|
235
|
+
|
|
236
|
+
logDebug('Mode suggestions', {
|
|
237
|
+
file: gradient.file,
|
|
238
|
+
count: suggestions.length,
|
|
239
|
+
top: suggestions[0]?.mode || 'none',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return suggestions;
|
|
243
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrality Signal Computation
|
|
3
|
+
* Per ADR-001 and second-spec §2.3 (lines 504-620)
|
|
4
|
+
*
|
|
5
|
+
* Calculates file centrality by building an import graph:
|
|
6
|
+
* - Extracts imports using regex patterns
|
|
7
|
+
* - Builds bidirectional import graph
|
|
8
|
+
* - Calculates fan-in (files that import this file)
|
|
9
|
+
* - Calculates fan-out (files this file imports)
|
|
10
|
+
* - Normalizes: (fanIn/maxFanIn)*0.67 + (fanOut/maxFanOut)*0.33
|
|
11
|
+
*
|
|
12
|
+
* Fan-in weighted 2x because heavily depended-on files are more critical
|
|
13
|
+
* Graph cached for 5 minutes to avoid repeated file scanning
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { readFile, listFilesRecursive, isTestFile } from '../../utils/file-utils.js';
|
|
19
|
+
import { logDebug, logWarn } from '../../utils/logger.js';
|
|
20
|
+
|
|
21
|
+
export interface CentralityResult {
|
|
22
|
+
fanIn: number;
|
|
23
|
+
fanOut: number;
|
|
24
|
+
normalized: number; // 0.0 - 1.0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ImportGraph {
|
|
28
|
+
imports: Record<string, string[]>;
|
|
29
|
+
importedBy: Record<string, string[]>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface CachedGraph {
|
|
33
|
+
graph: ImportGraph;
|
|
34
|
+
computedAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Cache graph for 5 minutes per spec
|
|
38
|
+
const GRAPH_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
39
|
+
let cachedGraph: CachedGraph | null = null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calculate centrality for a single file
|
|
43
|
+
* Builds import graph if needed (with caching)
|
|
44
|
+
*/
|
|
45
|
+
export async function calculateCentrality(filePath: string): Promise<CentralityResult> {
|
|
46
|
+
try {
|
|
47
|
+
// Determine root directory (go up until we find package.json or reach filesystem root)
|
|
48
|
+
const rootDir = findProjectRoot(filePath);
|
|
49
|
+
|
|
50
|
+
// Build or retrieve cached graph
|
|
51
|
+
const graph = await getOrBuildImportGraph(rootDir);
|
|
52
|
+
|
|
53
|
+
// Make file path relative to root for lookup
|
|
54
|
+
const relativeFile = path.relative(rootDir, filePath);
|
|
55
|
+
|
|
56
|
+
// Calculate fan-in and fan-out
|
|
57
|
+
const fanIn = (graph.importedBy[relativeFile] || []).length;
|
|
58
|
+
const fanOut = (graph.imports[relativeFile] || []).length;
|
|
59
|
+
|
|
60
|
+
// Find max values for normalization
|
|
61
|
+
let maxFanIn = 1;
|
|
62
|
+
let maxFanOut = 1;
|
|
63
|
+
|
|
64
|
+
for (const file of Object.keys(graph.imports)) {
|
|
65
|
+
const fileImportedBy = (graph.importedBy[file] || []).length;
|
|
66
|
+
const fileImports = (graph.imports[file] || []).length;
|
|
67
|
+
|
|
68
|
+
maxFanIn = Math.max(maxFanIn, fileImportedBy);
|
|
69
|
+
maxFanOut = Math.max(maxFanOut, fileImports);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Normalize: fan-in weighted 2x (67% vs 33%)
|
|
73
|
+
const normalizedFanIn = fanIn / maxFanIn;
|
|
74
|
+
const normalizedFanOut = fanOut / maxFanOut;
|
|
75
|
+
const normalized = normalizedFanIn * 0.67 + normalizedFanOut * 0.33;
|
|
76
|
+
|
|
77
|
+
logDebug('Calculated centrality', {
|
|
78
|
+
file: relativeFile,
|
|
79
|
+
fanIn,
|
|
80
|
+
fanOut,
|
|
81
|
+
maxFanIn,
|
|
82
|
+
maxFanOut,
|
|
83
|
+
normalized: normalized.toFixed(3),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
fanIn,
|
|
88
|
+
fanOut,
|
|
89
|
+
normalized,
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
logWarn('Failed to calculate centrality, returning 0', {
|
|
93
|
+
file: filePath,
|
|
94
|
+
error: error instanceof Error ? error.message : String(error),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
fanIn: 0,
|
|
99
|
+
fanOut: 0,
|
|
100
|
+
normalized: 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build import graph for entire project
|
|
107
|
+
* Public for testing and manual use
|
|
108
|
+
*/
|
|
109
|
+
export async function buildImportGraph(rootDir: string): Promise<ImportGraph> {
|
|
110
|
+
const imports: Record<string, string[]> = {};
|
|
111
|
+
const importedBy: Record<string, string[]> = {};
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Find all TypeScript/JavaScript files
|
|
115
|
+
const allFiles = listFilesRecursive(rootDir);
|
|
116
|
+
const sourceFiles = allFiles.filter(file => {
|
|
117
|
+
// Include .ts, .tsx, .js, .jsx
|
|
118
|
+
const isSourceFile = /\.(ts|tsx|js|jsx)$/.test(file);
|
|
119
|
+
// Exclude node_modules, test files, and .d.ts files
|
|
120
|
+
const isExcluded = file.includes('node_modules') ||
|
|
121
|
+
isTestFile(file) ||
|
|
122
|
+
file.endsWith('.d.ts');
|
|
123
|
+
return isSourceFile && !isExcluded;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
logDebug('Building import graph', {
|
|
127
|
+
rootDir,
|
|
128
|
+
totalFiles: sourceFiles.length,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Extract imports for each file
|
|
132
|
+
for (const filePath of sourceFiles) {
|
|
133
|
+
const relativeFile = path.relative(rootDir, filePath);
|
|
134
|
+
const fileImports = extractImports(filePath, rootDir);
|
|
135
|
+
|
|
136
|
+
imports[relativeFile] = fileImports;
|
|
137
|
+
|
|
138
|
+
// Build reverse index (importedBy)
|
|
139
|
+
for (const importedFile of fileImports) {
|
|
140
|
+
if (!importedBy[importedFile]) {
|
|
141
|
+
importedBy[importedFile] = [];
|
|
142
|
+
}
|
|
143
|
+
importedBy[importedFile].push(relativeFile);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
logDebug('Import graph built', {
|
|
148
|
+
totalFiles: Object.keys(imports).length,
|
|
149
|
+
totalEdges: Object.values(imports).reduce((sum, arr) => sum + arr.length, 0),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return { imports, importedBy };
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logWarn('Failed to build import graph, returning empty', {
|
|
155
|
+
rootDir,
|
|
156
|
+
error: error instanceof Error ? error.message : String(error),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return { imports, importedBy };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extract import paths from a file using regex
|
|
165
|
+
* Matches:
|
|
166
|
+
* - import ... from '...'
|
|
167
|
+
* - import '...'
|
|
168
|
+
* - require('...')
|
|
169
|
+
* - import('...')
|
|
170
|
+
*/
|
|
171
|
+
function extractImports(filePath: string, rootDir: string): string[] {
|
|
172
|
+
try {
|
|
173
|
+
const content = readFile(filePath);
|
|
174
|
+
const imports: Set<string> = new Set();
|
|
175
|
+
|
|
176
|
+
// Comprehensive regex for all import styles
|
|
177
|
+
// Matches: import ... from '...', import '...', require('...'), import('...')
|
|
178
|
+
const importRegex = /(?:import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\)|import\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
|
|
179
|
+
|
|
180
|
+
let match;
|
|
181
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
182
|
+
const importPath = match[1] || match[2] || match[3];
|
|
183
|
+
|
|
184
|
+
// Skip external packages (not relative or absolute paths)
|
|
185
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Resolve to actual file path
|
|
190
|
+
const resolved = resolveImportPath(importPath, filePath, rootDir);
|
|
191
|
+
if (resolved) {
|
|
192
|
+
imports.add(resolved);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Array.from(imports);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logWarn('Failed to extract imports', {
|
|
199
|
+
file: filePath,
|
|
200
|
+
error: error instanceof Error ? error.message : String(error),
|
|
201
|
+
});
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resolve import path to actual file
|
|
208
|
+
* Tries extensions: .ts, .tsx, .js, .jsx, /index.{ext}
|
|
209
|
+
*/
|
|
210
|
+
function resolveImportPath(importPath: string, fromFile: string, rootDir: string): string | null {
|
|
211
|
+
const fromDir = path.dirname(fromFile);
|
|
212
|
+
|
|
213
|
+
// Resolve relative to importing file
|
|
214
|
+
let resolved = path.resolve(fromDir, importPath);
|
|
215
|
+
|
|
216
|
+
// Make relative to root directory
|
|
217
|
+
resolved = path.relative(rootDir, resolved);
|
|
218
|
+
|
|
219
|
+
// Try different extensions and index files
|
|
220
|
+
const extensions = [
|
|
221
|
+
'', // Exact match
|
|
222
|
+
'.ts',
|
|
223
|
+
'.tsx',
|
|
224
|
+
'.js',
|
|
225
|
+
'.jsx',
|
|
226
|
+
'/index.ts',
|
|
227
|
+
'/index.tsx',
|
|
228
|
+
'/index.js',
|
|
229
|
+
'/index.jsx',
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
for (const ext of extensions) {
|
|
233
|
+
const candidate = resolved + ext;
|
|
234
|
+
const candidatePath = path.join(rootDir, candidate);
|
|
235
|
+
|
|
236
|
+
if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isFile()) {
|
|
237
|
+
return candidate;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Could not resolve
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Find project root by looking for package.json
|
|
247
|
+
*/
|
|
248
|
+
function findProjectRoot(startPath: string): string {
|
|
249
|
+
let currentDir = path.dirname(startPath);
|
|
250
|
+
|
|
251
|
+
// Walk up until we find package.json or reach root
|
|
252
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
253
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
254
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
255
|
+
return currentDir;
|
|
256
|
+
}
|
|
257
|
+
currentDir = path.dirname(currentDir);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Fallback to starting directory
|
|
261
|
+
return path.dirname(startPath);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get cached graph or build new one
|
|
266
|
+
*/
|
|
267
|
+
async function getOrBuildImportGraph(rootDir: string): Promise<ImportGraph> {
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
|
|
270
|
+
// Check if cache is valid
|
|
271
|
+
if (cachedGraph && (now - cachedGraph.computedAt) < GRAPH_CACHE_TTL_MS) {
|
|
272
|
+
logDebug('Using cached import graph', {
|
|
273
|
+
age: Math.round((now - cachedGraph.computedAt) / 1000) + 's',
|
|
274
|
+
});
|
|
275
|
+
return cachedGraph.graph;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Build new graph
|
|
279
|
+
logDebug('Cache expired or missing, building new import graph');
|
|
280
|
+
const graph = await buildImportGraph(rootDir);
|
|
281
|
+
|
|
282
|
+
// Update cache
|
|
283
|
+
cachedGraph = {
|
|
284
|
+
graph,
|
|
285
|
+
computedAt: now,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return graph;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Clear the import graph cache
|
|
293
|
+
* Useful for testing or when files change
|
|
294
|
+
*/
|
|
295
|
+
export function clearImportGraphCache(): void {
|
|
296
|
+
cachedGraph = null;
|
|
297
|
+
logDebug('Import graph cache cleared');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Batch calculate centrality for multiple files
|
|
302
|
+
* More efficient than individual calls when analyzing many files
|
|
303
|
+
*/
|
|
304
|
+
export async function calculateCentralityBatch(
|
|
305
|
+
filePaths: string[]
|
|
306
|
+
): Promise<Map<string, CentralityResult>> {
|
|
307
|
+
const results = new Map<string, CentralityResult>();
|
|
308
|
+
|
|
309
|
+
// Find common root
|
|
310
|
+
if (filePaths.length === 0) {
|
|
311
|
+
return results;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const rootDir = findProjectRoot(filePaths[0]);
|
|
315
|
+
|
|
316
|
+
// Build graph once (ensures cache is populated)
|
|
317
|
+
const graph = await getOrBuildImportGraph(rootDir);
|
|
318
|
+
// Graph is used to populate cache for subsequent calculateCentrality calls
|
|
319
|
+
void graph;
|
|
320
|
+
|
|
321
|
+
// Calculate for each file
|
|
322
|
+
for (const filePath of filePaths) {
|
|
323
|
+
const result = await calculateCentrality(filePath);
|
|
324
|
+
results.set(filePath, result);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return results;
|
|
328
|
+
}
|