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,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
+ }