codeguardian-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +348 -0
  3. package/dist/agent/agentTools.d.ts +26 -0
  4. package/dist/agent/agentTools.d.ts.map +1 -0
  5. package/dist/agent/agentTools.js +699 -0
  6. package/dist/agent/agentTools.js.map +1 -0
  7. package/dist/agent/autoValidator.d.ts +110 -0
  8. package/dist/agent/autoValidator.d.ts.map +1 -0
  9. package/dist/agent/autoValidator.js +964 -0
  10. package/dist/agent/autoValidator.js.map +1 -0
  11. package/dist/agent/fileWatcher.d.ts +28 -0
  12. package/dist/agent/fileWatcher.d.ts.map +1 -0
  13. package/dist/agent/fileWatcher.js +88 -0
  14. package/dist/agent/fileWatcher.js.map +1 -0
  15. package/dist/agent/guardianPersistence.d.ts +98 -0
  16. package/dist/agent/guardianPersistence.d.ts.map +1 -0
  17. package/dist/agent/guardianPersistence.js +296 -0
  18. package/dist/agent/guardianPersistence.js.map +1 -0
  19. package/dist/agent/mcpNotifications.d.ts +38 -0
  20. package/dist/agent/mcpNotifications.d.ts.map +1 -0
  21. package/dist/agent/mcpNotifications.js +81 -0
  22. package/dist/agent/mcpNotifications.js.map +1 -0
  23. package/dist/analyzers/aiPatterns.d.ts +16 -0
  24. package/dist/analyzers/aiPatterns.d.ts.map +1 -0
  25. package/dist/analyzers/aiPatterns.js +103 -0
  26. package/dist/analyzers/aiPatterns.js.map +1 -0
  27. package/dist/analyzers/antiPatterns.d.ts +60 -0
  28. package/dist/analyzers/antiPatterns.d.ts.map +1 -0
  29. package/dist/analyzers/antiPatterns.js +198 -0
  30. package/dist/analyzers/antiPatterns.js.map +1 -0
  31. package/dist/analyzers/builtinTypes.d.ts +18 -0
  32. package/dist/analyzers/builtinTypes.d.ts.map +1 -0
  33. package/dist/analyzers/builtinTypes.js +1275 -0
  34. package/dist/analyzers/builtinTypes.js.map +1 -0
  35. package/dist/analyzers/complexity.d.ts +14 -0
  36. package/dist/analyzers/complexity.d.ts.map +1 -0
  37. package/dist/analyzers/complexity.js +610 -0
  38. package/dist/analyzers/complexity.js.map +1 -0
  39. package/dist/analyzers/findingVerifier.d.ts +59 -0
  40. package/dist/analyzers/findingVerifier.d.ts.map +1 -0
  41. package/dist/analyzers/findingVerifier.js +1169 -0
  42. package/dist/analyzers/findingVerifier.js.map +1 -0
  43. package/dist/analyzers/impactAnalyzer.d.ts +53 -0
  44. package/dist/analyzers/impactAnalyzer.d.ts.map +1 -0
  45. package/dist/analyzers/impactAnalyzer.js +152 -0
  46. package/dist/analyzers/impactAnalyzer.js.map +1 -0
  47. package/dist/analyzers/languageDetector.d.ts +48 -0
  48. package/dist/analyzers/languageDetector.d.ts.map +1 -0
  49. package/dist/analyzers/languageDetector.js +404 -0
  50. package/dist/analyzers/languageDetector.js.map +1 -0
  51. package/dist/analyzers/parsers/incrementalParser.d.ts +53 -0
  52. package/dist/analyzers/parsers/incrementalParser.d.ts.map +1 -0
  53. package/dist/analyzers/parsers/incrementalParser.js +193 -0
  54. package/dist/analyzers/parsers/incrementalParser.js.map +1 -0
  55. package/dist/analyzers/parsers/scopeResolver.d.ts +92 -0
  56. package/dist/analyzers/parsers/scopeResolver.d.ts.map +1 -0
  57. package/dist/analyzers/parsers/scopeResolver.js +324 -0
  58. package/dist/analyzers/parsers/scopeResolver.js.map +1 -0
  59. package/dist/analyzers/parsers/semanticIndex.d.ts +127 -0
  60. package/dist/analyzers/parsers/semanticIndex.d.ts.map +1 -0
  61. package/dist/analyzers/parsers/semanticIndex.js +429 -0
  62. package/dist/analyzers/parsers/semanticIndex.js.map +1 -0
  63. package/dist/analyzers/parsers/sessionDiffAnalyzer.d.ts +42 -0
  64. package/dist/analyzers/parsers/sessionDiffAnalyzer.d.ts.map +1 -0
  65. package/dist/analyzers/parsers/sessionDiffAnalyzer.js +233 -0
  66. package/dist/analyzers/parsers/sessionDiffAnalyzer.js.map +1 -0
  67. package/dist/analyzers/parsers/treeSitterParser.d.ts +76 -0
  68. package/dist/analyzers/parsers/treeSitterParser.d.ts.map +1 -0
  69. package/dist/analyzers/parsers/treeSitterParser.js +709 -0
  70. package/dist/analyzers/parsers/treeSitterParser.js.map +1 -0
  71. package/dist/analyzers/relevanceScorer.d.ts +43 -0
  72. package/dist/analyzers/relevanceScorer.d.ts.map +1 -0
  73. package/dist/analyzers/relevanceScorer.js +200 -0
  74. package/dist/analyzers/relevanceScorer.js.map +1 -0
  75. package/dist/analyzers/standardLibrary.d.ts +22 -0
  76. package/dist/analyzers/standardLibrary.d.ts.map +1 -0
  77. package/dist/analyzers/standardLibrary.js +211 -0
  78. package/dist/analyzers/standardLibrary.js.map +1 -0
  79. package/dist/analyzers/symbolGraph.d.ts +30 -0
  80. package/dist/analyzers/symbolGraph.d.ts.map +1 -0
  81. package/dist/analyzers/symbolGraph.js +380 -0
  82. package/dist/analyzers/symbolGraph.js.map +1 -0
  83. package/dist/analyzers/symbolTable.d.ts +18 -0
  84. package/dist/analyzers/symbolTable.d.ts.map +1 -0
  85. package/dist/analyzers/symbolTable.js +176 -0
  86. package/dist/analyzers/symbolTable.js.map +1 -0
  87. package/dist/analyzers/typeChecker.d.ts +13 -0
  88. package/dist/analyzers/typeChecker.d.ts.map +1 -0
  89. package/dist/analyzers/typeChecker.js +580 -0
  90. package/dist/analyzers/typeChecker.js.map +1 -0
  91. package/dist/analyzers/usagePatterns.d.ts +42 -0
  92. package/dist/analyzers/usagePatterns.d.ts.map +1 -0
  93. package/dist/analyzers/usagePatterns.js +75 -0
  94. package/dist/analyzers/usagePatterns.js.map +1 -0
  95. package/dist/api-contract/context/backend.d.ts +19 -0
  96. package/dist/api-contract/context/backend.d.ts.map +1 -0
  97. package/dist/api-contract/context/backend.js +64 -0
  98. package/dist/api-contract/context/backend.js.map +1 -0
  99. package/dist/api-contract/context/contract.d.ts +34 -0
  100. package/dist/api-contract/context/contract.d.ts.map +1 -0
  101. package/dist/api-contract/context/contract.js +306 -0
  102. package/dist/api-contract/context/contract.js.map +1 -0
  103. package/dist/api-contract/context/frontend.d.ts +19 -0
  104. package/dist/api-contract/context/frontend.d.ts.map +1 -0
  105. package/dist/api-contract/context/frontend.js +64 -0
  106. package/dist/api-contract/context/frontend.js.map +1 -0
  107. package/dist/api-contract/detector.d.ts +28 -0
  108. package/dist/api-contract/detector.d.ts.map +1 -0
  109. package/dist/api-contract/detector.js +393 -0
  110. package/dist/api-contract/detector.js.map +1 -0
  111. package/dist/api-contract/extractors/python.d.ts +32 -0
  112. package/dist/api-contract/extractors/python.d.ts.map +1 -0
  113. package/dist/api-contract/extractors/python.js +521 -0
  114. package/dist/api-contract/extractors/python.js.map +1 -0
  115. package/dist/api-contract/extractors/pythonAstUtils.d.ts +44 -0
  116. package/dist/api-contract/extractors/pythonAstUtils.d.ts.map +1 -0
  117. package/dist/api-contract/extractors/pythonAstUtils.js +489 -0
  118. package/dist/api-contract/extractors/pythonAstUtils.js.map +1 -0
  119. package/dist/api-contract/extractors/tsAstUtils.d.ts +47 -0
  120. package/dist/api-contract/extractors/tsAstUtils.d.ts.map +1 -0
  121. package/dist/api-contract/extractors/tsAstUtils.js +173 -0
  122. package/dist/api-contract/extractors/tsAstUtils.js.map +1 -0
  123. package/dist/api-contract/extractors/typescript.d.ts +32 -0
  124. package/dist/api-contract/extractors/typescript.d.ts.map +1 -0
  125. package/dist/api-contract/extractors/typescript.js +666 -0
  126. package/dist/api-contract/extractors/typescript.js.map +1 -0
  127. package/dist/api-contract/index.d.ts +104 -0
  128. package/dist/api-contract/index.d.ts.map +1 -0
  129. package/dist/api-contract/index.js +232 -0
  130. package/dist/api-contract/index.js.map +1 -0
  131. package/dist/api-contract/types.d.ts +151 -0
  132. package/dist/api-contract/types.d.ts.map +1 -0
  133. package/dist/api-contract/types.js +19 -0
  134. package/dist/api-contract/types.js.map +1 -0
  135. package/dist/api-contract/validators/endpoint.d.ts +21 -0
  136. package/dist/api-contract/validators/endpoint.d.ts.map +1 -0
  137. package/dist/api-contract/validators/endpoint.js +224 -0
  138. package/dist/api-contract/validators/endpoint.js.map +1 -0
  139. package/dist/api-contract/validators/index.d.ts +40 -0
  140. package/dist/api-contract/validators/index.d.ts.map +1 -0
  141. package/dist/api-contract/validators/index.js +875 -0
  142. package/dist/api-contract/validators/index.js.map +1 -0
  143. package/dist/api-contract/validators/parameter.d.ts +17 -0
  144. package/dist/api-contract/validators/parameter.d.ts.map +1 -0
  145. package/dist/api-contract/validators/parameter.js +250 -0
  146. package/dist/api-contract/validators/parameter.js.map +1 -0
  147. package/dist/api-contract/validators/type.d.ts +38 -0
  148. package/dist/api-contract/validators/type.d.ts.map +1 -0
  149. package/dist/api-contract/validators/type.js +244 -0
  150. package/dist/api-contract/validators/type.js.map +1 -0
  151. package/dist/context/apiContract/complexTypeSupport.d.ts +83 -0
  152. package/dist/context/apiContract/complexTypeSupport.d.ts.map +1 -0
  153. package/dist/context/apiContract/complexTypeSupport.js +665 -0
  154. package/dist/context/apiContract/complexTypeSupport.js.map +1 -0
  155. package/dist/context/apiContract/graphqlSupport.d.ts +105 -0
  156. package/dist/context/apiContract/graphqlSupport.d.ts.map +1 -0
  157. package/dist/context/apiContract/graphqlSupport.js +671 -0
  158. package/dist/context/apiContract/graphqlSupport.js.map +1 -0
  159. package/dist/context/apiContract/index.d.ts +14 -0
  160. package/dist/context/apiContract/index.d.ts.map +1 -0
  161. package/dist/context/apiContract/index.js +17 -0
  162. package/dist/context/apiContract/index.js.map +1 -0
  163. package/dist/context/apiContract/webSocketSupport.d.ts +104 -0
  164. package/dist/context/apiContract/webSocketSupport.d.ts.map +1 -0
  165. package/dist/context/apiContract/webSocketSupport.js +465 -0
  166. package/dist/context/apiContract/webSocketSupport.js.map +1 -0
  167. package/dist/context/apiContractContext.d.ts +15 -0
  168. package/dist/context/apiContractContext.d.ts.map +1 -0
  169. package/dist/context/apiContractContext.js +979 -0
  170. package/dist/context/apiContractContext.js.map +1 -0
  171. package/dist/context/apiContractExtraction.d.ts +52 -0
  172. package/dist/context/apiContractExtraction.d.ts.map +1 -0
  173. package/dist/context/apiContractExtraction.js +438 -0
  174. package/dist/context/apiContractExtraction.js.map +1 -0
  175. package/dist/context/contextLineage.d.ts +79 -0
  176. package/dist/context/contextLineage.d.ts.map +1 -0
  177. package/dist/context/contextLineage.js +259 -0
  178. package/dist/context/contextLineage.js.map +1 -0
  179. package/dist/context/contextOrchestrator.d.ts +57 -0
  180. package/dist/context/contextOrchestrator.d.ts.map +1 -0
  181. package/dist/context/contextOrchestrator.js +162 -0
  182. package/dist/context/contextOrchestrator.js.map +1 -0
  183. package/dist/context/intentTracker.d.ts +73 -0
  184. package/dist/context/intentTracker.d.ts.map +1 -0
  185. package/dist/context/intentTracker.js +168 -0
  186. package/dist/context/intentTracker.js.map +1 -0
  187. package/dist/context/projectContext.d.ts +219 -0
  188. package/dist/context/projectContext.d.ts.map +1 -0
  189. package/dist/context/projectContext.js +1984 -0
  190. package/dist/context/projectContext.js.map +1 -0
  191. package/dist/prompts/index.d.ts +17 -0
  192. package/dist/prompts/index.d.ts.map +1 -0
  193. package/dist/prompts/index.js +260 -0
  194. package/dist/prompts/index.js.map +1 -0
  195. package/dist/prompts/library.d.ts +51 -0
  196. package/dist/prompts/library.d.ts.map +1 -0
  197. package/dist/prompts/library.js +65 -0
  198. package/dist/prompts/library.js.map +1 -0
  199. package/dist/prompts/templates.d.ts +44 -0
  200. package/dist/prompts/templates.d.ts.map +1 -0
  201. package/dist/prompts/templates.js +97 -0
  202. package/dist/prompts/templates.js.map +1 -0
  203. package/dist/queue/jobPersistence.d.ts +46 -0
  204. package/dist/queue/jobPersistence.d.ts.map +1 -0
  205. package/dist/queue/jobPersistence.js +158 -0
  206. package/dist/queue/jobPersistence.js.map +1 -0
  207. package/dist/queue/jobQueue.d.ts +116 -0
  208. package/dist/queue/jobQueue.d.ts.map +1 -0
  209. package/dist/queue/jobQueue.js +275 -0
  210. package/dist/queue/jobQueue.js.map +1 -0
  211. package/dist/queue/validationJob.d.ts +69 -0
  212. package/dist/queue/validationJob.d.ts.map +1 -0
  213. package/dist/queue/validationJob.js +435 -0
  214. package/dist/queue/validationJob.js.map +1 -0
  215. package/dist/resources/index.d.ts +15 -0
  216. package/dist/resources/index.d.ts.map +1 -0
  217. package/dist/resources/index.js +328 -0
  218. package/dist/resources/index.js.map +1 -0
  219. package/dist/resources/validationReportStore.d.ts +170 -0
  220. package/dist/resources/validationReportStore.d.ts.map +1 -0
  221. package/dist/resources/validationReportStore.js +515 -0
  222. package/dist/resources/validationReportStore.js.map +1 -0
  223. package/dist/server.d.ts +12 -0
  224. package/dist/server.d.ts.map +1 -0
  225. package/dist/server.js +102 -0
  226. package/dist/server.js.map +1 -0
  227. package/dist/tools/asyncValidation.d.ts +19 -0
  228. package/dist/tools/asyncValidation.d.ts.map +1 -0
  229. package/dist/tools/asyncValidation.js +346 -0
  230. package/dist/tools/asyncValidation.js.map +1 -0
  231. package/dist/tools/buildContext.d.ts +17 -0
  232. package/dist/tools/buildContext.d.ts.map +1 -0
  233. package/dist/tools/buildContext.js +188 -0
  234. package/dist/tools/buildContext.js.map +1 -0
  235. package/dist/tools/getDependencyGraph.d.ts +16 -0
  236. package/dist/tools/getDependencyGraph.d.ts.map +1 -0
  237. package/dist/tools/getDependencyGraph.js +436 -0
  238. package/dist/tools/getDependencyGraph.js.map +1 -0
  239. package/dist/tools/incrementalValidation.d.ts +71 -0
  240. package/dist/tools/incrementalValidation.d.ts.map +1 -0
  241. package/dist/tools/incrementalValidation.js +203 -0
  242. package/dist/tools/incrementalValidation.js.map +1 -0
  243. package/dist/tools/index.d.ts +24 -0
  244. package/dist/tools/index.d.ts.map +1 -0
  245. package/dist/tools/index.js +106 -0
  246. package/dist/tools/index.js.map +1 -0
  247. package/dist/tools/validateCode.d.ts +17 -0
  248. package/dist/tools/validateCode.d.ts.map +1 -0
  249. package/dist/tools/validateCode.js +368 -0
  250. package/dist/tools/validateCode.js.map +1 -0
  251. package/dist/tools/validateCodeLite.d.ts +2 -0
  252. package/dist/tools/validateCodeLite.d.ts.map +1 -0
  253. package/dist/tools/validateCodeLite.js +2 -0
  254. package/dist/tools/validateCodeLite.js.map +1 -0
  255. package/dist/tools/validation/builtins.d.ts +92 -0
  256. package/dist/tools/validation/builtins.d.ts.map +1 -0
  257. package/dist/tools/validation/builtins.js +2184 -0
  258. package/dist/tools/validation/builtins.js.map +1 -0
  259. package/dist/tools/validation/contextualNaming.d.ts +99 -0
  260. package/dist/tools/validation/contextualNaming.d.ts.map +1 -0
  261. package/dist/tools/validation/contextualNaming.js +959 -0
  262. package/dist/tools/validation/contextualNaming.js.map +1 -0
  263. package/dist/tools/validation/deadCode.d.ts +115 -0
  264. package/dist/tools/validation/deadCode.d.ts.map +1 -0
  265. package/dist/tools/validation/deadCode.js +861 -0
  266. package/dist/tools/validation/deadCode.js.map +1 -0
  267. package/dist/tools/validation/extractors/index.d.ts +131 -0
  268. package/dist/tools/validation/extractors/index.d.ts.map +1 -0
  269. package/dist/tools/validation/extractors/index.js +233 -0
  270. package/dist/tools/validation/extractors/index.js.map +1 -0
  271. package/dist/tools/validation/extractors/javascript.d.ts +73 -0
  272. package/dist/tools/validation/extractors/javascript.d.ts.map +1 -0
  273. package/dist/tools/validation/extractors/javascript.js +1841 -0
  274. package/dist/tools/validation/extractors/javascript.js.map +1 -0
  275. package/dist/tools/validation/extractors/python.d.ts +93 -0
  276. package/dist/tools/validation/extractors/python.d.ts.map +1 -0
  277. package/dist/tools/validation/extractors/python.js +799 -0
  278. package/dist/tools/validation/extractors/python.js.map +1 -0
  279. package/dist/tools/validation/manifest.d.ts +45 -0
  280. package/dist/tools/validation/manifest.d.ts.map +1 -0
  281. package/dist/tools/validation/manifest.js +719 -0
  282. package/dist/tools/validation/manifest.js.map +1 -0
  283. package/dist/tools/validation/parser.d.ts +58 -0
  284. package/dist/tools/validation/parser.d.ts.map +1 -0
  285. package/dist/tools/validation/parser.js +232 -0
  286. package/dist/tools/validation/parser.js.map +1 -0
  287. package/dist/tools/validation/registry.d.ts +15 -0
  288. package/dist/tools/validation/registry.d.ts.map +1 -0
  289. package/dist/tools/validation/registry.js +169 -0
  290. package/dist/tools/validation/registry.js.map +1 -0
  291. package/dist/tools/validation/scoring.d.ts +54 -0
  292. package/dist/tools/validation/scoring.d.ts.map +1 -0
  293. package/dist/tools/validation/scoring.js +242 -0
  294. package/dist/tools/validation/scoring.js.map +1 -0
  295. package/dist/tools/validation/types.d.ts +120 -0
  296. package/dist/tools/validation/types.d.ts.map +1 -0
  297. package/dist/tools/validation/types.js +11 -0
  298. package/dist/tools/validation/types.js.map +1 -0
  299. package/dist/tools/validation/unusedLocals.d.ts +36 -0
  300. package/dist/tools/validation/unusedLocals.d.ts.map +1 -0
  301. package/dist/tools/validation/unusedLocals.js +333 -0
  302. package/dist/tools/validation/unusedLocals.js.map +1 -0
  303. package/dist/tools/validation/validation.d.ts +98 -0
  304. package/dist/tools/validation/validation.d.ts.map +1 -0
  305. package/dist/tools/validation/validation.js +1837 -0
  306. package/dist/tools/validation/validation.js.map +1 -0
  307. package/dist/types/codeGraph.d.ts +163 -0
  308. package/dist/types/codeGraph.d.ts.map +1 -0
  309. package/dist/types/codeGraph.js +9 -0
  310. package/dist/types/codeGraph.js.map +1 -0
  311. package/dist/types/symbolGraph.d.ts +68 -0
  312. package/dist/types/symbolGraph.d.ts.map +1 -0
  313. package/dist/types/symbolGraph.js +10 -0
  314. package/dist/types/symbolGraph.js.map +1 -0
  315. package/dist/types/tools.d.ts +43 -0
  316. package/dist/types/tools.d.ts.map +1 -0
  317. package/dist/types/tools.js +7 -0
  318. package/dist/types/tools.js.map +1 -0
  319. package/dist/utils/fileFilter.d.ts +37 -0
  320. package/dist/utils/fileFilter.d.ts.map +1 -0
  321. package/dist/utils/fileFilter.js +91 -0
  322. package/dist/utils/fileFilter.js.map +1 -0
  323. package/dist/utils/gitUtils.d.ts +28 -0
  324. package/dist/utils/gitUtils.d.ts.map +1 -0
  325. package/dist/utils/gitUtils.js +81 -0
  326. package/dist/utils/gitUtils.js.map +1 -0
  327. package/dist/utils/logger.d.ts +15 -0
  328. package/dist/utils/logger.d.ts.map +1 -0
  329. package/dist/utils/logger.js +38 -0
  330. package/dist/utils/logger.js.map +1 -0
  331. package/dist/utils/serialization.d.ts +25 -0
  332. package/dist/utils/serialization.d.ts.map +1 -0
  333. package/dist/utils/serialization.js +53 -0
  334. package/dist/utils/serialization.js.map +1 -0
  335. package/package.json +90 -0
@@ -0,0 +1,1984 @@
1
+ /**
2
+ * Shared Project Context
3
+ *
4
+ * A centralized context system that builds a comprehensive project map once
5
+ * and shares it across all CodeGuardian tools. This enables:
6
+ *
7
+ * 1. Faster subsequent tool calls (no re-indexing)
8
+ * 2. Cross-tool insights (e.g., dead code + test coverage)
9
+ * 3. Smarter validation (knows what symbols exist in project)
10
+ * 4. Better relevance scoring (understands project structure)
11
+ *
12
+ * @format
13
+ */
14
+ import { glob } from "glob";
15
+ import * as fs from "fs/promises";
16
+ import * as fsSync from "fs";
17
+ import * as path from "path";
18
+ import { logger } from "../utils/logger.js";
19
+ import { filterExcludedFiles, getExcludePatternsForPath, } from "../utils/fileFilter.js";
20
+ import { extractSymbolsAST, extractImportsAST, extractImportsASTWithOptions, } from "../tools/validation/extractors/index.js";
21
+ import { getGitInfo, generateCacheKey, hasGitChanged, } from "../utils/gitUtils.js";
22
+ import { buildSymbolGraph } from "../analyzers/symbolGraph.js";
23
+ import { serialize, deserialize } from "../utils/serialization.js";
24
+ import { extractApiContractContext } from "./apiContractContext.js";
25
+ // ============================================================================
26
+ // Helper Functions for Lazy API Contract Loading
27
+ // ============================================================================
28
+ /**
29
+ * Detect if project has frontend code based on file patterns and symbols
30
+ */
31
+ function detectFrontendPresence(context) {
32
+ const frontendPatterns = [
33
+ '/frontend/', '/client/', '/web/', '/app/src/',
34
+ '/components/', '/pages/', '/views/', '/hooks/'
35
+ ];
36
+ // Check file paths
37
+ for (const filePath of context.files.keys()) {
38
+ const normalizedPath = filePath.toLowerCase();
39
+ if (frontendPatterns.some(pattern => normalizedPath.includes(pattern))) {
40
+ return true;
41
+ }
42
+ // Check for React/Vue/Angular imports
43
+ const fileInfo = context.files.get(filePath);
44
+ if (fileInfo?.imports.some(imp => imp.source.includes('react') ||
45
+ imp.source.includes('vue') ||
46
+ imp.source.includes('@angular'))) {
47
+ return true;
48
+ }
49
+ }
50
+ // Check for frontend-specific symbols
51
+ for (const [symbolName, symbolInfos] of context.symbolIndex) {
52
+ for (const info of symbolInfos) {
53
+ if (info.symbol.kind === 'component' || info.symbol.kind === 'hook') {
54
+ return true;
55
+ }
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * Detect if project has backend code based on file patterns and symbols
62
+ */
63
+ function detectBackendPresence(context) {
64
+ const backendPatterns = [
65
+ '/backend/', '/server/', '/api/', '/routes/',
66
+ '/routers/', '/controllers/', '/models/',
67
+ 'main.py', 'app.py', 'server.js', 'index.js'
68
+ ];
69
+ // Check file paths
70
+ for (const filePath of context.files.keys()) {
71
+ const normalizedPath = filePath.toLowerCase();
72
+ if (backendPatterns.some(pattern => normalizedPath.includes(pattern))) {
73
+ return true;
74
+ }
75
+ // Check for backend framework imports
76
+ const fileInfo = context.files.get(filePath);
77
+ if (fileInfo?.imports.some(imp => imp.source.includes('express') ||
78
+ imp.source.includes('fastapi') ||
79
+ imp.source.includes('flask') ||
80
+ imp.source.includes('fastify') ||
81
+ imp.source.includes('django'))) {
82
+ return true;
83
+ }
84
+ }
85
+ // Check for backend-specific symbols (route handlers, etc.)
86
+ for (const [symbolName, symbolInfos] of context.symbolIndex) {
87
+ for (const info of symbolInfos) {
88
+ // Check if symbol is in a backend file
89
+ const fileInfo = context.files.get(info.file);
90
+ if (fileInfo) {
91
+ const normalizedPath = info.file.toLowerCase();
92
+ if (backendPatterns.some(pattern => normalizedPath.includes(pattern))) {
93
+ return true;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+ /**
101
+ * Check if a file is relevant to API contract validation.
102
+ * When these files change, the API contract context should be refreshed.
103
+ */
104
+ function isApiContractRelevantFile(filePath) {
105
+ const normalized = filePath.toLowerCase();
106
+ // Frontend service files (API calls)
107
+ if (normalized.includes('/services/') && (normalized.endsWith('.ts') || normalized.endsWith('.tsx') || normalized.endsWith('.js'))) {
108
+ return true;
109
+ }
110
+ // Backend route/API files
111
+ if ((normalized.includes('/api/') || normalized.includes('/routes/') || normalized.includes('/routers/')) && normalized.endsWith('.py')) {
112
+ return true;
113
+ }
114
+ // Backend schema/model files (Pydantic models)
115
+ if (normalized.includes('/schemas/') && normalized.endsWith('.py')) {
116
+ return true;
117
+ }
118
+ // Frontend type definition files
119
+ if ((normalized.includes('/types/') || normalized.includes('/interfaces/')) && (normalized.endsWith('.ts') || normalized.endsWith('.tsx'))) {
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+ const contextCache = new Map();
125
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
126
+ const QUICK_CHECK_SAMPLE_SIZE = 20; // Number of files to sample for quick staleness check
127
+ const CACHE_DIR_NAME = ".codeguardian";
128
+ const CACHE_FILE_NAME = "context_cache.json";
129
+ // Track projects with an active guardian — skips TTL/staleness checks since
130
+ // the file watcher keeps context fresh via refreshFileContext
131
+ const guardianActiveProjects = new Set();
132
+ /**
133
+ * Mark a project as having an active guardian.
134
+ * While active, getProjectContext skips TTL/staleness checks and returns
135
+ * the cached context directly (file watcher keeps it fresh).
136
+ */
137
+ export function markGuardianActive(projectPath) {
138
+ guardianActiveProjects.add(projectPath);
139
+ logger.info(`Guardian active for ${projectPath} — context cache will be kept fresh by file watcher`);
140
+ }
141
+ /**
142
+ * Mark a project's guardian as stopped.
143
+ * Resumes normal TTL/staleness checks for cache validity.
144
+ */
145
+ export function markGuardianInactive(projectPath) {
146
+ guardianActiveProjects.delete(projectPath);
147
+ logger.info(`Guardian stopped for ${projectPath} — resuming normal cache TTL`);
148
+ }
149
+ // ============================================================================
150
+ // Main API
151
+ // ============================================================================
152
+ /**
153
+ * Get or build project context
154
+ * Automatically builds context if not cached, and validates freshness
155
+ * Uses smart invalidation based on file modification times
156
+ */
157
+ export async function getProjectContext(projectPath, options = {}) {
158
+ const { language = "all", forceRebuild = false, includeTests = true, maxFiles = 1000, } = options;
159
+ // Get git info for branch-aware caching
160
+ const gitInfo = await getGitInfo(projectPath);
161
+ const cacheKey = generateCacheKey(projectPath, language, includeTests, gitInfo);
162
+ const cached = contextCache.get(cacheKey);
163
+ // If guardian is actively watching this project, trust the cache —
164
+ // the file watcher keeps it fresh via refreshFileContext.
165
+ // Check ALL cache keys for this project (different tools may use different language params)
166
+ if (!forceRebuild && guardianActiveProjects.has(projectPath)) {
167
+ if (cached) {
168
+ logger.debug(`Using guardian-managed context for ${projectPath} (exact key match)`);
169
+ return cached.context;
170
+ }
171
+ // Try to find any cached context for this project path (different language key)
172
+ for (const [key, entry] of contextCache.entries()) {
173
+ if (key.startsWith(projectPath + ":")) {
174
+ logger.debug(`Using guardian-managed context for ${projectPath} (cross-language key match)`);
175
+ return entry.context;
176
+ }
177
+ }
178
+ }
179
+ // Check if cache exists and is potentially valid
180
+ if (!forceRebuild && cached) {
181
+ const age = Date.now() - cached.timestamp;
182
+ // Check if git state changed (branch switch or new commits)
183
+ const gitChanged = await hasGitChanged(projectPath, cached.gitInfo);
184
+ if (gitChanged) {
185
+ logger.info(`Git state changed for ${projectPath} (branch/commit), rebuilding context...`);
186
+ }
187
+ else {
188
+ // If cache is within TTL, do a quick staleness check
189
+ if (age < CACHE_TTL_MS) {
190
+ const isStale = await isContextStale(cached, projectPath);
191
+ if (!isStale) {
192
+ logger.info(`Using cached context for ${projectPath} (validated fresh, age: ${age}ms)`);
193
+ return cached.context;
194
+ }
195
+ logger.info(`Cache is stale for ${projectPath}, rebuilding...`);
196
+ }
197
+ }
198
+ }
199
+ // Try to load from disk if memory cache missed
200
+ if (!forceRebuild) {
201
+ const diskContext = await loadContextFromDisk(projectPath, gitInfo);
202
+ if (diskContext) {
203
+ // Rehydrate into memory cache
204
+ contextCache.set(cacheKey, diskContext);
205
+ // Perform synchronization check to handle files edited while agent was offline
206
+ return reconcileContextWithDisk(diskContext, projectPath, { language, includeTests });
207
+ }
208
+ }
209
+ // Build fresh context (auto-build if not exists)
210
+ const gitBranch = gitInfo ? `${gitInfo.branch}@${gitInfo.commitSHA}` : "no-git";
211
+ logger.info(`Building project context for ${projectPath} [${gitBranch}]${cached ? " (cache invalidated)" : " (first build)"}`);
212
+ const startTime = Date.now();
213
+ // Create .codeguardian directory early so users see immediate feedback
214
+ // This ensures the directory exists before the potentially long context build
215
+ try {
216
+ const cacheDir = path.join(projectPath, CACHE_DIR_NAME);
217
+ await fs.mkdir(cacheDir, { recursive: true });
218
+ logger.debug(`Created ${CACHE_DIR_NAME} directory at ${cacheDir}`);
219
+ }
220
+ catch (err) {
221
+ logger.warn(`Failed to create ${CACHE_DIR_NAME} directory: ${err}`);
222
+ }
223
+ const context = await buildProjectContext(projectPath, {
224
+ language,
225
+ includeTests,
226
+ maxFiles,
227
+ });
228
+ const buildTime = Date.now() - startTime;
229
+ logger.info(`Project context built in ${buildTime}ms - ${context.files.size} files indexed`);
230
+ // Performance warning for large projects
231
+ if (buildTime > 30000) {
232
+ logger.warn(`Context build took ${buildTime}ms - consider using 'scope' parameter to limit files`);
233
+ }
234
+ // Store git info in context
235
+ context.gitInfo = gitInfo;
236
+ // Build file hash map for smart invalidation
237
+ const fileHashes = new Map();
238
+ for (const [filePath, fileInfo] of context.files) {
239
+ if (fileInfo.lastModified) {
240
+ fileHashes.set(filePath, fileInfo.lastModified);
241
+ }
242
+ }
243
+ // Cache it
244
+ contextCache.set(cacheKey, {
245
+ context,
246
+ timestamp: Date.now(),
247
+ fileHashes,
248
+ fileCount: context.files.size,
249
+ gitInfo,
250
+ });
251
+ logger.info(`Context built in ${Date.now() - startTime}ms (${context.files.size} files) [${gitBranch}]`);
252
+ // Save to disk for persistence
253
+ try {
254
+ await saveContextToDisk(projectPath, contextCache.get(cacheKey));
255
+ }
256
+ catch (err) {
257
+ logger.warn(`Failed to save context to disk: ${err instanceof Error ? err.message : String(err)}`);
258
+ }
259
+ return context;
260
+ }
261
+ /**
262
+ * Incrementally refresh a single file in the project context
263
+ */
264
+ export async function refreshFileContext(projectPath, filePath, options = {}) {
265
+ const { language = "all", includeTests = true } = options;
266
+ // Get git info
267
+ const gitInfo = await getGitInfo(projectPath);
268
+ const cacheKey = generateCacheKey(projectPath, language, includeTests, gitInfo);
269
+ // Try memory cache first (exact key match)
270
+ let cached = contextCache.get(cacheKey);
271
+ // If exact key missed but guardian is active, try cross-language key match
272
+ if (!cached && guardianActiveProjects.has(projectPath)) {
273
+ for (const [key, entry] of contextCache.entries()) {
274
+ if (key.startsWith(projectPath + ":")) {
275
+ cached = entry;
276
+ break;
277
+ }
278
+ }
279
+ }
280
+ // If not in memory, try disk
281
+ if (!cached) {
282
+ const diskContext = await loadContextFromDisk(projectPath, gitInfo);
283
+ if (diskContext) {
284
+ contextCache.set(cacheKey, diskContext);
285
+ cached = diskContext;
286
+ }
287
+ }
288
+ // If we still don't have a context, we have to build it full
289
+ if (!cached) {
290
+ return getProjectContext(projectPath, { language, includeTests });
291
+ }
292
+ // Update the file in the context
293
+ await updateFileInContext(cached.context, filePath, projectPath);
294
+ // If the changed file is relevant to API contracts (services, routes, schemas),
295
+ // rebuild the API contract context so all tools see fresh data
296
+ if (cached.context.apiContract && isApiContractRelevantFile(filePath)) {
297
+ try {
298
+ logger.debug(`API contract relevant file changed: ${filePath} — refreshing API contract context...`);
299
+ cached.context.apiContract = await extractApiContractContext(cached.context);
300
+ }
301
+ catch (err) {
302
+ logger.warn(`Failed to refresh API contract context: ${err instanceof Error ? err.message : String(err)}`);
303
+ }
304
+ }
305
+ // Update file hashes in cached record
306
+ try {
307
+ const stats = await fs.stat(filePath);
308
+ cached.fileHashes.set(filePath, stats.mtimeMs);
309
+ cached.timestamp = Date.now(); // Update timestamp to extend TTL
310
+ }
311
+ catch (err) {
312
+ // File might have been deleted
313
+ cached.fileHashes.delete(filePath);
314
+ }
315
+ // Debounce disk writes — during rapid vibecoding, dozens of refreshes fire
316
+ // within seconds. Writing to disk on every one causes I/O contention and
317
+ // concurrent writes to the same file. Instead, schedule a single write
318
+ // after a 2-second quiet period.
319
+ debouncedSaveContextToDisk(projectPath, cached);
320
+ return cached.context;
321
+ }
322
+ /**
323
+ * Synchronize a cached context with the current state of the filesystem.
324
+ * Detects files changed while the agent was offline and performs an incremental catch-up.
325
+ */
326
+ async function reconcileContextWithDisk(cached, projectPath, options) {
327
+ const { language, includeTests } = options;
328
+ // We perform a full timestamp scan to ensure 100% accuracy as requested.
329
+ // This detects any edits made while the agent was offline.
330
+ logger.debug(`Reconciling context with disk for ${projectPath}...`);
331
+ const startTime = Date.now();
332
+ try {
333
+ // 1. Find all current files on disk
334
+ const currentFilesOnDisk = await findProjectFiles(projectPath, language, includeTests);
335
+ const currentFileSet = new Set(currentFilesOnDisk);
336
+ const toUpdate = [];
337
+ // 2. Scan for deleted or modified files
338
+ for (const [filePath, cachedMtime] of cached.fileHashes.entries()) {
339
+ if (!currentFileSet.has(filePath)) {
340
+ toUpdate.push(filePath); // Deleted
341
+ }
342
+ else {
343
+ try {
344
+ const stats = await fs.stat(filePath);
345
+ if (stats.mtimeMs > cachedMtime) {
346
+ toUpdate.push(filePath); // Modified
347
+ }
348
+ }
349
+ catch {
350
+ toUpdate.push(filePath);
351
+ }
352
+ }
353
+ }
354
+ // 3. Scan for new files
355
+ for (const filePath of currentFilesOnDisk) {
356
+ if (!cached.fileHashes.has(filePath)) {
357
+ toUpdate.push(filePath); // New
358
+ }
359
+ }
360
+ if (toUpdate.length === 0) {
361
+ return cached.context;
362
+ }
363
+ logger.info(`Updating context: ${toUpdate.length} files changed while offline.`);
364
+ // 4. Update files incrementally (skipping graph rebuild during batch)
365
+ const BATCH_SIZE = 20;
366
+ for (let i = 0; i < toUpdate.length; i += BATCH_SIZE) {
367
+ const batch = toUpdate.slice(i, i + BATCH_SIZE);
368
+ await Promise.all(batch.map(file => updateFileInContext(cached.context, file, projectPath, true)));
369
+ await new Promise(resolve => setImmediate(resolve));
370
+ }
371
+ // 5. Rebuild symbol graph ONCE at the end
372
+ cached.context.symbolGraph = await buildSymbolGraph(cached.context, {
373
+ includeCallRelationships: true,
374
+ includeCoOccurrence: true,
375
+ minCoOccurrenceCount: 2,
376
+ });
377
+ // 5b. Refresh API contract context if any changed files are API-relevant
378
+ if (cached.context.apiContract && toUpdate.some(f => isApiContractRelevantFile(f))) {
379
+ try {
380
+ logger.info(`Refreshing API contract context after offline reconciliation...`);
381
+ cached.context.apiContract = await extractApiContractContext(cached.context);
382
+ }
383
+ catch (err) {
384
+ logger.warn(`Failed to refresh API contract context during reconciliation: ${err instanceof Error ? err.message : String(err)}`);
385
+ }
386
+ }
387
+ // 6. Update metadata and save
388
+ cached.timestamp = Date.now();
389
+ cached.fileCount = currentFilesOnDisk.length;
390
+ for (const file of toUpdate) {
391
+ try {
392
+ const stats = await fs.stat(file);
393
+ cached.fileHashes.set(file, stats.mtimeMs);
394
+ }
395
+ catch {
396
+ cached.fileHashes.delete(file);
397
+ }
398
+ }
399
+ // Save updated context back to disk
400
+ await saveContextToDisk(projectPath, cached);
401
+ logger.info(`Context synchronized in ${Date.now() - startTime}ms`);
402
+ return cached.context;
403
+ }
404
+ catch (error) {
405
+ logger.warn(`Failure during context reconciliation: ${error instanceof Error ? error.message : String(error)}`);
406
+ return cached.context;
407
+ }
408
+ }
409
+ /**
410
+ * Check if cached context is stale by sampling file modification times
411
+ * This is a quick check that doesn't require reading all files
412
+ */
413
+ async function isContextStale(cached, projectPath) {
414
+ try {
415
+ // Quick check 1: See if file count changed significantly
416
+ // Use the same search logic as building context to ensure consistency
417
+ // PROTOTYPE: Only fully supported languages
418
+ const extensions = {
419
+ javascript: [".js", ".jsx", ".mjs", ".cjs"],
420
+ typescript: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx"], // Include js in ts projects
421
+ python: [".py"],
422
+ all: [".js", ".jsx", ".ts", ".tsx", ".py"], // Only TS/JS/Python for prototype
423
+ };
424
+ const exts = extensions[cached.context.language] || extensions.all;
425
+ const patterns = exts.map((ext) => `${projectPath}/**/*${ext}`);
426
+ const currentFiles = await glob(patterns, {
427
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/venv/**"],
428
+ nodir: true,
429
+ });
430
+ const countDiff = Math.abs(currentFiles.length - cached.fileCount);
431
+ if (countDiff > 5) {
432
+ logger.debug(`File count changed: ${cached.fileCount} -> ${currentFiles.length}`);
433
+ return true;
434
+ }
435
+ // Quick check 2: Sample some files and check their modification times
436
+ const filesToCheck = Array.from(cached.fileHashes.keys())
437
+ .sort(() => Math.random() - 0.5) // Shuffle
438
+ .slice(0, QUICK_CHECK_SAMPLE_SIZE);
439
+ for (const filePath of filesToCheck) {
440
+ try {
441
+ const stats = await fs.stat(filePath);
442
+ const cachedMtime = cached.fileHashes.get(filePath);
443
+ if (cachedMtime && stats.mtimeMs > cachedMtime) {
444
+ logger.debug(`File modified: ${filePath}`);
445
+ return true;
446
+ }
447
+ }
448
+ catch {
449
+ // File was deleted
450
+ logger.debug(`File deleted: ${filePath}`);
451
+ return true;
452
+ }
453
+ }
454
+ // Quick check 3: Check for new files in common directories
455
+ const commonDirs = [
456
+ "src",
457
+ "lib",
458
+ "app",
459
+ "components",
460
+ "pages",
461
+ "hooks",
462
+ "utils",
463
+ ];
464
+ for (const dir of commonDirs) {
465
+ const dirPath = path.join(projectPath, dir);
466
+ try {
467
+ const dirStats = await fs.stat(dirPath);
468
+ // If directory was modified after cache, might have new files
469
+ if (dirStats.mtimeMs > cached.timestamp) {
470
+ const dirFiles = await glob(`${dirPath}/**/*.{ts,tsx,js,jsx,py}`, {
471
+ nodir: true,
472
+ });
473
+ for (const file of dirFiles.slice(0, 5)) {
474
+ if (!cached.fileHashes.has(file)) {
475
+ logger.debug(`New file detected: ${file}`);
476
+ return true;
477
+ }
478
+ }
479
+ }
480
+ }
481
+ catch {
482
+ // Directory doesn't exist, that's fine
483
+ }
484
+ }
485
+ return false;
486
+ }
487
+ catch (error) {
488
+ logger.warn(`Error checking cache staleness: ${error}`);
489
+ return true; // Assume stale on error
490
+ }
491
+ }
492
+ /**
493
+ * Invalidate context cache for a project
494
+ */
495
+ export function invalidateContext(projectPath) {
496
+ for (const key of contextCache.keys()) {
497
+ if (key.startsWith(projectPath)) {
498
+ contextCache.delete(key);
499
+ }
500
+ }
501
+ }
502
+ /**
503
+ * Clear all cached contexts
504
+ */
505
+ export function clearContextCache() {
506
+ contextCache.clear();
507
+ }
508
+ // ============================================================================
509
+ // Context Building
510
+ // ============================================================================
511
+ async function buildProjectContext(projectPath, options) {
512
+ const { language, includeTests, maxFiles } = options;
513
+ // Initialize context
514
+ const context = {
515
+ projectPath,
516
+ language,
517
+ buildTime: new Date().toISOString(),
518
+ totalFiles: 0,
519
+ files: new Map(),
520
+ symbolIndex: new Map(),
521
+ dependencies: [],
522
+ importGraph: new Map(),
523
+ reverseImportGraph: new Map(),
524
+ keywordIndex: new Map(),
525
+ externalDependencies: new Set(),
526
+ entryPoints: [],
527
+ frameworks: [],
528
+ };
529
+ // Find source files (excluding tests if requested)
530
+ const files = await findProjectFiles(projectPath, language, includeTests);
531
+ logger.info(`Found ${files.length} source files (${language}) to analyze`);
532
+ const filesToProcess = files.slice(0, maxFiles);
533
+ // Always find test files separately for import tracking (dead code detection)
534
+ // This ensures exports used only in tests aren't flagged as dead code
535
+ let testFiles = [];
536
+ if (!includeTests) {
537
+ testFiles = await findTestFiles(projectPath, language);
538
+ }
539
+ // Detect frameworks (supports multiple for full-stack projects)
540
+ context.frameworks = await detectFrameworks(projectPath, filesToProcess);
541
+ // Process each file
542
+ for (let i = 0; i < filesToProcess.length; i++) {
543
+ const filePath = filesToProcess[i];
544
+ try {
545
+ const content = await fs.readFile(filePath, "utf-8");
546
+ const stats = await fs.stat(filePath);
547
+ const fileInfo = analyzeFile(filePath, content, projectPath, context.frameworks);
548
+ fileInfo.lastModified = stats.mtimeMs;
549
+ context.files.set(filePath, fileInfo);
550
+ // Build symbol index
551
+ for (const symbol of fileInfo.symbols) {
552
+ if (!context.symbolIndex.has(symbol.name)) {
553
+ context.symbolIndex.set(symbol.name, []);
554
+ }
555
+ context.symbolIndex.get(symbol.name).push({ file: filePath, symbol });
556
+ }
557
+ // Build keyword index
558
+ for (const keyword of fileInfo.keywords) {
559
+ if (!context.keywordIndex.has(keyword)) {
560
+ context.keywordIndex.set(keyword, []);
561
+ }
562
+ context.keywordIndex.get(keyword).push(filePath);
563
+ }
564
+ // Track external dependencies
565
+ for (const imp of fileInfo.imports) {
566
+ if (imp.isExternal) {
567
+ context.externalDependencies.add(imp.source);
568
+ }
569
+ }
570
+ // Track entry points
571
+ if (fileInfo.isEntryPoint) {
572
+ context.entryPoints.push(filePath);
573
+ }
574
+ }
575
+ catch (err) {
576
+ // Skip unreadable files
577
+ }
578
+ // Yield to event loop every 5 files to allow MCP requests to be processed
579
+ if (i % 5 === 0 && i > 0) {
580
+ await new Promise((resolve) => setImmediate(resolve));
581
+ }
582
+ // Log progress periodically to avoid "stuck" feeling
583
+ if (i % 50 === 0 && i > 0) {
584
+ logger.info(`Context build progress: ${i}/${filesToProcess.length} files analyzed`);
585
+ }
586
+ }
587
+ // Process test files for import tracking only (not for symbol indexing)
588
+ // This ensures exports used only in tests aren't flagged as dead code
589
+ for (let i = 0; i < testFiles.length; i++) {
590
+ const testFilePath = testFiles[i];
591
+ try {
592
+ const content = await fs.readFile(testFilePath, "utf-8");
593
+ const stats = await fs.stat(testFilePath);
594
+ const fileInfo = analyzeFile(testFilePath, content, projectPath, context.frameworks);
595
+ fileInfo.lastModified = stats.mtimeMs;
596
+ fileInfo.isTest = true; // Ensure it's marked as test
597
+ // Add to files map (needed for dependency graph building)
598
+ context.files.set(testFilePath, fileInfo);
599
+ // DON'T add symbols to symbolIndex - we don't want to scan test code for issues
600
+ // DON'T add keywords - not needed for test files
601
+ // DON'T track entry points - test files aren't entry points
602
+ }
603
+ catch (err) {
604
+ // Skip unreadable files
605
+ }
606
+ // Yield to event loop every 5 files to allow MCP requests to be processed
607
+ if (i % 5 === 0 && i > 0) {
608
+ await new Promise((resolve) => setImmediate(resolve));
609
+ }
610
+ }
611
+ // Build dependency graph (second pass after all files indexed)
612
+ // Use async version to read tsconfig.json for path aliases
613
+ await buildDependencyGraphAsync(context);
614
+ // Build symbol-level dependency graph (async/yielding)
615
+ context.symbolGraph = await buildSymbolGraph(context, {
616
+ includeCallRelationships: true,
617
+ includeCoOccurrence: true,
618
+ minCoOccurrenceCount: 2,
619
+ });
620
+ // Extract API Contract information (frontend/backend alignment)
621
+ // This integrates API Contract Guardian into the existing context system
622
+ // LAZY LOADING: Only build API contract context if both frontend and backend detected
623
+ try {
624
+ const hasFrontend = detectFrontendPresence(context);
625
+ const hasBackend = detectBackendPresence(context);
626
+ if (hasFrontend && hasBackend) {
627
+ logger.info("Full-stack project detected - building API contract context...");
628
+ const apiContractStart = Date.now();
629
+ context.apiContract = await extractApiContractContext(context);
630
+ logger.info(`API contract context built in ${Date.now() - apiContractStart}ms`);
631
+ }
632
+ else {
633
+ logger.info(`${hasFrontend ? 'Frontend' : hasBackend ? 'Backend' : 'Unknown'}-only project - skipping API contract context`);
634
+ }
635
+ }
636
+ catch (error) {
637
+ logger.warn("Failed to extract API Contract context:", error);
638
+ // Don't fail the entire context build if API Contract extraction fails
639
+ }
640
+ context.totalFiles = context.files.size;
641
+ return context;
642
+ }
643
+ /**
644
+ * Update a single file's information within an existing ProjectContext
645
+ */
646
+ async function updateFileInContext(context, filePath, projectPath, skipGraphRebuild = false) {
647
+ // 1. Remove old data for this file
648
+ context.files.delete(filePath);
649
+ // Remove from symbol index
650
+ for (const [symbolName, infoArray] of context.symbolIndex) {
651
+ const filtered = infoArray.filter(item => item.file !== filePath);
652
+ if (filtered.length === 0) {
653
+ context.symbolIndex.delete(symbolName);
654
+ }
655
+ else {
656
+ context.symbolIndex.set(symbolName, filtered);
657
+ }
658
+ }
659
+ // Remove from keyword index
660
+ for (const [keyword, fileList] of context.keywordIndex) {
661
+ const filtered = fileList.filter(f => f !== filePath);
662
+ if (filtered.length === 0) {
663
+ context.keywordIndex.delete(keyword);
664
+ }
665
+ else {
666
+ context.keywordIndex.set(keyword, filtered);
667
+ }
668
+ }
669
+ // Remove dependencies originating from this file AND pointing to this file
670
+ context.dependencies = context.dependencies.filter(d => d.from !== filePath && d.to !== filePath);
671
+ // Update reverse import graph: remove this file from everyone it imported
672
+ const oldImports = context.importGraph.get(filePath) || [];
673
+ for (const impPath of oldImports) {
674
+ const reverse = context.reverseImportGraph.get(impPath) || [];
675
+ context.reverseImportGraph.set(impPath, reverse.filter(f => f !== filePath));
676
+ }
677
+ context.importGraph.delete(filePath);
678
+ // Clean up files that imported the deleted file:
679
+ // remove the deleted file from their importGraph entries
680
+ const importersOfDeleted = context.reverseImportGraph.get(filePath) || [];
681
+ for (const importerPath of importersOfDeleted) {
682
+ const importerImports = context.importGraph.get(importerPath);
683
+ if (importerImports) {
684
+ context.importGraph.set(importerPath, importerImports.filter(f => f !== filePath));
685
+ }
686
+ }
687
+ // Remove the deleted file's own reverse import graph entry
688
+ context.reverseImportGraph.delete(filePath);
689
+ // Remove from entry points
690
+ context.entryPoints = context.entryPoints.filter(f => f !== filePath);
691
+ // 2. Re-analyze if file still exists
692
+ try {
693
+ if (fsSync.existsSync(filePath)) {
694
+ const content = await fs.readFile(filePath, "utf-8");
695
+ const stats = await fs.stat(filePath);
696
+ const fileInfo = analyzeFile(filePath, content, projectPath, context.frameworks);
697
+ fileInfo.lastModified = stats.mtimeMs;
698
+ // Ensure it's marked correctly if it's a test file
699
+ const isTestDir = filePath.includes("/test/") || filePath.includes("/tests/") || filePath.includes("__tests__");
700
+ const isTestFile = filePath.match(/\.(test|spec)\.[^.]+$/);
701
+ if (isTestDir || isTestFile) {
702
+ fileInfo.isTest = true;
703
+ }
704
+ context.files.set(filePath, fileInfo);
705
+ // Add to symbol index (only if not a test file, matching buildProjectContext logic)
706
+ if (!fileInfo.isTest) {
707
+ for (const symbol of fileInfo.symbols) {
708
+ if (!context.symbolIndex.has(symbol.name)) {
709
+ context.symbolIndex.set(symbol.name, []);
710
+ }
711
+ context.symbolIndex.get(symbol.name).push({ file: filePath, symbol });
712
+ }
713
+ // Add keywords
714
+ for (const keyword of fileInfo.keywords) {
715
+ if (!context.keywordIndex.has(keyword)) {
716
+ context.keywordIndex.set(keyword, []);
717
+ }
718
+ context.keywordIndex.get(keyword).push(filePath);
719
+ }
720
+ // Track entry points
721
+ if (fileInfo.isEntryPoint) {
722
+ context.entryPoints.push(filePath);
723
+ }
724
+ }
725
+ // Track external dependencies
726
+ for (const imp of fileInfo.imports) {
727
+ if (imp.isExternal) {
728
+ context.externalDependencies.add(imp.source);
729
+ }
730
+ }
731
+ // 3. Re-resolve dependencies for this file
732
+ const allFiles = Array.from(context.files.keys());
733
+ const pathAliases = await detectPathAliasesAsync(context.projectPath, allFiles);
734
+ const fileImports = [];
735
+ for (const imp of fileInfo.imports) {
736
+ let resolved = null;
737
+ if (imp.isRelative) {
738
+ resolved = resolveImport(imp.source, filePath, allFiles);
739
+ }
740
+ else if (!imp.isExternal && fileInfo.language === "python") {
741
+ resolved = resolvePythonImport(imp.source, context.projectPath, allFiles);
742
+ }
743
+ else {
744
+ resolved = resolvePathAlias(imp.source, pathAliases, allFiles);
745
+ }
746
+ if (resolved) {
747
+ fileImports.push(resolved);
748
+ context.dependencies.push({
749
+ from: filePath,
750
+ to: resolved,
751
+ importedSymbols: [...imp.namedImports, imp.defaultImport].filter(Boolean),
752
+ });
753
+ // Update reverse graph
754
+ if (!context.reverseImportGraph.has(resolved)) {
755
+ context.reverseImportGraph.set(resolved, []);
756
+ }
757
+ context.reverseImportGraph.get(resolved).push(filePath);
758
+ }
759
+ }
760
+ context.importGraph.set(filePath, fileImports);
761
+ // 3b. Re-resolve OTHER files' unresolved imports that might now point to this file.
762
+ // When a new file is created during vibecoding (e.g., OrbitMap.tsx), existing files
763
+ // (e.g., App.tsx) may have had unresolved imports to it. We check if any other file
764
+ // has a relative import whose resolution target matches this new file's path.
765
+ const fileBaseName = path.basename(filePath).replace(/\.[^.]+$/, ""); // e.g., "OrbitMap"
766
+ for (const [otherPath, otherInfo] of context.files) {
767
+ if (otherPath === filePath)
768
+ continue;
769
+ // Quick check: does this file have any import whose source contains our filename?
770
+ const hasRelevantImport = otherInfo.imports.some(imp => imp.isRelative && imp.source.includes(fileBaseName));
771
+ if (!hasRelevantImport)
772
+ continue;
773
+ // Check if any of this file's imports should resolve to the new/modified file
774
+ for (const imp of otherInfo.imports) {
775
+ if (!imp.isRelative)
776
+ continue;
777
+ const resolved = resolveImport(imp.source, otherPath, allFiles);
778
+ if (resolved !== filePath)
779
+ continue;
780
+ // This import resolves to our file! Check if this edge already exists.
781
+ const existingEdge = context.dependencies.some(d => d.from === otherPath && d.to === filePath);
782
+ if (existingEdge)
783
+ continue;
784
+ // Add the missing dependency edge
785
+ const importedSymbols = [...imp.namedImports, imp.defaultImport].filter(Boolean);
786
+ context.dependencies.push({
787
+ from: otherPath,
788
+ to: filePath,
789
+ importedSymbols,
790
+ });
791
+ // Update import graph
792
+ const otherImports = context.importGraph.get(otherPath) || [];
793
+ if (!otherImports.includes(filePath)) {
794
+ otherImports.push(filePath);
795
+ context.importGraph.set(otherPath, otherImports);
796
+ }
797
+ // Update reverse import graph
798
+ if (!context.reverseImportGraph.has(filePath)) {
799
+ context.reverseImportGraph.set(filePath, []);
800
+ }
801
+ const reverseList = context.reverseImportGraph.get(filePath);
802
+ if (!reverseList.includes(otherPath)) {
803
+ reverseList.push(otherPath);
804
+ }
805
+ }
806
+ }
807
+ }
808
+ }
809
+ catch (err) {
810
+ logger.warn(`Failed to incrementally update context for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
811
+ }
812
+ // 4. Rebuild symbol graph
813
+ if (!skipGraphRebuild) {
814
+ context.symbolGraph = await buildSymbolGraph(context, {
815
+ includeCallRelationships: true,
816
+ includeCoOccurrence: true,
817
+ minCoOccurrenceCount: 2,
818
+ });
819
+ }
820
+ context.buildTime = new Date().toISOString();
821
+ context.totalFiles = context.files.size;
822
+ }
823
+ /**
824
+ * Helper to detect common source root directories in a project.
825
+ * This makes the tool smarter about where to look for code.
826
+ */
827
+ function detectRootSourceDirs(projectPath, language) {
828
+ // Include additional common top-level folders used in smaller repos and fixtures.
829
+ // Notably, many projects (and our tests) place shared helpers in ./utils.
830
+ const commonDirs = language === "python"
831
+ ? ["app", "src", "server", "core", "api"]
832
+ : [
833
+ "src",
834
+ "app",
835
+ "pages",
836
+ "lib",
837
+ "components",
838
+ "actions",
839
+ "services",
840
+ "utils",
841
+ ];
842
+ const found = [];
843
+ for (const dir of commonDirs) {
844
+ const fullPath = path.join(projectPath, dir);
845
+ if (fsSync.existsSync(fullPath)) {
846
+ found.push(dir);
847
+ }
848
+ }
849
+ // If no common dirs found, return the root
850
+ return found.length > 0 ? found : ["."];
851
+ }
852
+ async function findProjectFiles(projectPath, language, includeTests) {
853
+ // PROTOTYPE: Only fully supported languages
854
+ // TODO: Add support for Go, Java, and other languages in future versions
855
+ const extensions = {
856
+ javascript: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"], // Include TS in JS projects for modern interop
857
+ typescript: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx"], // Include JS in TS projects for Vite/React
858
+ python: [".py"],
859
+ all: [".js", ".jsx", ".ts", ".tsx", ".py"], // Only TS/JS/Python for prototype
860
+ };
861
+ const exts = extensions[language] || extensions.all;
862
+ // Intelligence: If running on project root, try to narrow down to common source dirs
863
+ const sourceDirs = detectRootSourceDirs(projectPath, language);
864
+ const patterns = [];
865
+ for (const dir of sourceDirs) {
866
+ for (const ext of exts) {
867
+ // Use standard glob pattern from detected side dirs
868
+ patterns.push(path.join(projectPath, dir, `**/*${ext}`));
869
+ }
870
+ }
871
+ const excludes = [
872
+ "**/node_modules/**",
873
+ "**/venv/**",
874
+ "**/.venv/**",
875
+ "**/env/**",
876
+ "**/__pycache__/**",
877
+ "**/dist/**",
878
+ "**/build/**",
879
+ "**/.next/**",
880
+ "**/coverage/**",
881
+ "**/.git/**",
882
+ "**/vendor/**",
883
+ "**/*.min.js",
884
+ ...getExcludePatternsForPath(projectPath),
885
+ ];
886
+ if (!includeTests) {
887
+ excludes.push("**/*.test.*", "**/*.spec.*", "**/test/**", "**/__tests__/**");
888
+ }
889
+ const files = await glob(patterns, {
890
+ ignore: excludes,
891
+ nodir: true,
892
+ absolute: true,
893
+ });
894
+ return filterExcludedFiles(files);
895
+ }
896
+ /**
897
+ * Find test files only - used for import tracking when includeTests is false
898
+ * This ensures exports used only in tests aren't flagged as dead code
899
+ */
900
+ async function findTestFiles(projectPath, language) {
901
+ const extensions = {
902
+ javascript: [".js", ".jsx", ".mjs", ".cjs"],
903
+ typescript: [".ts", ".tsx", ".mts", ".cts"],
904
+ python: [".py"],
905
+ go: [".go"],
906
+ java: [".java"],
907
+ all: [".js", ".jsx", ".ts", ".tsx", ".py", ".go", ".java"],
908
+ };
909
+ const exts = extensions[language] || extensions.all;
910
+ // Only look for test files
911
+ const testPatterns = [
912
+ ...exts.map((ext) => `${projectPath}/**/*.test${ext}`),
913
+ ...exts.map((ext) => `${projectPath}/**/*.spec${ext}`),
914
+ ...exts.map((ext) => `${projectPath}/**/test/**/*${ext}`),
915
+ ...exts.map((ext) => `${projectPath}/**/__tests__/**/*${ext}`),
916
+ ...exts.map((ext) => `${projectPath}/**/tests/**/*${ext}`),
917
+ ];
918
+ const excludes = [
919
+ "**/node_modules/**",
920
+ "**/venv/**",
921
+ "**/.venv/**",
922
+ "**/env/**",
923
+ "**/__pycache__/**",
924
+ "**/dist/**",
925
+ "**/build/**",
926
+ "**/.next/**",
927
+ "**/coverage/**",
928
+ "**/.git/**",
929
+ "**/vendor/**",
930
+ ];
931
+ const files = await glob(testPatterns, {
932
+ ignore: excludes,
933
+ nodir: true,
934
+ absolute: true,
935
+ });
936
+ return filterExcludedFiles(files);
937
+ }
938
+ // ============================================================================
939
+ // File Analysis
940
+ // ============================================================================
941
+ function analyzeFile(filePath, content, projectPath, frameworks) {
942
+ const ext = path.extname(filePath);
943
+ const language = getLanguageFromExt(ext);
944
+ const relativePath = path.relative(projectPath, filePath);
945
+ const fileName = path.basename(filePath);
946
+ const fileInfo = {
947
+ path: filePath,
948
+ relativePath,
949
+ language,
950
+ size: content.length,
951
+ symbols: [],
952
+ imports: [],
953
+ exports: [],
954
+ keywords: [],
955
+ isTest: isTestFile(filePath),
956
+ isConfig: isConfigFile(fileName),
957
+ isEntryPoint: isEntryPointFile(filePath, frameworks),
958
+ };
959
+ // Extract based on language - use AST for accurate multi-line parsing
960
+ if (language === "typescript" || language === "javascript") {
961
+ extractJSSymbolsAST(content, filePath, language, fileInfo);
962
+ extractJSImportsAST(content, language, fileInfo);
963
+ extractJSExportsRegex(content, fileInfo); // Keep regex for exports (simpler)
964
+ }
965
+ else if (language === "python") {
966
+ extractPythonSymbolsAST(content, filePath, fileInfo);
967
+ extractPythonImportsAST(content, fileInfo);
968
+ }
969
+ // Extract keywords from path and content
970
+ fileInfo.keywords = extractKeywords(filePath, content);
971
+ return fileInfo;
972
+ }
973
+ /**
974
+ * AST-based JS/TS symbol extraction - handles multi-line signatures correctly
975
+ */
976
+ function extractJSSymbolsAST(content, filePath, language, fileInfo) {
977
+ try {
978
+ const astSymbols = extractSymbolsAST(content, filePath, language);
979
+ for (const sym of astSymbols) {
980
+ // Map AST symbol types to FileInfo symbol kinds
981
+ let kind;
982
+ switch (sym.type) {
983
+ case "function":
984
+ // Detect hooks and components
985
+ if (sym.name.startsWith("use") &&
986
+ sym.name[3]?.toUpperCase() === sym.name[3]) {
987
+ kind = "hook";
988
+ }
989
+ else if (/^[A-Z]/.test(sym.name)) {
990
+ kind = "component";
991
+ }
992
+ else {
993
+ kind = "function";
994
+ }
995
+ break;
996
+ case "class":
997
+ kind = "class";
998
+ break;
999
+ case "method":
1000
+ kind = "function"; // Methods are stored as functions with scope info
1001
+ break;
1002
+ case "variable":
1003
+ kind = "variable";
1004
+ break;
1005
+ case "interface":
1006
+ kind = "interface";
1007
+ break;
1008
+ case "type":
1009
+ kind = "type";
1010
+ break;
1011
+ default:
1012
+ kind = "function";
1013
+ }
1014
+ fileInfo.symbols.push({
1015
+ name: sym.name,
1016
+ kind,
1017
+ line: sym.line,
1018
+ exported: sym.isExported ?? false,
1019
+ async: sym.isAsync,
1020
+ params: sym.params?.map((p) => ({ name: p })),
1021
+ returnType: sym.returnType,
1022
+ scope: sym.scope,
1023
+ });
1024
+ }
1025
+ // Also extract interfaces, types, and enums using regex (AST doesn't cover these well)
1026
+ extractJSTypesRegex(content, fileInfo);
1027
+ }
1028
+ catch (err) {
1029
+ // Fallback to regex if AST parsing fails
1030
+ logger.debug(`AST parsing failed for ${filePath}, falling back to regex`);
1031
+ extractJSSymbolsRegex(content, fileInfo);
1032
+ }
1033
+ }
1034
+ /**
1035
+ * Extract TypeScript-specific types (interfaces, types, enums) using regex
1036
+ * These aren't well-supported by tree-sitter-javascript
1037
+ *
1038
+ * NOTE: This now only adds symbols that weren't already extracted by AST parsing
1039
+ */
1040
+ function extractJSTypesRegex(content, fileInfo) {
1041
+ const lines = content.split("\n");
1042
+ // Helper to check if symbol already exists
1043
+ const symbolExists = (name) => fileInfo.symbols.some(s => s.name === name);
1044
+ lines.forEach((line, idx) => {
1045
+ const lineNum = idx + 1;
1046
+ // Interfaces - only add if not already extracted by AST
1047
+ const interfaceMatch = line.match(/(?:export\s+)?interface\s+(\w+)/);
1048
+ if (interfaceMatch && !symbolExists(interfaceMatch[1])) {
1049
+ fileInfo.symbols.push({
1050
+ name: interfaceMatch[1],
1051
+ kind: "interface",
1052
+ line: lineNum,
1053
+ exported: line.includes("export"),
1054
+ });
1055
+ }
1056
+ // Types - only add if not already extracted by AST
1057
+ const typeMatch = line.match(/(?:export\s+)?type\s+(\w+)\s*(?:<[^>]+>)?\s*=/);
1058
+ if (typeMatch && !symbolExists(typeMatch[1])) {
1059
+ fileInfo.symbols.push({
1060
+ name: typeMatch[1],
1061
+ kind: "type",
1062
+ line: lineNum,
1063
+ exported: line.includes("export"),
1064
+ });
1065
+ }
1066
+ // Enums - only add if not already extracted by AST
1067
+ const enumMatch = line.match(/(?:export\s+)?enum\s+(\w+)/);
1068
+ if (enumMatch && !symbolExists(enumMatch[1])) {
1069
+ fileInfo.symbols.push({
1070
+ name: enumMatch[1],
1071
+ kind: "enum",
1072
+ line: lineNum,
1073
+ exported: line.includes("export"),
1074
+ });
1075
+ }
1076
+ });
1077
+ }
1078
+ /**
1079
+ * Fallback regex-based extraction for when AST fails
1080
+ */
1081
+ function extractJSSymbolsRegex(content, fileInfo) {
1082
+ const lines = content.split("\n");
1083
+ lines.forEach((line, idx) => {
1084
+ const lineNum = idx + 1;
1085
+ const funcMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?/);
1086
+ if (funcMatch) {
1087
+ fileInfo.symbols.push({
1088
+ name: funcMatch[1],
1089
+ kind: "function",
1090
+ line: lineNum,
1091
+ exported: line.includes("export"),
1092
+ async: line.includes("async"),
1093
+ returnType: funcMatch[3]?.trim(),
1094
+ params: parseParams(funcMatch[2]),
1095
+ });
1096
+ }
1097
+ // Arrow functions
1098
+ const arrowMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s*)?\(/);
1099
+ if (arrowMatch && !fileInfo.symbols.find((s) => s.name === arrowMatch[1])) {
1100
+ const isHook = arrowMatch[1].startsWith("use") &&
1101
+ arrowMatch[1][3]?.toUpperCase() === arrowMatch[1][3];
1102
+ const isComponent = /^[A-Z]/.test(arrowMatch[1]);
1103
+ fileInfo.symbols.push({
1104
+ name: arrowMatch[1],
1105
+ kind: isHook ? "hook"
1106
+ : isComponent ? "component"
1107
+ : "function",
1108
+ line: lineNum,
1109
+ exported: line.includes("export"),
1110
+ async: line.includes("async"),
1111
+ });
1112
+ }
1113
+ // Classes
1114
+ const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
1115
+ if (classMatch) {
1116
+ fileInfo.symbols.push({
1117
+ name: classMatch[1],
1118
+ kind: "class",
1119
+ line: lineNum,
1120
+ exported: line.includes("export"),
1121
+ });
1122
+ }
1123
+ // Interfaces
1124
+ const interfaceMatch = line.match(/(?:export\s+)?interface\s+(\w+)/);
1125
+ if (interfaceMatch) {
1126
+ fileInfo.symbols.push({
1127
+ name: interfaceMatch[1],
1128
+ kind: "interface",
1129
+ line: lineNum,
1130
+ exported: line.includes("export"),
1131
+ });
1132
+ }
1133
+ // Types
1134
+ const typeMatch = line.match(/(?:export\s+)?type\s+(\w+)\s*(?:<[^>]+>)?\s*=/);
1135
+ if (typeMatch) {
1136
+ fileInfo.symbols.push({
1137
+ name: typeMatch[1],
1138
+ kind: "type",
1139
+ line: lineNum,
1140
+ exported: line.includes("export"),
1141
+ });
1142
+ }
1143
+ // Enums
1144
+ const enumMatch = line.match(/(?:export\s+)?enum\s+(\w+)/);
1145
+ if (enumMatch) {
1146
+ fileInfo.symbols.push({
1147
+ name: enumMatch[1],
1148
+ kind: "enum",
1149
+ line: lineNum,
1150
+ exported: line.includes("export"),
1151
+ });
1152
+ }
1153
+ // Exported variables/constants
1154
+ const varMatch = line.match(/export\s+(?:const|let|var)\s+(\w+)/);
1155
+ if (varMatch && !fileInfo.symbols.find((s) => s.name === varMatch[1])) {
1156
+ fileInfo.symbols.push({
1157
+ name: varMatch[1],
1158
+ kind: "variable",
1159
+ line: lineNum,
1160
+ exported: true,
1161
+ });
1162
+ }
1163
+ });
1164
+ }
1165
+ /**
1166
+ * AST-based JS/TS import extraction
1167
+ */
1168
+ function extractJSImportsAST(content, language, fileInfo) {
1169
+ try {
1170
+ const astImports = extractImportsASTWithOptions(content, language, {
1171
+ filePath: fileInfo.path,
1172
+ });
1173
+ for (const imp of astImports) {
1174
+ fileInfo.imports.push({
1175
+ source: imp.module,
1176
+ isRelative: imp.module.startsWith("."),
1177
+ isExternal: imp.isExternal,
1178
+ defaultImport: imp.names.find((n) => n.imported === "default")?.local,
1179
+ namespaceImport: imp.names.find((n) => n.imported === "*")?.local,
1180
+ namedImports: imp.names
1181
+ .filter((n) => n.imported !== "default" && n.imported !== "*")
1182
+ .map((n) => n.local),
1183
+ line: imp.line,
1184
+ });
1185
+ }
1186
+ }
1187
+ catch (err) {
1188
+ // Fallback to regex if AST parsing fails
1189
+ extractJSImportsRegex(content, fileInfo);
1190
+ }
1191
+ }
1192
+ /**
1193
+ * Fallback regex-based import extraction
1194
+ */
1195
+ function extractJSImportsRegex(content, fileInfo) {
1196
+ const lines = content.split("\n");
1197
+ lines.forEach((line, idx) => {
1198
+ // ES imports
1199
+ const importMatch = line.match(/import\s+(?:(\w+)(?:\s*,\s*)?)?(?:\{([^}]+)\})?\s*from\s*['"]([^'"]+)['"]/);
1200
+ if (importMatch) {
1201
+ const source = importMatch[3];
1202
+ const isRelative = source.startsWith(".");
1203
+ const isExternal = !isRelative &&
1204
+ !source.startsWith("/") &&
1205
+ !source.startsWith("@/") &&
1206
+ !source.startsWith("~/");
1207
+ fileInfo.imports.push({
1208
+ source,
1209
+ isRelative,
1210
+ isExternal,
1211
+ defaultImport: importMatch[1],
1212
+ namedImports: importMatch[2] ?
1213
+ importMatch[2].split(",").map((s) => s.trim().split(" ")[0])
1214
+ : [],
1215
+ line: idx + 1,
1216
+ });
1217
+ }
1218
+ // Dynamic imports: await import('...') or import('...')
1219
+ const dynamicImportMatch = line.match(/(?:await\s+)?import\s*\(\s*['"]([^'"]+)['"]\s*\)/);
1220
+ if (dynamicImportMatch) {
1221
+ const source = dynamicImportMatch[1];
1222
+ const isRelative = source.startsWith(".");
1223
+ const isExternal = !isRelative &&
1224
+ !source.startsWith("/") &&
1225
+ !source.startsWith("@/") &&
1226
+ !source.startsWith("~/");
1227
+ // Try to extract destructured names from the same or next line
1228
+ // e.g., const { foo, bar } = await import('...')
1229
+ const destructureMatch = line.match(/\{\s*([^}]+)\s*\}\s*=\s*(?:await\s+)?import/);
1230
+ const namedImports = [];
1231
+ if (destructureMatch) {
1232
+ const names = destructureMatch[1].split(",");
1233
+ for (const name of names) {
1234
+ const cleanName = name
1235
+ .trim()
1236
+ .split(/\s+as\s+/)[0]
1237
+ .trim();
1238
+ if (cleanName)
1239
+ namedImports.push(cleanName);
1240
+ }
1241
+ }
1242
+ fileInfo.imports.push({
1243
+ source,
1244
+ isRelative,
1245
+ isExternal,
1246
+ namedImports,
1247
+ line: idx + 1,
1248
+ });
1249
+ }
1250
+ // Require
1251
+ const requireMatch = line.match(/(?:const|let|var)\s+(?:(\w+)|\{([^}]+)\})\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
1252
+ if (requireMatch) {
1253
+ const source = requireMatch[3];
1254
+ const isRelative = source.startsWith(".");
1255
+ const isExternal = !isRelative &&
1256
+ !source.startsWith("/") &&
1257
+ !source.startsWith("@/") &&
1258
+ !source.startsWith("~/");
1259
+ fileInfo.imports.push({
1260
+ source,
1261
+ isRelative,
1262
+ isExternal,
1263
+ defaultImport: requireMatch[1],
1264
+ namedImports: requireMatch[2] ?
1265
+ requireMatch[2].split(",").map((s) => s.trim())
1266
+ : [],
1267
+ line: idx + 1,
1268
+ });
1269
+ }
1270
+ });
1271
+ }
1272
+ /**
1273
+ * Regex-based export extraction (simple enough that AST isn't needed)
1274
+ */
1275
+ function extractJSExportsRegex(content, fileInfo) {
1276
+ const lines = content.split("\n");
1277
+ lines.forEach((line, idx) => {
1278
+ // Named exports
1279
+ const namedMatch = line.match(/export\s+(?:const|let|var|function|class|interface|type|enum)\s+(\w+)/);
1280
+ if (namedMatch) {
1281
+ fileInfo.exports.push({
1282
+ name: namedMatch[1],
1283
+ kind: "named",
1284
+ isDefault: false,
1285
+ line: idx + 1,
1286
+ });
1287
+ }
1288
+ // Default export
1289
+ const defaultMatch = line.match(/export\s+default\s+(?:function\s+)?(\w+)?/);
1290
+ if (defaultMatch) {
1291
+ fileInfo.exports.push({
1292
+ name: defaultMatch[1] || "default",
1293
+ kind: "default",
1294
+ isDefault: true,
1295
+ line: idx + 1,
1296
+ });
1297
+ }
1298
+ });
1299
+ }
1300
+ /**
1301
+ * AST-based Python symbol extraction
1302
+ */
1303
+ function extractPythonSymbolsAST(content, filePath, fileInfo) {
1304
+ try {
1305
+ const astSymbols = extractSymbolsAST(content, filePath, "python");
1306
+ for (const sym of astSymbols) {
1307
+ let kind;
1308
+ switch (sym.type) {
1309
+ case "class":
1310
+ kind = "class";
1311
+ break;
1312
+ case "method":
1313
+ kind = "method";
1314
+ break;
1315
+ case "variable":
1316
+ kind = "variable";
1317
+ break;
1318
+ default:
1319
+ kind = "function";
1320
+ }
1321
+ // In Python, names not starting with _ are public (exported)
1322
+ const isExported = sym.isExported ?? !sym.name.startsWith("_");
1323
+ fileInfo.symbols.push({
1324
+ name: sym.name,
1325
+ kind,
1326
+ line: sym.line,
1327
+ exported: isExported,
1328
+ async: sym.isAsync,
1329
+ params: sym.params?.map((p) => ({ name: p })),
1330
+ scope: sym.scope,
1331
+ });
1332
+ // Populate exports list for Python (module-level public symbols)
1333
+ if (isExported && !sym.scope) {
1334
+ fileInfo.exports.push({
1335
+ name: sym.name,
1336
+ kind: kind,
1337
+ isDefault: false,
1338
+ line: sym.line,
1339
+ });
1340
+ }
1341
+ }
1342
+ }
1343
+ catch (err) {
1344
+ // Fallback to regex if AST parsing fails
1345
+ extractPythonSymbolsRegex(content, fileInfo);
1346
+ }
1347
+ }
1348
+ /**
1349
+ * Fallback regex-based Python symbol extraction
1350
+ */
1351
+ function extractPythonSymbolsRegex(content, fileInfo) {
1352
+ const lines = content.split("\n");
1353
+ lines.forEach((line, idx) => {
1354
+ // Functions
1355
+ const funcMatch = line.match(/^(?:async\s+)?def\s+(\w+)\s*\(/);
1356
+ if (funcMatch) {
1357
+ fileInfo.symbols.push({
1358
+ name: funcMatch[1],
1359
+ kind: "function",
1360
+ line: idx + 1,
1361
+ exported: !funcMatch[1].startsWith("_"),
1362
+ async: line.includes("async"),
1363
+ });
1364
+ }
1365
+ // Classes
1366
+ const classMatch = line.match(/^class\s+(\w+)/);
1367
+ if (classMatch) {
1368
+ fileInfo.symbols.push({
1369
+ name: classMatch[1],
1370
+ kind: "class",
1371
+ line: idx + 1,
1372
+ exported: !classMatch[1].startsWith("_"),
1373
+ });
1374
+ }
1375
+ });
1376
+ }
1377
+ /**
1378
+ * AST-based Python import extraction
1379
+ */
1380
+ function extractPythonImportsAST(content, fileInfo) {
1381
+ try {
1382
+ const astImports = extractImportsAST(content, "python");
1383
+ for (const imp of astImports) {
1384
+ fileInfo.imports.push({
1385
+ source: imp.module,
1386
+ isRelative: imp.module.startsWith("."),
1387
+ isExternal: imp.isExternal,
1388
+ namedImports: imp.names.map((n) => n.local),
1389
+ defaultImport: imp.names.length === 1 ? imp.names[0].local : undefined,
1390
+ line: imp.line,
1391
+ });
1392
+ }
1393
+ }
1394
+ catch (err) {
1395
+ // Fallback to regex if AST parsing fails
1396
+ extractPythonImportsRegex(content, fileInfo);
1397
+ }
1398
+ }
1399
+ /**
1400
+ * Fallback regex-based Python import extraction
1401
+ */
1402
+ function extractPythonImportsRegex(content, fileInfo) {
1403
+ const lines = content.split("\n");
1404
+ lines.forEach((line, idx) => {
1405
+ // from X import Y
1406
+ const fromMatch = line.match(/from\s+([\w.]+)\s+import\s+(.+)/);
1407
+ if (fromMatch) {
1408
+ const source = fromMatch[1];
1409
+ const isRelative = source.startsWith(".");
1410
+ const isExternal = !isRelative && !source.includes(".");
1411
+ fileInfo.imports.push({
1412
+ source,
1413
+ isRelative,
1414
+ isExternal,
1415
+ namedImports: fromMatch[2]
1416
+ .split(",")
1417
+ .map((s) => s.trim().split(" ")[0]),
1418
+ line: idx + 1,
1419
+ });
1420
+ }
1421
+ // import X
1422
+ const importMatch = line.match(/^import\s+([\w.]+)/);
1423
+ if (importMatch) {
1424
+ const source = importMatch[1];
1425
+ fileInfo.imports.push({
1426
+ source,
1427
+ isRelative: false,
1428
+ isExternal: !source.includes("."),
1429
+ namedImports: [],
1430
+ defaultImport: source.split(".")[0],
1431
+ line: idx + 1,
1432
+ });
1433
+ }
1434
+ });
1435
+ }
1436
+ // ============================================================================
1437
+ // Dependency Graph
1438
+ // ============================================================================
1439
+ /**
1440
+ * Build dependency graph with async tsconfig.json reading for path aliases
1441
+ */
1442
+ async function buildDependencyGraphAsync(context) {
1443
+ const allFiles = Array.from(context.files.keys());
1444
+ const pathAliases = await detectPathAliasesAsync(context.projectPath, allFiles);
1445
+ let i = 0;
1446
+ for (const [filePath, fileInfo] of context.files) {
1447
+ i++;
1448
+ const imports = [];
1449
+ for (const imp of fileInfo.imports) {
1450
+ let resolved = null;
1451
+ if (imp.isRelative) {
1452
+ // Standard relative import: ./foo, ../bar, from . import x
1453
+ resolved = resolveImport(imp.source, filePath, allFiles);
1454
+ }
1455
+ else if (!imp.isExternal && fileInfo.language === "python") {
1456
+ // Python absolute import: from app.core.config import settings
1457
+ resolved = resolvePythonImport(imp.source, context.projectPath, allFiles);
1458
+ }
1459
+ else {
1460
+ // Check if it's a path alias (e.g., @/services/timeEntries, ~/utils)
1461
+ resolved = resolvePathAlias(imp.source, pathAliases, allFiles);
1462
+ }
1463
+ if (resolved) {
1464
+ imports.push(resolved);
1465
+ // Add dependency edge
1466
+ context.dependencies.push({
1467
+ from: filePath,
1468
+ to: resolved,
1469
+ importedSymbols: [...imp.namedImports, imp.defaultImport].filter(Boolean),
1470
+ });
1471
+ // Update reverse graph
1472
+ if (!context.reverseImportGraph.has(resolved)) {
1473
+ context.reverseImportGraph.set(resolved, []);
1474
+ }
1475
+ context.reverseImportGraph.get(resolved).push(filePath);
1476
+ }
1477
+ }
1478
+ context.importGraph.set(filePath, imports);
1479
+ // Yield every 20 files
1480
+ if (i % 20 === 0) {
1481
+ await new Promise((resolve) => setImmediate(resolve));
1482
+ }
1483
+ }
1484
+ }
1485
+ // Unused function - kept for potential future use
1486
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1487
+ function buildDependencyGraph(context) {
1488
+ const allFiles = Array.from(context.files.keys());
1489
+ // Detect path aliases from common patterns (sync version)
1490
+ // For async tsconfig reading, we'd need to refactor buildProjectContext
1491
+ const pathAliases = detectPathAliases(context.projectPath, allFiles);
1492
+ for (const [filePath, fileInfo] of context.files) {
1493
+ const imports = [];
1494
+ for (const imp of fileInfo.imports) {
1495
+ let resolved = null;
1496
+ if (imp.isRelative) {
1497
+ // Standard relative import: ./foo, ../bar
1498
+ resolved = resolveImport(imp.source, filePath, allFiles);
1499
+ }
1500
+ else if (!imp.isExternal && fileInfo.language === "python") {
1501
+ // Python absolute import: from app.core.config import settings
1502
+ resolved = resolvePythonImport(imp.source, context.projectPath, allFiles);
1503
+ }
1504
+ else {
1505
+ // Check if it's a path alias (e.g., @/services/timeEntries, ~/utils)
1506
+ resolved = resolvePathAlias(imp.source, pathAliases, allFiles);
1507
+ }
1508
+ if (resolved) {
1509
+ imports.push(resolved);
1510
+ // Add dependency edge
1511
+ context.dependencies.push({
1512
+ from: filePath,
1513
+ to: resolved,
1514
+ importedSymbols: [...imp.namedImports, imp.defaultImport].filter(Boolean),
1515
+ });
1516
+ // Update reverse graph
1517
+ if (!context.reverseImportGraph.has(resolved)) {
1518
+ context.reverseImportGraph.set(resolved, []);
1519
+ }
1520
+ context.reverseImportGraph.get(resolved).push(filePath);
1521
+ }
1522
+ }
1523
+ context.importGraph.set(filePath, imports);
1524
+ }
1525
+ }
1526
+ /**
1527
+ * Detect path aliases from tsconfig.json or common patterns
1528
+ */
1529
+ async function detectPathAliasesAsync(projectPath, allFiles) {
1530
+ const aliases = new Map();
1531
+ // Try to read tsconfig.json for actual path mappings
1532
+ try {
1533
+ const tsconfigPath = path.join(projectPath, "tsconfig.json");
1534
+ const tsconfigContent = await fs.readFile(tsconfigPath, "utf-8");
1535
+ const tsconfig = JSON.parse(tsconfigContent);
1536
+ if (tsconfig.compilerOptions?.paths) {
1537
+ const baseUrl = tsconfig.compilerOptions.baseUrl || ".";
1538
+ const basePath = path.join(projectPath, baseUrl);
1539
+ for (const [alias, targets] of Object.entries(tsconfig.compilerOptions.paths)) {
1540
+ if (Array.isArray(targets) && targets.length > 0) {
1541
+ // Remove trailing /* from alias pattern
1542
+ const cleanAlias = alias.replace(/\/\*$/, "/");
1543
+ // Remove trailing /* from target and resolve path
1544
+ const target = targets[0].replace(/\/\*$/, "");
1545
+ const resolvedTarget = path.join(basePath, target);
1546
+ aliases.set(cleanAlias, resolvedTarget);
1547
+ }
1548
+ }
1549
+ }
1550
+ }
1551
+ catch {
1552
+ // tsconfig.json not found or invalid, use heuristics
1553
+ }
1554
+ // If no aliases found from tsconfig, use common patterns
1555
+ if (aliases.size === 0) {
1556
+ const commonAliases = [
1557
+ { prefix: "@/", dirs: ["src", "app", "lib", "."] },
1558
+ { prefix: "~/", dirs: ["src", "app", "lib", "."] },
1559
+ { prefix: "@", dirs: ["src", "app", "lib"] },
1560
+ ];
1561
+ for (const alias of commonAliases) {
1562
+ for (const dir of alias.dirs) {
1563
+ const testPath = path.join(projectPath, dir);
1564
+ const hasFiles = allFiles.some((f) => f.startsWith(testPath + path.sep));
1565
+ if (hasFiles) {
1566
+ aliases.set(alias.prefix, testPath);
1567
+ break;
1568
+ }
1569
+ }
1570
+ }
1571
+ }
1572
+ return aliases;
1573
+ }
1574
+ /**
1575
+ * Synchronous version for use in buildDependencyGraph
1576
+ */
1577
+ function detectPathAliases(projectPath, allFiles) {
1578
+ const aliases = new Map();
1579
+ // Common alias patterns and their typical mappings
1580
+ const commonAliases = [
1581
+ { prefix: "@/", dirs: ["src", "app", "lib", "."] },
1582
+ { prefix: "~/", dirs: ["src", "app", "lib", "."] },
1583
+ { prefix: "@", dirs: ["src", "app", "lib"] },
1584
+ ];
1585
+ for (const alias of commonAliases) {
1586
+ for (const dir of alias.dirs) {
1587
+ const testPath = path.join(projectPath, dir);
1588
+ const hasFiles = allFiles.some((f) => f.startsWith(testPath + path.sep));
1589
+ if (hasFiles) {
1590
+ aliases.set(alias.prefix, testPath);
1591
+ break;
1592
+ }
1593
+ }
1594
+ }
1595
+ return aliases;
1596
+ }
1597
+ /**
1598
+ * Resolve a path alias import to an actual file
1599
+ */
1600
+ function resolvePathAlias(importPath, aliases, allFiles) {
1601
+ // Try each alias prefix
1602
+ for (const [prefix, basePath] of aliases) {
1603
+ if (importPath.startsWith(prefix)) {
1604
+ const relativePart = importPath.slice(prefix.length);
1605
+ const resolved = path.join(basePath, relativePart);
1606
+ // Try exact match
1607
+ if (allFiles.includes(resolved))
1608
+ return resolved;
1609
+ // Try with extensions
1610
+ for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
1611
+ const withExt = resolved + ext;
1612
+ if (allFiles.includes(withExt))
1613
+ return withExt;
1614
+ }
1615
+ // Try index files
1616
+ for (const indexFile of ["index.ts", "index.tsx", "index.js"]) {
1617
+ const withIndex = path.join(resolved, indexFile);
1618
+ if (allFiles.includes(withIndex))
1619
+ return withIndex;
1620
+ }
1621
+ }
1622
+ }
1623
+ return null;
1624
+ }
1625
+ export function resolveImport(importPath, fromFile, allFiles) {
1626
+ const dir = path.dirname(fromFile);
1627
+ const resolved = path.normalize(path.join(dir, importPath));
1628
+ // Try exact match
1629
+ if (allFiles.includes(resolved))
1630
+ return resolved;
1631
+ // Handle .js -> .ts extension mapping (common in TypeScript projects with ESM)
1632
+ // e.g., import from './foo.js' should resolve to './foo.ts'
1633
+ if (resolved.endsWith(".js")) {
1634
+ const withTs = resolved.slice(0, -3) + ".ts";
1635
+ if (allFiles.includes(withTs))
1636
+ return withTs;
1637
+ const withTsx = resolved.slice(0, -3) + ".tsx";
1638
+ if (allFiles.includes(withTsx))
1639
+ return withTsx;
1640
+ }
1641
+ if (resolved.endsWith(".jsx")) {
1642
+ const withTsx = resolved.slice(0, -4) + ".tsx";
1643
+ if (allFiles.includes(withTsx))
1644
+ return withTsx;
1645
+ const withTs = resolved.slice(0, -4) + ".ts";
1646
+ if (allFiles.includes(withTs))
1647
+ return withTs;
1648
+ }
1649
+ // Try with extensions
1650
+ for (const ext of [".ts", ".tsx", ".js", ".jsx", ".py"]) {
1651
+ const withExt = resolved + ext;
1652
+ if (allFiles.includes(withExt))
1653
+ return withExt;
1654
+ }
1655
+ // Try index files
1656
+ for (const indexFile of [
1657
+ "index.ts",
1658
+ "index.tsx",
1659
+ "index.js",
1660
+ "__init__.py",
1661
+ ]) {
1662
+ const withIndex = path.join(resolved, indexFile);
1663
+ if (allFiles.includes(withIndex))
1664
+ return withIndex;
1665
+ }
1666
+ return null;
1667
+ }
1668
+ /**
1669
+ * Resolve a Python dotted module path to an actual file path.
1670
+ * e.g., "app.core.config" → "/project/app/core/config.py"
1671
+ * "app.db" → "/project/app/db/__init__.py"
1672
+ */
1673
+ function resolvePythonImport(modulePath, projectPath, allFiles) {
1674
+ // Convert dotted path to filesystem path: app.core.config → app/core/config
1675
+ const parts = modulePath.split(".");
1676
+ const relPath = parts.join("/");
1677
+ // Try as .py file relative to project root
1678
+ const asPy = path.join(projectPath, relPath + ".py");
1679
+ if (allFiles.includes(asPy))
1680
+ return asPy;
1681
+ // Try as package directory with __init__.py
1682
+ const asInit = path.join(projectPath, relPath, "__init__.py");
1683
+ if (allFiles.includes(asInit))
1684
+ return asInit;
1685
+ // Try looking under common subdirs (backend/, src/, etc.)
1686
+ for (const subdir of ["backend", "src", ""]) {
1687
+ if (!subdir)
1688
+ continue;
1689
+ const withSubdir = path.join(projectPath, subdir, relPath + ".py");
1690
+ if (allFiles.includes(withSubdir))
1691
+ return withSubdir;
1692
+ const withSubdirInit = path.join(projectPath, subdir, relPath, "__init__.py");
1693
+ if (allFiles.includes(withSubdirInit))
1694
+ return withSubdirInit;
1695
+ }
1696
+ return null;
1697
+ }
1698
+ // ============================================================================
1699
+ // Helpers
1700
+ // ============================================================================
1701
+ function getLanguageFromExt(ext) {
1702
+ const map = {
1703
+ ".js": "javascript",
1704
+ ".jsx": "javascript",
1705
+ ".mjs": "javascript",
1706
+ ".ts": "typescript",
1707
+ ".tsx": "typescript",
1708
+ ".mts": "typescript",
1709
+ ".py": "python",
1710
+ ".go": "go",
1711
+ ".java": "java",
1712
+ };
1713
+ return map[ext] || "unknown";
1714
+ }
1715
+ function parseParams(paramStr) {
1716
+ if (!paramStr.trim())
1717
+ return [];
1718
+ return paramStr.split(",").map((p) => {
1719
+ const parts = p.trim().split(":");
1720
+ return {
1721
+ name: parts[0].replace(/[?]$/, "").trim(),
1722
+ type: parts[1]?.trim(),
1723
+ };
1724
+ });
1725
+ }
1726
+ function isTestFile(filePath) {
1727
+ const lower = filePath.toLowerCase();
1728
+ return (lower.includes(".test.") ||
1729
+ lower.includes(".spec.") ||
1730
+ lower.includes("__tests__") ||
1731
+ lower.includes("/test/") ||
1732
+ lower.includes("/tests/"));
1733
+ }
1734
+ function isConfigFile(fileName) {
1735
+ const configPatterns = [
1736
+ /^\..*rc$/,
1737
+ /config\./,
1738
+ /\.config\./,
1739
+ /settings\./,
1740
+ /\.env/,
1741
+ /package\.json/,
1742
+ /tsconfig/,
1743
+ /jest\.config/,
1744
+ /webpack\.config/,
1745
+ /vite\.config/,
1746
+ /next\.config/,
1747
+ ];
1748
+ return configPatterns.some((p) => p.test(fileName.toLowerCase()));
1749
+ }
1750
+ function isEntryPointFile(filePath, frameworks) {
1751
+ const fileName = path.basename(filePath).toLowerCase();
1752
+ const relativePath = filePath.toLowerCase();
1753
+ // Common entry points
1754
+ const commonEntryNames = ["index", "main", "app", "server", "cli", "tool", "handler", "mcp", "worker"];
1755
+ if (commonEntryNames.some((n) => fileName.startsWith(n))) {
1756
+ return true;
1757
+ }
1758
+ // Root files in src/ are often entry points or public APIs
1759
+ // For example: src/validateCode.ts
1760
+ const parts = relativePath.split(/[/\\]/);
1761
+ if (parts.length === 2 && parts[0] === "src") {
1762
+ return true;
1763
+ }
1764
+ // Bin directory
1765
+ if (relativePath.includes("/bin/")) {
1766
+ return true;
1767
+ }
1768
+ // Framework-specific — check ALL detected frameworks
1769
+ if (frameworks) {
1770
+ for (const framework of frameworks) {
1771
+ if (framework.name === "nextjs") {
1772
+ if (relativePath.includes("/app/") &&
1773
+ (fileName === "page.tsx" ||
1774
+ fileName === "page.ts" ||
1775
+ fileName === "layout.tsx")) {
1776
+ return true;
1777
+ }
1778
+ if (relativePath.includes("/pages/") && !fileName.startsWith("_")) {
1779
+ return true;
1780
+ }
1781
+ }
1782
+ if (framework.name === "fastapi") {
1783
+ if (fileName === "main.py" || fileName === "app.py") {
1784
+ return true;
1785
+ }
1786
+ if (relativePath.includes("/api/") && fileName.endsWith(".py")) {
1787
+ return true;
1788
+ }
1789
+ }
1790
+ }
1791
+ }
1792
+ return false;
1793
+ }
1794
+ async function detectFrameworks(projectPath, files) {
1795
+ const frameworks = [];
1796
+ // Check for Next.js
1797
+ const hasNextConfig = files.some((f) => f.includes("next.config"));
1798
+ const hasAppDir = files.some((f) => f.includes("/app/page."));
1799
+ const hasPagesDir = files.some((f) => f.includes("/pages/"));
1800
+ if (hasNextConfig || hasAppDir || hasPagesDir) {
1801
+ frameworks.push({
1802
+ name: "nextjs",
1803
+ patterns: ["app/", "pages/", "components/", "lib/"],
1804
+ });
1805
+ }
1806
+ // Check for React (without Next)
1807
+ if (frameworks.length === 0) {
1808
+ const hasReact = files.some((f) => f.includes("App.tsx") || f.includes("App.jsx"));
1809
+ if (hasReact) {
1810
+ frameworks.push({
1811
+ name: "react",
1812
+ patterns: ["src/", "components/"],
1813
+ });
1814
+ }
1815
+ }
1816
+ // Check for FastAPI/Flask (always check — not mutually exclusive with frontend frameworks)
1817
+ const hasFastAPI = files.some((f) => {
1818
+ const name = path.basename(f);
1819
+ return name === "main.py" || name === "app.py";
1820
+ });
1821
+ if (hasFastAPI) {
1822
+ frameworks.push({
1823
+ name: "fastapi",
1824
+ patterns: ["app/", "api/", "routers/", "services/"],
1825
+ });
1826
+ }
1827
+ return frameworks;
1828
+ }
1829
+ function extractKeywords(filePath, content) {
1830
+ const keywords = [];
1831
+ // From path
1832
+ const pathParts = filePath
1833
+ .split(/[/\\]/)
1834
+ .filter((p) => p && !p.startsWith("."));
1835
+ keywords.push(...pathParts.map((p) => p.toLowerCase().replace(/\.[^.]+$/, "")));
1836
+ // From content (top words by frequency)
1837
+ const words = content.match(/\b[a-zA-Z][a-zA-Z0-9_]{2,}\b/g) || [];
1838
+ const wordFreq = new Map();
1839
+ const stopWords = new Set([
1840
+ "the",
1841
+ "and",
1842
+ "for",
1843
+ "from",
1844
+ "import",
1845
+ "export",
1846
+ "const",
1847
+ "let",
1848
+ "var",
1849
+ "function",
1850
+ "return",
1851
+ "this",
1852
+ "that",
1853
+ "with",
1854
+ "async",
1855
+ "await",
1856
+ "true",
1857
+ "false",
1858
+ ]);
1859
+ for (const word of words) {
1860
+ const lower = word.toLowerCase();
1861
+ if (!stopWords.has(lower) && lower.length > 3) {
1862
+ wordFreq.set(lower, (wordFreq.get(lower) || 0) + 1);
1863
+ }
1864
+ }
1865
+ const topKeywords = Array.from(wordFreq.entries())
1866
+ .sort((a, b) => b[1] - a[1])
1867
+ .slice(0, 15)
1868
+ .map(([word]) => word);
1869
+ keywords.push(...topKeywords);
1870
+ return [...new Set(keywords)];
1871
+ }
1872
+ // ============================================================================
1873
+ // Disk Persistence
1874
+ // ============================================================================
1875
+ /**
1876
+ * Load context from disk cache
1877
+ */
1878
+ async function loadContextFromDisk(projectPath, currentGitInfo) {
1879
+ try {
1880
+ const cacheDir = path.join(projectPath, CACHE_DIR_NAME);
1881
+ const cacheFile = path.join(cacheDir, CACHE_FILE_NAME);
1882
+ // Check if file exists
1883
+ try {
1884
+ await fs.access(cacheFile);
1885
+ }
1886
+ catch {
1887
+ return null;
1888
+ }
1889
+ const content = await fs.readFile(cacheFile, "utf-8");
1890
+ const cached = deserialize(content);
1891
+ // Backward compat: migrate old "framework" (singular) to "frameworks" (array)
1892
+ const ctx = cached.context;
1893
+ if (!ctx.frameworks) {
1894
+ ctx.frameworks = ctx.framework ? [ctx.framework] : [];
1895
+ delete ctx.framework;
1896
+ }
1897
+ // Verify it belongs to this project path (just in case)
1898
+ if (cached.context.projectPath !== projectPath) {
1899
+ logger.info(`Disk cache path mismatch: ${cached.context.projectPath} vs ${projectPath}`);
1900
+ return null;
1901
+ }
1902
+ // Strict Git Check: If git info doesn't match exactly, discard cache
1903
+ if (currentGitInfo && cached.gitInfo) {
1904
+ if (currentGitInfo.branch !== cached.gitInfo.branch ||
1905
+ currentGitInfo.commitSHA !== cached.gitInfo.commitSHA) {
1906
+ logger.info(`Disk cache invalid: Git commit/branch mismatch (${currentGitInfo.commitSHA} vs ${cached.gitInfo.commitSHA})`);
1907
+ return null;
1908
+ }
1909
+ }
1910
+ else if (currentGitInfo || cached.gitInfo) {
1911
+ // One has git, the other doesn't -> mismatch
1912
+ logger.info("Disk cache invalid: Git presence mismatch");
1913
+ return null;
1914
+ }
1915
+ // If no git, check file count as basic proxy
1916
+ // (This is less reliable but better than nothing for non-git projects)
1917
+ if (!currentGitInfo) {
1918
+ // Simple age check - expire after 1 hour if no git
1919
+ if (Date.now() - cached.timestamp > 60 * 60 * 1000) {
1920
+ logger.info("Disk cache expired (no git)");
1921
+ return null;
1922
+ }
1923
+ }
1924
+ logger.info(`Hydrated context from disk for ${projectPath}`);
1925
+ return cached;
1926
+ }
1927
+ catch (error) {
1928
+ logger.warn(`Failed to load context from disk: ${error instanceof Error ? error.message : String(error)}`);
1929
+ return null;
1930
+ }
1931
+ }
1932
+ /**
1933
+ * Debounced wrapper for saveContextToDisk.
1934
+ * During rapid vibecoding, dozens of refreshFileContext calls fire within seconds.
1935
+ * Each one previously called saveContextToDisk immediately, causing concurrent writes
1936
+ * to the same file. This debouncer coalesces them into a single write after a 2-second
1937
+ * quiet period.
1938
+ */
1939
+ const pendingSaveTimers = new Map();
1940
+ function debouncedSaveContextToDisk(projectPath, cachedContext) {
1941
+ const existing = pendingSaveTimers.get(projectPath);
1942
+ if (existing) {
1943
+ clearTimeout(existing);
1944
+ }
1945
+ const timer = setTimeout(() => {
1946
+ pendingSaveTimers.delete(projectPath);
1947
+ saveContextToDisk(projectPath, cachedContext).catch(err => {
1948
+ logger.warn(`Failed to save refreshed context to disk: ${err instanceof Error ? err.message : String(err)}`);
1949
+ });
1950
+ }, 2_000);
1951
+ pendingSaveTimers.set(projectPath, timer);
1952
+ }
1953
+ /**
1954
+ * Save context to disk cache
1955
+ */
1956
+ async function saveContextToDisk(projectPath, cachedContext) {
1957
+ try {
1958
+ const cacheDir = path.join(projectPath, CACHE_DIR_NAME);
1959
+ // Ensure .gitignore exists and includes the cache directory
1960
+ const gitignorePath = path.join(projectPath, ".gitignore");
1961
+ try {
1962
+ const gitignore = await fs.readFile(gitignorePath, "utf-8");
1963
+ if (!gitignore.includes(CACHE_DIR_NAME)) {
1964
+ await fs.appendFile(gitignorePath, `\n# CodeGuardian Cache\n${CACHE_DIR_NAME}/\n`);
1965
+ }
1966
+ }
1967
+ catch {
1968
+ // No .gitignore, create one
1969
+ await fs.writeFile(gitignorePath, `# CodeGuardian Cache\n${CACHE_DIR_NAME}/\n`);
1970
+ }
1971
+ // Create cache directory
1972
+ await fs.mkdir(cacheDir, { recursive: true });
1973
+ const cacheFile = path.join(cacheDir, CACHE_FILE_NAME);
1974
+ const tempFile = `${cacheFile}.tmp`;
1975
+ const content = serialize(cachedContext);
1976
+ await fs.writeFile(tempFile, content, "utf-8");
1977
+ await fs.rename(tempFile, cacheFile);
1978
+ logger.debug(`Persisted context to ${cacheFile}`);
1979
+ }
1980
+ catch (error) {
1981
+ logger.warn(`Failed to save context to disk: ${error instanceof Error ? error.message : String(error)}`);
1982
+ }
1983
+ }
1984
+ //# sourceMappingURL=projectContext.js.map