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,979 @@
1
+ /**
2
+ * API Contract Guardian - Context Integration
3
+ *
4
+ * Integrates API Contract extraction into the existing ProjectContext system.
5
+ * This module extracts frontend services/backend routes and adds them to the context.
6
+ *
7
+ * @format
8
+ */
9
+ import * as fs from "fs/promises";
10
+ import * as path from "path";
11
+ import { logger } from "../utils/logger.js";
12
+ import { detectProjectStructure as detectProjectStructureAuto } from "../api-contract/detector.js";
13
+ import { extractServicesFromFileAST, extractTypesFromFileAST, extractRoutesFromFile, extractModelsFromFile, } from "./apiContractExtraction.js";
14
+ // ============================================================================
15
+ // Main Extraction Function
16
+ // ============================================================================
17
+ /**
18
+ * Extract API Contract information and add it to the project context
19
+ * This is called during context building when API contract validation is enabled
20
+ */
21
+ export async function extractApiContractContext(context) {
22
+ const startTime = Date.now();
23
+ logger.info("Extracting API Contract context...");
24
+ try {
25
+ // Step 1: Detect project structure (frontend/backend)
26
+ const projectStructure = (await detectProjectStructureAuto(path.resolve(context.projectPath)));
27
+ if (!projectStructure.frontend && !projectStructure.backend) {
28
+ logger.info("No frontend or backend detected - skipping API Contract extraction");
29
+ return undefined;
30
+ }
31
+ // Step 2: Extract frontend services and types
32
+ let frontendServices = [];
33
+ let frontendTypes = [];
34
+ if (projectStructure.frontend) {
35
+ const frontendPath = projectStructure.frontend.path;
36
+ [frontendServices, frontendTypes] = await Promise.all([
37
+ extractFrontendServices(context, frontendPath),
38
+ extractFrontendTypes(context, frontendPath),
39
+ ]);
40
+ }
41
+ // Step 3: Extract backend routes and models
42
+ let backendRoutes = [];
43
+ let backendModels = [];
44
+ if (projectStructure.backend) {
45
+ const backendPath = projectStructure.backend.path;
46
+ // Extract router prefixes from main.py/app.py
47
+ const routerPrefixes = await extractRouterPrefixes(backendPath);
48
+ [backendRoutes, backendModels] = await Promise.all([
49
+ extractBackendRoutes(context, backendPath, projectStructure.backend.framework, routerPrefixes),
50
+ extractBackendModels(context, backendPath, projectStructure.backend.framework),
51
+ ]);
52
+ }
53
+ // Step 4: Build mappings
54
+ const { endpointMappings, typeMappings, unmatchedFrontend, unmatchedBackend } = buildContractMappings(frontendServices, frontendTypes, backendRoutes, backendModels);
55
+ const apiContractContext = {
56
+ projectStructure,
57
+ frontendServices,
58
+ frontendTypes,
59
+ backendRoutes,
60
+ backendModels,
61
+ endpointMappings,
62
+ typeMappings,
63
+ unmatchedFrontend,
64
+ unmatchedBackend,
65
+ lastUpdated: new Date().toISOString(),
66
+ };
67
+ const duration = Date.now() - startTime;
68
+ logger.info(`API Contract context extracted in ${duration}ms: ` +
69
+ `${frontendServices.length} services, ${frontendTypes.length} types, ` +
70
+ `${backendRoutes.length} routes, ${backendModels.length} models, ` +
71
+ `${endpointMappings.size} matched endpoints`);
72
+ return apiContractContext;
73
+ }
74
+ catch (error) {
75
+ logger.error("Failed to extract API Contract context:", error);
76
+ return undefined;
77
+ }
78
+ }
79
+ // ============================================================================
80
+ // Frontend Extraction (TypeScript Services & Types)
81
+ // ============================================================================
82
+ async function extractFrontendServices(context, frontendPath) {
83
+ const services = [];
84
+ // Find service files using the context's file index
85
+ // Include /features/, /hooks/, /lib/ since many React projects make API calls there
86
+ const serviceFiles = Array.from(context.files.values()).filter((f) => f.path.startsWith(frontendPath) &&
87
+ (f.path.endsWith(".ts") || f.path.endsWith(".tsx")) &&
88
+ (f.path.includes("/services/") ||
89
+ f.path.includes("/api/") ||
90
+ f.path.includes("/clients/") ||
91
+ f.path.includes("/features/") ||
92
+ f.path.includes("/hooks/") ||
93
+ f.path.includes("/lib/")));
94
+ logger.debug(`[API Contract] Found ${serviceFiles.length} service files in ${frontendPath}`);
95
+ for (const fileInfo of serviceFiles) {
96
+ try {
97
+ logger.debug(`[API Contract] Extracting from: ${fileInfo.path}`);
98
+ // Use AST-based extraction
99
+ const fileServices = await extractServicesFromFileAST(fileInfo.path);
100
+ logger.debug(`[API Contract] Extracted ${fileServices.length} services from ${fileInfo.path}`);
101
+ services.push(...fileServices);
102
+ }
103
+ catch (err) {
104
+ logger.warn(`[API Contract] Failed to extract services from ${fileInfo.path}: ${err}`);
105
+ }
106
+ }
107
+ return services;
108
+ }
109
+ async function extractFrontendTypes(context, frontendPath) {
110
+ const types = [];
111
+ // Find type definition files using the context's file index
112
+ // Also extract types from service files since many projects define types there
113
+ const typeFiles = Array.from(context.files.values()).filter((f) => f.path.startsWith(frontendPath) &&
114
+ (f.path.endsWith(".ts") || f.path.endsWith(".tsx")) &&
115
+ (f.path.includes("/types/") ||
116
+ f.path.includes("/interfaces/") ||
117
+ f.path.includes("/models/") ||
118
+ f.path.includes("/services/") ||
119
+ f.path.includes("/api/") ||
120
+ f.path.includes("/clients/") ||
121
+ f.path.includes("/features/") ||
122
+ f.path.includes("/hooks/") ||
123
+ f.path.includes("/lib/")));
124
+ logger.debug(`[API Contract] Found ${typeFiles.length} type files in ${frontendPath}`);
125
+ for (const fileInfo of typeFiles) {
126
+ try {
127
+ logger.debug(`[API Contract] Extracting types from: ${fileInfo.path}`);
128
+ // Use AST-based extraction
129
+ const fileTypes = await extractTypesFromFileAST(fileInfo.path);
130
+ logger.debug(`[API Contract] Extracted ${fileTypes.length} types from ${fileInfo.path}`);
131
+ types.push(...fileTypes);
132
+ }
133
+ catch (err) {
134
+ logger.warn(`[API Contract] Failed to extract types from ${fileInfo.path}: ${err}`);
135
+ }
136
+ }
137
+ return types;
138
+ }
139
+ // ============================================================================
140
+ // Backend Extraction (Python Routes & Models)
141
+ // ============================================================================
142
+ async function extractBackendRoutes(context, backendPath, framework, routerPrefixes) {
143
+ const routes = [];
144
+ // Find route files using the context's file index
145
+ const routeFiles = Array.from(context.files.values()).filter((f) => f.path.startsWith(backendPath));
146
+ if (framework === "express" || framework === "nestjs") {
147
+ // Express/Node.js backend — process TS/JS files
148
+ for (const fileInfo of routeFiles) {
149
+ if (!fileInfo.path.endsWith(".ts") && !fileInfo.path.endsWith(".js"))
150
+ continue;
151
+ // Only process route files (in routes/ or controllers/ directories, or files with .routes. or .controller. in name)
152
+ const isRouteFile = fileInfo.path.includes("/routes/") ||
153
+ fileInfo.path.includes("/controllers/") ||
154
+ fileInfo.path.includes(".routes.") ||
155
+ fileInfo.path.includes(".controller.");
156
+ if (!isRouteFile)
157
+ continue;
158
+ try {
159
+ const content = await fs.readFile(fileInfo.path, "utf-8");
160
+ const fileRoutes = extractRoutesFromExpressContent(content, fileInfo.path, routerPrefixes);
161
+ routes.push(...fileRoutes);
162
+ }
163
+ catch (err) {
164
+ logger.debug(`Failed to extract routes from ${fileInfo.path}`);
165
+ }
166
+ }
167
+ }
168
+ else {
169
+ // Python backend — process .py files
170
+ for (const fileInfo of routeFiles) {
171
+ if (!fileInfo.path.endsWith(".py"))
172
+ continue;
173
+ try {
174
+ const moduleName = path.basename(fileInfo.path).replace(/\.py$/, "");
175
+ const mountPrefix = routerPrefixes.get(moduleName) || "";
176
+ const fileRoutes = await extractRoutesFromFile(fileInfo.path, framework);
177
+ for (const r of fileRoutes) {
178
+ const normalizedRoutePath = normalizeFullPath(r.path);
179
+ const normalizedMount = normalizeFullPath(mountPrefix);
180
+ const shouldPrefix = Boolean(normalizedMount) &&
181
+ normalizedMount !== "/" &&
182
+ normalizedRoutePath !== normalizedMount &&
183
+ !normalizedRoutePath.startsWith(normalizedMount + "/");
184
+ routes.push({
185
+ ...r,
186
+ path: shouldPrefix ? normalizeFullPath(normalizedMount + normalizedRoutePath) : normalizedRoutePath,
187
+ });
188
+ }
189
+ }
190
+ catch (err) {
191
+ logger.debug(`Failed to extract routes from ${fileInfo.path}`);
192
+ }
193
+ }
194
+ }
195
+ return routes;
196
+ }
197
+ function extractRoutesFromPythonContent(content, filePath, framework, routerPrefixes) {
198
+ const routes = [];
199
+ const lines = content.split("\n");
200
+ // Extract module name from file path (e.g., "clients" from ".../api/clients.py")
201
+ const moduleMatch = filePath.match(/\/(\w+)\.py$/);
202
+ const moduleName = moduleMatch ? moduleMatch[1] : "";
203
+ const mainPrefix = routerPrefixes.get(moduleName) || "";
204
+ // Extract router's internal prefix (e.g., router = APIRouter(prefix="/time-entries"))
205
+ let routerPrefix = "";
206
+ for (const line of lines) {
207
+ const routerPrefixMatch = line.match(/APIRouter\s*\(\s*.*prefix\s*=\s*["']([^"']+)["']/);
208
+ if (routerPrefixMatch) {
209
+ routerPrefix = routerPrefixMatch[1];
210
+ break;
211
+ }
212
+ }
213
+ // Combine prefixes: main.py prefix + router internal prefix
214
+ const prefix = mainPrefix + routerPrefix;
215
+ for (let i = 0; i < lines.length; i++) {
216
+ const line = lines[i];
217
+ const lineNum = i + 1;
218
+ if (framework === "fastapi") {
219
+ // FastAPI: @app.post("/api/clients") or @router.delete("")
220
+ const fastapiMatch = line.match(/@(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*["']([^"']*)["']/i);
221
+ if (fastapiMatch) {
222
+ const routePath = prefix + fastapiMatch[2];
223
+ const route = extractFastAPIRouteDetails(lines, i, fastapiMatch[1], routePath, filePath, lineNum);
224
+ if (route)
225
+ routes.push(route);
226
+ }
227
+ }
228
+ else if (framework === "flask") {
229
+ // Flask: @app.route("/api/clients", methods=["POST"])
230
+ const flaskMatch = line.match(/@app\.route\s*\(\s*["']([^"']+)["']/i);
231
+ if (flaskMatch) {
232
+ const routePath = prefix + flaskMatch[1];
233
+ const route = extractFlaskRouteDetails(lines, i, routePath, filePath, lineNum);
234
+ if (route)
235
+ routes.push(route);
236
+ }
237
+ }
238
+ }
239
+ return routes;
240
+ }
241
+ function extractFastAPIRouteDetails(lines, decoratorLine, method, path, filePath, lineNum) {
242
+ const searchRange = Math.min(decoratorLine + 10, lines.length);
243
+ let funcName = "";
244
+ let requestModel;
245
+ let responseModel;
246
+ let queryParams;
247
+ for (let i = decoratorLine + 1; i < searchRange; i++) {
248
+ const line = lines[i];
249
+ const funcMatch = line.match(/(?:async\s+)?def\s+(\w+)\s*\(/);
250
+ if (funcMatch) {
251
+ funcName = funcMatch[1];
252
+ // Collect the full function signature (may span multiple lines)
253
+ let signature = line;
254
+ let j = i;
255
+ while (!signature.includes(")") && j < searchRange - 1) {
256
+ j++;
257
+ signature += " " + lines[j].trim();
258
+ }
259
+ // Strip inline comments to avoid regex matching comment text as parameters
260
+ signature = signature.replace(/#.*$/gm, "");
261
+ // Extract path parameter names from the route path (e.g., {project_id} -> "project_id")
262
+ const pathParamNames = new Set();
263
+ const pathParamMatches = path.matchAll(/\{(\w+)(?::\w+)?\}/g);
264
+ for (const pm of pathParamMatches) {
265
+ pathParamNames.add(pm[1]);
266
+ }
267
+ // Extract request model from parameter type hint (non-primitive types are request bodies)
268
+ const paramMatches = signature.matchAll(/(\w+)\s*:\s*(\w+)(?:\s*=\s*([^,\)]+))?/g);
269
+ for (const match of paramMatches) {
270
+ const paramName = match[1];
271
+ const paramType = match[2];
272
+ const defaultValue = match[3];
273
+ // Skip common non-body parameters
274
+ if (["db", "session", "request", "response", "user", "current_user"].includes(paramName)) {
275
+ continue;
276
+ }
277
+ // Skip path parameters — they are NOT query parameters
278
+ if (pathParamNames.has(paramName)) {
279
+ continue;
280
+ }
281
+ // Check if it's a query parameter (primitive type)
282
+ const primitiveTypes = ["str", "int", "float", "bool", "uuid", "datetime", "date"];
283
+ if (primitiveTypes.includes(paramType.toLowerCase())) {
284
+ if (!queryParams)
285
+ queryParams = [];
286
+ queryParams.push({
287
+ name: paramName,
288
+ type: paramType,
289
+ required: !defaultValue, // Has default value = optional
290
+ });
291
+ }
292
+ else if (!requestModel && !["str", "int", "float", "bool"].includes(paramType)) {
293
+ // Non-primitive type without default is likely the request body model
294
+ requestModel = paramType;
295
+ }
296
+ }
297
+ // Extract response model from return type
298
+ const returnMatch = signature.match(/-\s*>\s*(\w+)/);
299
+ if (returnMatch && !["str", "int", "float", "bool", "dict", "list", "none"].includes(returnMatch[1].toLowerCase())) {
300
+ responseModel = returnMatch[1];
301
+ }
302
+ // If no request model found from params (e.g., function reads request.json() manually),
303
+ // scan the function body for Pydantic model instantiation patterns:
304
+ // ModelName(**body_json) or ModelName.model_validate(body) or ModelName.parse_obj(body)
305
+ if (!requestModel && (method.toUpperCase() === "POST" || method.toUpperCase() === "PUT" || method.toUpperCase() === "PATCH")) {
306
+ const bodySearchEnd = Math.min(decoratorLine + 40, lines.length);
307
+ for (let k = i + 1; k < bodySearchEnd; k++) {
308
+ const bodyLine = lines[k];
309
+ // Match: variable = ModelName(**anything)
310
+ const instantiationMatch = bodyLine.match(/=\s*([A-Z]\w+)\s*\(\s*\*\*/);
311
+ if (instantiationMatch) {
312
+ requestModel = instantiationMatch[1];
313
+ break;
314
+ }
315
+ // Match: variable = ModelName.model_validate(anything) or .parse_obj(anything)
316
+ const validateMatch = bodyLine.match(/=\s*([A-Z]\w+)\s*\.(?:model_validate|parse_obj)\s*\(/);
317
+ if (validateMatch) {
318
+ requestModel = validateMatch[1];
319
+ break;
320
+ }
321
+ }
322
+ }
323
+ break;
324
+ }
325
+ }
326
+ if (!funcName)
327
+ return null;
328
+ return {
329
+ method: method.toUpperCase(),
330
+ path,
331
+ handler: funcName,
332
+ requestModel,
333
+ responseModel,
334
+ queryParams,
335
+ file: filePath,
336
+ line: lineNum,
337
+ };
338
+ }
339
+ function extractFlaskRouteDetails(lines, decoratorLine, path, filePath, lineNum) {
340
+ const searchRange = Math.min(decoratorLine + 5, lines.length);
341
+ let funcName = "";
342
+ let method = "GET";
343
+ // Check decorator line for methods parameter
344
+ const decoratorLineContent = lines[decoratorLine];
345
+ const methodsMatch = decoratorLineContent.match(/methods\s*=\s*\[(.+?)\]/);
346
+ if (methodsMatch) {
347
+ const methods = methodsMatch[1].split(",").map((m) => m.trim().replace(/["']/g, ""));
348
+ if (methods.length > 0) {
349
+ method = methods[0].toUpperCase();
350
+ }
351
+ }
352
+ for (let i = decoratorLine + 1; i < searchRange; i++) {
353
+ const line = lines[i];
354
+ const funcMatch = line.match(/def\s+(\w+)\s*\(/);
355
+ if (funcMatch) {
356
+ funcName = funcMatch[1];
357
+ break;
358
+ }
359
+ }
360
+ if (!funcName)
361
+ return null;
362
+ return {
363
+ method: method,
364
+ path,
365
+ handler: funcName,
366
+ file: filePath,
367
+ line: lineNum,
368
+ };
369
+ }
370
+ // ============================================================================
371
+ // Express/TypeScript Backend Route Extraction
372
+ // ============================================================================
373
+ function extractRoutesFromExpressContent(content, filePath, routerPrefixes) {
374
+ const routes = [];
375
+ const lines = content.split("\n");
376
+ // Determine the mount prefix for this file
377
+ // routerPrefixes maps file basenames (e.g. "scan.routes") -> mount prefix (e.g. "/api/scans")
378
+ const fileBasename = path.basename(filePath).replace(/\.(ts|js|mjs)$/, "");
379
+ const mountPrefix = routerPrefixes.get(fileBasename) || "";
380
+ for (let i = 0; i < lines.length; i++) {
381
+ const line = lines[i];
382
+ const lineNum = i + 1;
383
+ // Match Express route patterns:
384
+ // router.get("/", handler)
385
+ // router.post("/upload", requireAuth, uploadLimiter, upload.array("files", 100), async (req, res) => {
386
+ // router.delete("/:id/sops/:sopId", async (req, res) => {
387
+ const routeMatch = line.match(/router\.(get|post|put|patch|delete)\s*\(\s*["']([^"']*)["']/i);
388
+ if (!routeMatch)
389
+ continue;
390
+ const method = routeMatch[1].toUpperCase();
391
+ const routePath = mountPrefix + routeMatch[2];
392
+ // Try to find handler name
393
+ let handler = "";
394
+ // Check if the handler is a named function reference on the same line
395
+ // Pattern: router.get("/", requireAuth, getEmployees);
396
+ // The handler is the last non-middleware argument
397
+ const argsAfterPath = line.substring(line.indexOf(routeMatch[2]) + routeMatch[2].length + 1);
398
+ const namedHandlerMatch = argsAfterPath.match(/,\s*(\w+)\s*\)\s*;?\s*$/);
399
+ if (namedHandlerMatch) {
400
+ handler = namedHandlerMatch[1];
401
+ }
402
+ // If no named handler found, check for inline async (req, res) => { pattern
403
+ if (!handler) {
404
+ const inlineMatch = line.match(/async\s*\(\s*\w+\s*,\s*\w+\s*\)/);
405
+ if (inlineMatch) {
406
+ // Use route path as handler name
407
+ handler = `${method.toLowerCase()}_${routePath.replace(/[^a-zA-Z0-9]/g, "_")}`;
408
+ }
409
+ }
410
+ // If handler still not found, search the next few lines
411
+ if (!handler) {
412
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
413
+ const nextLine = lines[j];
414
+ const asyncMatch = nextLine.match(/async\s*\(\s*\w+\s*,\s*\w+\s*\)/);
415
+ if (asyncMatch) {
416
+ handler = `${method.toLowerCase()}_${routePath.replace(/[^a-zA-Z0-9]/g, "_")}`;
417
+ break;
418
+ }
419
+ const namedMatch = nextLine.match(/^\s*(\w+)\s*\)\s*;?\s*$/);
420
+ if (namedMatch) {
421
+ handler = namedMatch[1];
422
+ break;
423
+ }
424
+ }
425
+ }
426
+ if (!handler) {
427
+ handler = `${method.toLowerCase()}_${routePath.replace(/[^a-zA-Z0-9]/g, "_")}`;
428
+ }
429
+ routes.push({
430
+ method: method,
431
+ path: routePath,
432
+ handler,
433
+ file: filePath,
434
+ line: lineNum,
435
+ });
436
+ }
437
+ return routes;
438
+ }
439
+ /**
440
+ * Extract router prefixes from main.py/app.py (Python) or app.ts/server.ts (Express)
441
+ * This maps router module names to their URL prefixes
442
+ */
443
+ async function extractRouterPrefixes(backendPath) {
444
+ const prefixes = new Map();
445
+ // Try to find the main entry file
446
+ const mainFiles = [
447
+ // Express/Node.js
448
+ path.join(backendPath, "src/app.ts"),
449
+ path.join(backendPath, "src/server.ts"),
450
+ path.join(backendPath, "src/index.ts"),
451
+ path.join(backendPath, "app.ts"),
452
+ path.join(backendPath, "server.ts"),
453
+ path.join(backendPath, "index.ts"),
454
+ path.join(backendPath, "src/app.js"),
455
+ path.join(backendPath, "src/server.js"),
456
+ path.join(backendPath, "app.js"),
457
+ path.join(backendPath, "server.js"),
458
+ // Python
459
+ path.join(backendPath, "app/main.py"),
460
+ path.join(backendPath, "main.py"),
461
+ path.join(backendPath, "app.py"),
462
+ ];
463
+ let mainFile = null;
464
+ for (const file of mainFiles) {
465
+ try {
466
+ await fs.access(file);
467
+ mainFile = file;
468
+ break;
469
+ }
470
+ catch {
471
+ // File doesn't exist, try next
472
+ }
473
+ }
474
+ if (!mainFile) {
475
+ logger.debug("No main entry file found for router prefix extraction");
476
+ return prefixes;
477
+ }
478
+ try {
479
+ const content = await fs.readFile(mainFile, "utf-8");
480
+ const lines = content.split("\n");
481
+ if (mainFile.endsWith(".py")) {
482
+ // Python: app.include_router(clients.router, prefix="/api", tags=["clients"])
483
+ for (const line of lines) {
484
+ const match = line.match(/app\.include_router\(\s*(\w+)\.router\s*,\s*prefix\s*=\s*["']([^"']+)["']/);
485
+ if (match) {
486
+ const moduleName = match[1];
487
+ const prefix = match[2];
488
+ prefixes.set(moduleName, prefix);
489
+ logger.debug(`Found router prefix: ${moduleName} -> ${prefix}`);
490
+ }
491
+ }
492
+ }
493
+ else {
494
+ // Express: app.use("/api/scans", scanRoutes);
495
+ // Also need to build a map from import variable name -> source file basename
496
+ const importMap = new Map();
497
+ for (const line of lines) {
498
+ // Match: import scanRoutes from "./routes/scan.routes";
499
+ // Match: import authRoutes from "./routes/auth.routes";
500
+ // Match: const scanRoutes = require("./routes/scan.routes");
501
+ const importMatch = line.match(/import\s+(\w+)\s+from\s+["']([^"']+)["']/);
502
+ if (importMatch) {
503
+ const varName = importMatch[1];
504
+ const importPath = importMatch[2];
505
+ // Extract basename without extension: "./routes/scan.routes" -> "scan.routes"
506
+ const basename = path.basename(importPath).replace(/\.(ts|js|mjs)$/, "");
507
+ importMap.set(varName, basename);
508
+ }
509
+ }
510
+ for (const line of lines) {
511
+ // Match: app.use("/api/scans", scanRoutes);
512
+ // Match: app.use("/api/auth", authLimiter, authRoutes);
513
+ // The route variable is the LAST identifier before the closing paren
514
+ const useMatch = line.match(/app\.use\(\s*["']([^"']+)["']\s*,(.+)\)/);
515
+ if (useMatch) {
516
+ const mountPrefix = useMatch[1];
517
+ const argsStr = useMatch[2].trim();
518
+ // The route handler is the last argument: split by comma, take last, trim
519
+ const args = argsStr.split(",").map(a => a.trim());
520
+ const routeVar = args[args.length - 1];
521
+ if (routeVar && /^\w+$/.test(routeVar)) {
522
+ // Map both the variable name AND the source file basename to the prefix
523
+ // so we can match route files by their filename
524
+ const sourceBasename = importMap.get(routeVar);
525
+ if (sourceBasename) {
526
+ prefixes.set(sourceBasename, mountPrefix);
527
+ logger.debug(`Found Express router prefix: ${sourceBasename} -> ${mountPrefix}`);
528
+ }
529
+ // Also store by variable name as fallback
530
+ prefixes.set(routeVar, mountPrefix);
531
+ }
532
+ }
533
+ }
534
+ }
535
+ }
536
+ catch (err) {
537
+ logger.debug(`Failed to extract router prefixes from ${mainFile}: ${err}`);
538
+ }
539
+ return prefixes;
540
+ }
541
+ async function extractBackendModels(context, backendPath, framework) {
542
+ const models = [];
543
+ if (framework === "express" || framework === "nestjs") {
544
+ // For TS backends, extract types/interfaces from type definition files, schema files, etc.
545
+ const modelFiles = Array.from(context.files.values()).filter((f) => f.path.startsWith(backendPath) &&
546
+ (f.path.endsWith(".ts") || f.path.endsWith(".js")) &&
547
+ (f.path.includes("/types/") ||
548
+ f.path.includes("/models/") ||
549
+ f.path.includes("/schemas/") ||
550
+ f.path.includes("/interfaces/") ||
551
+ f.path.includes("/db/") ||
552
+ f.path.includes(".types.") ||
553
+ f.path.includes(".schema.") ||
554
+ f.path.includes(".model.")));
555
+ for (const fileInfo of modelFiles) {
556
+ try {
557
+ // Reuse the frontend type extraction for TS interfaces
558
+ const fileTypes = await extractTypesFromFileAST(fileInfo.path);
559
+ for (const t of fileTypes) {
560
+ models.push({
561
+ name: t.name,
562
+ fields: t.fields.map(f => ({
563
+ name: f.name,
564
+ type: f.type,
565
+ required: f.required,
566
+ })),
567
+ file: t.file,
568
+ line: t.line,
569
+ baseClasses: [],
570
+ });
571
+ }
572
+ }
573
+ catch (err) {
574
+ logger.debug(`Failed to extract models from ${fileInfo.path}`);
575
+ }
576
+ }
577
+ }
578
+ else {
579
+ // Python backend — find model files
580
+ const modelFiles = Array.from(context.files.values()).filter((f) => f.path.startsWith(backendPath) && f.path.endsWith(".py"));
581
+ for (const fileInfo of modelFiles) {
582
+ try {
583
+ const fileModels = await extractModelsFromFile(fileInfo.path);
584
+ models.push(...fileModels);
585
+ }
586
+ catch (err) {
587
+ logger.debug(`Failed to extract models from ${fileInfo.path}`);
588
+ }
589
+ }
590
+ }
591
+ return models;
592
+ }
593
+ function extractModelsFromPythonContent(content, filePath) {
594
+ const lines = content.split("\n");
595
+ // Pass 1: Extract all classes with their directly declared fields and base class names
596
+ const rawModels = [];
597
+ let currentModel = null;
598
+ for (let i = 0; i < lines.length; i++) {
599
+ const line = lines[i];
600
+ // Class definition: class ClientCreate(BaseModel): or class ClientCreate(ClientBase):
601
+ const classMatch = line.match(/class\s+(\w+)\s*\(\s*([\w.]+)\s*\)/);
602
+ if (classMatch) {
603
+ if (currentModel) {
604
+ rawModels.push(currentModel);
605
+ currentModel = null;
606
+ }
607
+ const className = classMatch[1];
608
+ const baseClass = classMatch[2];
609
+ // Track all classes that could be Pydantic models
610
+ // We'll resolve inheritance in pass 2 to determine which are real models
611
+ currentModel = {
612
+ name: className,
613
+ fields: [],
614
+ file: filePath,
615
+ line: i + 1,
616
+ baseClasses: [baseClass],
617
+ parentName: baseClass,
618
+ };
619
+ continue;
620
+ }
621
+ // Inside a model class
622
+ if (currentModel) {
623
+ const isIndented = line.startsWith(" ") || line.startsWith("\t");
624
+ const isEmpty = line.trim() === "";
625
+ const isComment = line.trim().startsWith("#");
626
+ if (!isIndented && !isEmpty && !isComment) {
627
+ rawModels.push(currentModel);
628
+ currentModel = null;
629
+ continue;
630
+ }
631
+ // Extract field: name: str or email: str = Field(...)
632
+ const fieldMatch = line.match(/^(?:\s+)(\w+)\s*:\s*([\w\[\],\s]+?)(?:\s*=\s*(.+))?$/);
633
+ if (fieldMatch) {
634
+ const fieldName = fieldMatch[1];
635
+ const fieldType = fieldMatch[2].trim();
636
+ const fieldDefault = fieldMatch[3]?.trim();
637
+ // Check if field is required
638
+ let required = true;
639
+ if (fieldType.includes("Optional")) {
640
+ required = false;
641
+ }
642
+ else if (fieldDefault) {
643
+ if (fieldDefault === "None" || fieldDefault.startsWith('"') || fieldDefault.startsWith("'")) {
644
+ required = false;
645
+ }
646
+ else if (!fieldDefault.includes("...")) {
647
+ required = false;
648
+ }
649
+ }
650
+ currentModel.fields.push({
651
+ name: fieldName,
652
+ type: fieldType,
653
+ required,
654
+ default: fieldDefault,
655
+ });
656
+ }
657
+ }
658
+ }
659
+ if (currentModel) {
660
+ rawModels.push(currentModel);
661
+ }
662
+ // Pass 2: Resolve Pydantic inheritance
663
+ // Build a map of class names to their raw models for lookup
664
+ const PYDANTIC_BASES = ["BaseModel", "BaseConfig", "RootModel"];
665
+ const modelMap = new Map();
666
+ for (const m of rawModels) {
667
+ if (m.name)
668
+ modelMap.set(m.name, m);
669
+ }
670
+ // Check if a class is a Pydantic model (directly or transitively)
671
+ function isPydanticModel(className, visited = new Set()) {
672
+ if (PYDANTIC_BASES.includes(className))
673
+ return true;
674
+ if (visited.has(className))
675
+ return false;
676
+ visited.add(className);
677
+ const model = modelMap.get(className);
678
+ if (!model || !model.parentName)
679
+ return false;
680
+ return isPydanticModel(model.parentName, visited);
681
+ }
682
+ // Collect inherited fields by walking up the chain
683
+ function getInheritedFields(className, visited = new Set()) {
684
+ if (PYDANTIC_BASES.includes(className) || visited.has(className))
685
+ return [];
686
+ visited.add(className);
687
+ const model = modelMap.get(className);
688
+ if (!model)
689
+ return [];
690
+ // Get parent fields first, then own fields (own fields override parent)
691
+ const parentFields = model.parentName ? getInheritedFields(model.parentName, visited) : [];
692
+ const ownFieldNames = new Set((model.fields || []).map(f => f.name));
693
+ // Include parent fields that aren't overridden
694
+ const inherited = parentFields.filter(f => !ownFieldNames.has(f.name));
695
+ return [...inherited, ...(model.fields || [])];
696
+ }
697
+ // Build final models with inherited fields
698
+ const models = [];
699
+ for (const raw of rawModels) {
700
+ if (!raw.name || !raw.parentName)
701
+ continue;
702
+ if (!isPydanticModel(raw.parentName))
703
+ continue;
704
+ const allFields = getInheritedFields(raw.name);
705
+ models.push({
706
+ name: raw.name,
707
+ fields: allFields,
708
+ file: raw.file || filePath,
709
+ line: raw.line || 0,
710
+ baseClasses: raw.baseClasses,
711
+ });
712
+ }
713
+ return models;
714
+ }
715
+ // ============================================================================
716
+ // Contract Mapping
717
+ // ============================================================================
718
+ function buildContractMappings(frontendServices, frontendTypes, backendRoutes, backendModels) {
719
+ const endpointMappings = new Map();
720
+ const typeMappings = new Map();
721
+ const unmatchedFrontend = [];
722
+ const unmatchedBackend = [];
723
+ // Match frontend services to backend routes
724
+ for (const service of frontendServices) {
725
+ const matchResult = findMatchingRoute(service, backendRoutes);
726
+ if (matchResult) {
727
+ const score = calculateEndpointMatchScore(service, matchResult.route);
728
+ // If it's a method mismatch, reduce the score significantly
729
+ const finalScore = matchResult.isMethodMismatch ? 50 : score;
730
+ // Check if there are multiple backend routes with the same path (different methods)
731
+ const samePathRoutes = backendRoutes.filter(r => {
732
+ const normalizedRoute = normalizePath(r.path);
733
+ const normalizedService = normalizePath(service.endpoint);
734
+ return normalizedRoute === normalizedService;
735
+ });
736
+ const mapKey = `${service.method} ${service.endpoint}`;
737
+ endpointMappings.set(mapKey, {
738
+ frontend: service,
739
+ backend: matchResult.route,
740
+ score: finalScore,
741
+ hasMultipleMethods: samePathRoutes.length > 1,
742
+ availableMethods: samePathRoutes.map(r => r.method),
743
+ });
744
+ }
745
+ else {
746
+ unmatchedFrontend.push(service);
747
+ }
748
+ }
749
+ // Find unmatched backend routes
750
+ for (const route of backendRoutes) {
751
+ const isMatched = Array.from(endpointMappings.values()).some((m) => m.backend === route);
752
+ if (!isMatched) {
753
+ unmatchedBackend.push(route);
754
+ }
755
+ }
756
+ // Match frontend types to backend models
757
+ for (const type of frontendTypes) {
758
+ const matchingModel = findMatchingModel(type, backendModels);
759
+ if (matchingModel) {
760
+ const compatibility = calculateTypeCompatibility(type, matchingModel);
761
+ typeMappings.set(type.name, {
762
+ frontend: type,
763
+ backend: matchingModel,
764
+ compatibility,
765
+ });
766
+ }
767
+ }
768
+ return { endpointMappings, typeMappings, unmatchedFrontend, unmatchedBackend };
769
+ }
770
+ function findMatchingRoute(service, routes) {
771
+ const normalizedEndpoint = normalizePath(service.endpoint);
772
+ // First try exact match (path + method)
773
+ const exactMatch = routes.find((route) => {
774
+ const normalizedRoute = normalizePath(route.path);
775
+ return (normalizedRoute === normalizedEndpoint &&
776
+ route.method.toUpperCase() === service.method.toUpperCase());
777
+ });
778
+ if (exactMatch)
779
+ return { route: exactMatch, isMethodMismatch: false };
780
+ // Check if there's a route with same path but DIFFERENT method
781
+ // This is a method mismatch we need to flag
782
+ const samePathDifferentMethod = routes.find((route) => {
783
+ const normalizedRoute = normalizePath(route.path);
784
+ return (normalizedRoute === normalizedEndpoint &&
785
+ route.method.toUpperCase() !== service.method.toUpperCase());
786
+ });
787
+ if (samePathDifferentMethod) {
788
+ return { route: samePathDifferentMethod, isMethodMismatch: true };
789
+ }
790
+ // Try fuzzy match (handle API prefix differences)
791
+ const fuzzyMatch = routes.find((route) => {
792
+ const normalizedRoute = normalizePath(route.path);
793
+ const routeWithoutPrefix = removeApiPrefix(normalizedRoute);
794
+ const serviceWithoutPrefix = removeApiPrefix(normalizedEndpoint);
795
+ return (routeWithoutPrefix === serviceWithoutPrefix &&
796
+ route.method.toUpperCase() === service.method.toUpperCase());
797
+ });
798
+ if (fuzzyMatch)
799
+ return { route: fuzzyMatch, isMethodMismatch: false };
800
+ // Try matching with path parameters (normalize all param formats to {param})
801
+ const paramMatch = routes.find((route) => {
802
+ if (route.method.toUpperCase() !== service.method.toUpperCase())
803
+ return false;
804
+ const normalizedRoute = normalizePath(route.path);
805
+ const normalizedServiceEndpoint = normalizePath(service.endpoint);
806
+ // Replace all param formats with generic {param}:
807
+ // - Python/FastAPI: {id}, {project_id}
808
+ // - Express: :id, :projectId
809
+ // - JavaScript template: ${id}, ${projectId}
810
+ const routeWithGenericParams = normalizedRoute
811
+ .replace(/\{[^}]+\}/g, "{param}")
812
+ .replace(/:([a-zA-Z_]\w*)/g, "{param}");
813
+ const endpointWithGenericParams = normalizedServiceEndpoint
814
+ .replace(/\{[^}]+\}/g, "{param}")
815
+ .replace(/\$\{\w+\}/g, "{param}")
816
+ .replace(/:([a-zA-Z_]\w*)/g, "{param}");
817
+ return endpointWithGenericParams === routeWithGenericParams;
818
+ });
819
+ if (paramMatch)
820
+ return { route: paramMatch, isMethodMismatch: false };
821
+ // Try matching with API prefix stripped AND path parameters normalized
822
+ const prefixParamMatch = routes.find((route) => {
823
+ if (route.method.toUpperCase() !== service.method.toUpperCase())
824
+ return false;
825
+ const normalizedRoute = removeApiPrefix(normalizePath(route.path));
826
+ const normalizedServiceEndpoint = removeApiPrefix(normalizePath(service.endpoint));
827
+ const routeWithGenericParams = normalizedRoute
828
+ .replace(/\{[^}]+\}/g, "{param}")
829
+ .replace(/:([a-zA-Z_]\w*)/g, "{param}");
830
+ const endpointWithGenericParams = normalizedServiceEndpoint
831
+ .replace(/\{[^}]+\}/g, "{param}")
832
+ .replace(/\$\{\w+\}/g, "{param}")
833
+ .replace(/:([a-zA-Z_]\w*)/g, "{param}");
834
+ return endpointWithGenericParams === routeWithGenericParams;
835
+ });
836
+ if (prefixParamMatch)
837
+ return { route: prefixParamMatch, isMethodMismatch: false };
838
+ return undefined;
839
+ }
840
+ function calculateEndpointMatchScore(service, route) {
841
+ let score = 100;
842
+ if (service.method.toUpperCase() !== route.method.toUpperCase()) {
843
+ score -= 50;
844
+ }
845
+ const normalizedService = normalizePath(service.endpoint);
846
+ const normalizedRoute = normalizePath(route.path);
847
+ if (normalizedService === normalizedRoute) {
848
+ score += 10;
849
+ }
850
+ else if (removeApiPrefix(normalizedService) === removeApiPrefix(normalizedRoute)) {
851
+ score += 5;
852
+ }
853
+ if (service.requestType && service.requestType === route.requestModel) {
854
+ score += 10;
855
+ }
856
+ if (service.responseType && service.responseType === route.responseModel) {
857
+ score += 10;
858
+ }
859
+ return Math.max(0, Math.min(100, score));
860
+ }
861
+ function findMatchingModel(type, models) {
862
+ // Try exact name match first, but validate field overlap to avoid
863
+ // matching types that share a name but are semantically different
864
+ // (e.g., FE TimeEntryResponse = action response vs BE TimeEntryResponse = data model)
865
+ const exactMatch = models.find((m) => m.name === type.name);
866
+ if (exactMatch) {
867
+ const fieldOverlap = calculateFieldSimilarity(type, exactMatch);
868
+ if (type.fields.length > 0 && exactMatch.fields.length > 0 && fieldOverlap < 0.10) {
869
+ // Very low overlap despite same name — likely different concepts, skip
870
+ }
871
+ else {
872
+ return exactMatch;
873
+ }
874
+ }
875
+ // Try normalized name match with same field overlap guard
876
+ const normalizedTypeName = normalizeName(type.name);
877
+ const normalizedMatch = models.find((m) => normalizeName(m.name) === normalizedTypeName);
878
+ if (normalizedMatch) {
879
+ const fieldOverlap = calculateFieldSimilarity(type, normalizedMatch);
880
+ if (type.fields.length > 0 && normalizedMatch.fields.length > 0 && fieldOverlap < 0.10) {
881
+ // Very low overlap despite similar name — skip
882
+ }
883
+ else {
884
+ return normalizedMatch;
885
+ }
886
+ }
887
+ // Try fuzzy match based on field similarity
888
+ let bestMatch;
889
+ let bestScore = 0;
890
+ for (const model of models) {
891
+ const score = calculateFieldSimilarity(type, model);
892
+ if (score > bestScore && score > 0.7) {
893
+ bestScore = score;
894
+ bestMatch = model;
895
+ }
896
+ }
897
+ return bestMatch;
898
+ }
899
+ function calculateTypeCompatibility(type, model) {
900
+ const issues = [];
901
+ let score = 100;
902
+ // Check for missing required fields in frontend
903
+ for (const modelField of model.fields) {
904
+ if (modelField.required) {
905
+ const frontendField = type.fields.find((f) => normalizeName(f.name) === normalizeName(modelField.name));
906
+ if (!frontendField) {
907
+ score -= 15;
908
+ issues.push(`Missing required field: ${modelField.name}`);
909
+ }
910
+ }
911
+ }
912
+ // Check for naming convention mismatches
913
+ for (const frontendField of type.fields) {
914
+ const backendField = model.fields.find((f) => normalizeName(f.name) === normalizeName(frontendField.name));
915
+ if (backendField && frontendField.name !== backendField.name) {
916
+ score -= 5;
917
+ issues.push(`Naming convention mismatch: ${frontendField.name} vs ${backendField.name}`);
918
+ }
919
+ }
920
+ return { score: Math.max(0, score), issues };
921
+ }
922
+ function calculateFieldSimilarity(type, model) {
923
+ if (type.fields.length === 0 || model.fields.length === 0)
924
+ return 0;
925
+ const typeFieldNames = new Set(type.fields.map((f) => normalizeName(f.name)));
926
+ const modelFieldNames = new Set(model.fields.map((f) => normalizeName(f.name)));
927
+ const intersection = new Set([...typeFieldNames].filter((x) => modelFieldNames.has(x)));
928
+ const union = new Set([...typeFieldNames, ...modelFieldNames]);
929
+ return intersection.size / union.size;
930
+ }
931
+ // ============================================================================
932
+ // Utility Functions
933
+ // ============================================================================
934
+ function normalizePath(path) {
935
+ return path.replace(/\/+/g, "/").replace(/\/$/, "").replace(/^\//, "");
936
+ }
937
+ /**
938
+ * Normalize a URL/API path for storage/display (keeps a leading slash).
939
+ */
940
+ function normalizeFullPath(p) {
941
+ let out = (p || "").trim();
942
+ if (!out)
943
+ return "";
944
+ out = out.replace(/\/+/g, "/");
945
+ if (!out.startsWith("/"))
946
+ out = "/" + out;
947
+ // Strip trailing slash (except root)
948
+ if (out.length > 1)
949
+ out = out.replace(/\/$/, "");
950
+ return out;
951
+ }
952
+ function removeApiPrefix(path) {
953
+ return path.replace(/^(api|v\d+|rest)\//, "");
954
+ }
955
+ function normalizeName(name) {
956
+ return name
957
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
958
+ .toLowerCase()
959
+ .replace(/_/g, "");
960
+ }
961
+ async function fileExists(filePath) {
962
+ try {
963
+ const stats = await fs.stat(filePath);
964
+ return stats.isFile();
965
+ }
966
+ catch {
967
+ return false;
968
+ }
969
+ }
970
+ async function isDirectory(dirPath) {
971
+ try {
972
+ const stats = await fs.stat(dirPath);
973
+ return stats.isDirectory();
974
+ }
975
+ catch {
976
+ return false;
977
+ }
978
+ }
979
+ //# sourceMappingURL=apiContractContext.js.map