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.
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/agent/agentTools.d.ts +26 -0
- package/dist/agent/agentTools.d.ts.map +1 -0
- package/dist/agent/agentTools.js +699 -0
- package/dist/agent/agentTools.js.map +1 -0
- package/dist/agent/autoValidator.d.ts +110 -0
- package/dist/agent/autoValidator.d.ts.map +1 -0
- package/dist/agent/autoValidator.js +964 -0
- package/dist/agent/autoValidator.js.map +1 -0
- package/dist/agent/fileWatcher.d.ts +28 -0
- package/dist/agent/fileWatcher.d.ts.map +1 -0
- package/dist/agent/fileWatcher.js +88 -0
- package/dist/agent/fileWatcher.js.map +1 -0
- package/dist/agent/guardianPersistence.d.ts +98 -0
- package/dist/agent/guardianPersistence.d.ts.map +1 -0
- package/dist/agent/guardianPersistence.js +296 -0
- package/dist/agent/guardianPersistence.js.map +1 -0
- package/dist/agent/mcpNotifications.d.ts +38 -0
- package/dist/agent/mcpNotifications.d.ts.map +1 -0
- package/dist/agent/mcpNotifications.js +81 -0
- package/dist/agent/mcpNotifications.js.map +1 -0
- package/dist/analyzers/aiPatterns.d.ts +16 -0
- package/dist/analyzers/aiPatterns.d.ts.map +1 -0
- package/dist/analyzers/aiPatterns.js +103 -0
- package/dist/analyzers/aiPatterns.js.map +1 -0
- package/dist/analyzers/antiPatterns.d.ts +60 -0
- package/dist/analyzers/antiPatterns.d.ts.map +1 -0
- package/dist/analyzers/antiPatterns.js +198 -0
- package/dist/analyzers/antiPatterns.js.map +1 -0
- package/dist/analyzers/builtinTypes.d.ts +18 -0
- package/dist/analyzers/builtinTypes.d.ts.map +1 -0
- package/dist/analyzers/builtinTypes.js +1275 -0
- package/dist/analyzers/builtinTypes.js.map +1 -0
- package/dist/analyzers/complexity.d.ts +14 -0
- package/dist/analyzers/complexity.d.ts.map +1 -0
- package/dist/analyzers/complexity.js +610 -0
- package/dist/analyzers/complexity.js.map +1 -0
- package/dist/analyzers/findingVerifier.d.ts +59 -0
- package/dist/analyzers/findingVerifier.d.ts.map +1 -0
- package/dist/analyzers/findingVerifier.js +1169 -0
- package/dist/analyzers/findingVerifier.js.map +1 -0
- package/dist/analyzers/impactAnalyzer.d.ts +53 -0
- package/dist/analyzers/impactAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/impactAnalyzer.js +152 -0
- package/dist/analyzers/impactAnalyzer.js.map +1 -0
- package/dist/analyzers/languageDetector.d.ts +48 -0
- package/dist/analyzers/languageDetector.d.ts.map +1 -0
- package/dist/analyzers/languageDetector.js +404 -0
- package/dist/analyzers/languageDetector.js.map +1 -0
- package/dist/analyzers/parsers/incrementalParser.d.ts +53 -0
- package/dist/analyzers/parsers/incrementalParser.d.ts.map +1 -0
- package/dist/analyzers/parsers/incrementalParser.js +193 -0
- package/dist/analyzers/parsers/incrementalParser.js.map +1 -0
- package/dist/analyzers/parsers/scopeResolver.d.ts +92 -0
- package/dist/analyzers/parsers/scopeResolver.d.ts.map +1 -0
- package/dist/analyzers/parsers/scopeResolver.js +324 -0
- package/dist/analyzers/parsers/scopeResolver.js.map +1 -0
- package/dist/analyzers/parsers/semanticIndex.d.ts +127 -0
- package/dist/analyzers/parsers/semanticIndex.d.ts.map +1 -0
- package/dist/analyzers/parsers/semanticIndex.js +429 -0
- package/dist/analyzers/parsers/semanticIndex.js.map +1 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.d.ts +42 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.js +233 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.js.map +1 -0
- package/dist/analyzers/parsers/treeSitterParser.d.ts +76 -0
- package/dist/analyzers/parsers/treeSitterParser.d.ts.map +1 -0
- package/dist/analyzers/parsers/treeSitterParser.js +709 -0
- package/dist/analyzers/parsers/treeSitterParser.js.map +1 -0
- package/dist/analyzers/relevanceScorer.d.ts +43 -0
- package/dist/analyzers/relevanceScorer.d.ts.map +1 -0
- package/dist/analyzers/relevanceScorer.js +200 -0
- package/dist/analyzers/relevanceScorer.js.map +1 -0
- package/dist/analyzers/standardLibrary.d.ts +22 -0
- package/dist/analyzers/standardLibrary.d.ts.map +1 -0
- package/dist/analyzers/standardLibrary.js +211 -0
- package/dist/analyzers/standardLibrary.js.map +1 -0
- package/dist/analyzers/symbolGraph.d.ts +30 -0
- package/dist/analyzers/symbolGraph.d.ts.map +1 -0
- package/dist/analyzers/symbolGraph.js +380 -0
- package/dist/analyzers/symbolGraph.js.map +1 -0
- package/dist/analyzers/symbolTable.d.ts +18 -0
- package/dist/analyzers/symbolTable.d.ts.map +1 -0
- package/dist/analyzers/symbolTable.js +176 -0
- package/dist/analyzers/symbolTable.js.map +1 -0
- package/dist/analyzers/typeChecker.d.ts +13 -0
- package/dist/analyzers/typeChecker.d.ts.map +1 -0
- package/dist/analyzers/typeChecker.js +580 -0
- package/dist/analyzers/typeChecker.js.map +1 -0
- package/dist/analyzers/usagePatterns.d.ts +42 -0
- package/dist/analyzers/usagePatterns.d.ts.map +1 -0
- package/dist/analyzers/usagePatterns.js +75 -0
- package/dist/analyzers/usagePatterns.js.map +1 -0
- package/dist/api-contract/context/backend.d.ts +19 -0
- package/dist/api-contract/context/backend.d.ts.map +1 -0
- package/dist/api-contract/context/backend.js +64 -0
- package/dist/api-contract/context/backend.js.map +1 -0
- package/dist/api-contract/context/contract.d.ts +34 -0
- package/dist/api-contract/context/contract.d.ts.map +1 -0
- package/dist/api-contract/context/contract.js +306 -0
- package/dist/api-contract/context/contract.js.map +1 -0
- package/dist/api-contract/context/frontend.d.ts +19 -0
- package/dist/api-contract/context/frontend.d.ts.map +1 -0
- package/dist/api-contract/context/frontend.js +64 -0
- package/dist/api-contract/context/frontend.js.map +1 -0
- package/dist/api-contract/detector.d.ts +28 -0
- package/dist/api-contract/detector.d.ts.map +1 -0
- package/dist/api-contract/detector.js +393 -0
- package/dist/api-contract/detector.js.map +1 -0
- package/dist/api-contract/extractors/python.d.ts +32 -0
- package/dist/api-contract/extractors/python.d.ts.map +1 -0
- package/dist/api-contract/extractors/python.js +521 -0
- package/dist/api-contract/extractors/python.js.map +1 -0
- package/dist/api-contract/extractors/pythonAstUtils.d.ts +44 -0
- package/dist/api-contract/extractors/pythonAstUtils.d.ts.map +1 -0
- package/dist/api-contract/extractors/pythonAstUtils.js +489 -0
- package/dist/api-contract/extractors/pythonAstUtils.js.map +1 -0
- package/dist/api-contract/extractors/tsAstUtils.d.ts +47 -0
- package/dist/api-contract/extractors/tsAstUtils.d.ts.map +1 -0
- package/dist/api-contract/extractors/tsAstUtils.js +173 -0
- package/dist/api-contract/extractors/tsAstUtils.js.map +1 -0
- package/dist/api-contract/extractors/typescript.d.ts +32 -0
- package/dist/api-contract/extractors/typescript.d.ts.map +1 -0
- package/dist/api-contract/extractors/typescript.js +666 -0
- package/dist/api-contract/extractors/typescript.js.map +1 -0
- package/dist/api-contract/index.d.ts +104 -0
- package/dist/api-contract/index.d.ts.map +1 -0
- package/dist/api-contract/index.js +232 -0
- package/dist/api-contract/index.js.map +1 -0
- package/dist/api-contract/types.d.ts +151 -0
- package/dist/api-contract/types.d.ts.map +1 -0
- package/dist/api-contract/types.js +19 -0
- package/dist/api-contract/types.js.map +1 -0
- package/dist/api-contract/validators/endpoint.d.ts +21 -0
- package/dist/api-contract/validators/endpoint.d.ts.map +1 -0
- package/dist/api-contract/validators/endpoint.js +224 -0
- package/dist/api-contract/validators/endpoint.js.map +1 -0
- package/dist/api-contract/validators/index.d.ts +40 -0
- package/dist/api-contract/validators/index.d.ts.map +1 -0
- package/dist/api-contract/validators/index.js +875 -0
- package/dist/api-contract/validators/index.js.map +1 -0
- package/dist/api-contract/validators/parameter.d.ts +17 -0
- package/dist/api-contract/validators/parameter.d.ts.map +1 -0
- package/dist/api-contract/validators/parameter.js +250 -0
- package/dist/api-contract/validators/parameter.js.map +1 -0
- package/dist/api-contract/validators/type.d.ts +38 -0
- package/dist/api-contract/validators/type.d.ts.map +1 -0
- package/dist/api-contract/validators/type.js +244 -0
- package/dist/api-contract/validators/type.js.map +1 -0
- package/dist/context/apiContract/complexTypeSupport.d.ts +83 -0
- package/dist/context/apiContract/complexTypeSupport.d.ts.map +1 -0
- package/dist/context/apiContract/complexTypeSupport.js +665 -0
- package/dist/context/apiContract/complexTypeSupport.js.map +1 -0
- package/dist/context/apiContract/graphqlSupport.d.ts +105 -0
- package/dist/context/apiContract/graphqlSupport.d.ts.map +1 -0
- package/dist/context/apiContract/graphqlSupport.js +671 -0
- package/dist/context/apiContract/graphqlSupport.js.map +1 -0
- package/dist/context/apiContract/index.d.ts +14 -0
- package/dist/context/apiContract/index.d.ts.map +1 -0
- package/dist/context/apiContract/index.js +17 -0
- package/dist/context/apiContract/index.js.map +1 -0
- package/dist/context/apiContract/webSocketSupport.d.ts +104 -0
- package/dist/context/apiContract/webSocketSupport.d.ts.map +1 -0
- package/dist/context/apiContract/webSocketSupport.js +465 -0
- package/dist/context/apiContract/webSocketSupport.js.map +1 -0
- package/dist/context/apiContractContext.d.ts +15 -0
- package/dist/context/apiContractContext.d.ts.map +1 -0
- package/dist/context/apiContractContext.js +979 -0
- package/dist/context/apiContractContext.js.map +1 -0
- package/dist/context/apiContractExtraction.d.ts +52 -0
- package/dist/context/apiContractExtraction.d.ts.map +1 -0
- package/dist/context/apiContractExtraction.js +438 -0
- package/dist/context/apiContractExtraction.js.map +1 -0
- package/dist/context/contextLineage.d.ts +79 -0
- package/dist/context/contextLineage.d.ts.map +1 -0
- package/dist/context/contextLineage.js +259 -0
- package/dist/context/contextLineage.js.map +1 -0
- package/dist/context/contextOrchestrator.d.ts +57 -0
- package/dist/context/contextOrchestrator.d.ts.map +1 -0
- package/dist/context/contextOrchestrator.js +162 -0
- package/dist/context/contextOrchestrator.js.map +1 -0
- package/dist/context/intentTracker.d.ts +73 -0
- package/dist/context/intentTracker.d.ts.map +1 -0
- package/dist/context/intentTracker.js +168 -0
- package/dist/context/intentTracker.js.map +1 -0
- package/dist/context/projectContext.d.ts +219 -0
- package/dist/context/projectContext.d.ts.map +1 -0
- package/dist/context/projectContext.js +1984 -0
- package/dist/context/projectContext.js.map +1 -0
- package/dist/prompts/index.d.ts +17 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +260 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/library.d.ts +51 -0
- package/dist/prompts/library.d.ts.map +1 -0
- package/dist/prompts/library.js +65 -0
- package/dist/prompts/library.js.map +1 -0
- package/dist/prompts/templates.d.ts +44 -0
- package/dist/prompts/templates.d.ts.map +1 -0
- package/dist/prompts/templates.js +97 -0
- package/dist/prompts/templates.js.map +1 -0
- package/dist/queue/jobPersistence.d.ts +46 -0
- package/dist/queue/jobPersistence.d.ts.map +1 -0
- package/dist/queue/jobPersistence.js +158 -0
- package/dist/queue/jobPersistence.js.map +1 -0
- package/dist/queue/jobQueue.d.ts +116 -0
- package/dist/queue/jobQueue.d.ts.map +1 -0
- package/dist/queue/jobQueue.js +275 -0
- package/dist/queue/jobQueue.js.map +1 -0
- package/dist/queue/validationJob.d.ts +69 -0
- package/dist/queue/validationJob.d.ts.map +1 -0
- package/dist/queue/validationJob.js +435 -0
- package/dist/queue/validationJob.js.map +1 -0
- package/dist/resources/index.d.ts +15 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +328 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/validationReportStore.d.ts +170 -0
- package/dist/resources/validationReportStore.d.ts.map +1 -0
- package/dist/resources/validationReportStore.js +515 -0
- package/dist/resources/validationReportStore.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +102 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/asyncValidation.d.ts +19 -0
- package/dist/tools/asyncValidation.d.ts.map +1 -0
- package/dist/tools/asyncValidation.js +346 -0
- package/dist/tools/asyncValidation.js.map +1 -0
- package/dist/tools/buildContext.d.ts +17 -0
- package/dist/tools/buildContext.d.ts.map +1 -0
- package/dist/tools/buildContext.js +188 -0
- package/dist/tools/buildContext.js.map +1 -0
- package/dist/tools/getDependencyGraph.d.ts +16 -0
- package/dist/tools/getDependencyGraph.d.ts.map +1 -0
- package/dist/tools/getDependencyGraph.js +436 -0
- package/dist/tools/getDependencyGraph.js.map +1 -0
- package/dist/tools/incrementalValidation.d.ts +71 -0
- package/dist/tools/incrementalValidation.d.ts.map +1 -0
- package/dist/tools/incrementalValidation.js +203 -0
- package/dist/tools/incrementalValidation.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +106 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/validateCode.d.ts +17 -0
- package/dist/tools/validateCode.d.ts.map +1 -0
- package/dist/tools/validateCode.js +368 -0
- package/dist/tools/validateCode.js.map +1 -0
- package/dist/tools/validateCodeLite.d.ts +2 -0
- package/dist/tools/validateCodeLite.d.ts.map +1 -0
- package/dist/tools/validateCodeLite.js +2 -0
- package/dist/tools/validateCodeLite.js.map +1 -0
- package/dist/tools/validation/builtins.d.ts +92 -0
- package/dist/tools/validation/builtins.d.ts.map +1 -0
- package/dist/tools/validation/builtins.js +2184 -0
- package/dist/tools/validation/builtins.js.map +1 -0
- package/dist/tools/validation/contextualNaming.d.ts +99 -0
- package/dist/tools/validation/contextualNaming.d.ts.map +1 -0
- package/dist/tools/validation/contextualNaming.js +959 -0
- package/dist/tools/validation/contextualNaming.js.map +1 -0
- package/dist/tools/validation/deadCode.d.ts +115 -0
- package/dist/tools/validation/deadCode.d.ts.map +1 -0
- package/dist/tools/validation/deadCode.js +861 -0
- package/dist/tools/validation/deadCode.js.map +1 -0
- package/dist/tools/validation/extractors/index.d.ts +131 -0
- package/dist/tools/validation/extractors/index.d.ts.map +1 -0
- package/dist/tools/validation/extractors/index.js +233 -0
- package/dist/tools/validation/extractors/index.js.map +1 -0
- package/dist/tools/validation/extractors/javascript.d.ts +73 -0
- package/dist/tools/validation/extractors/javascript.d.ts.map +1 -0
- package/dist/tools/validation/extractors/javascript.js +1841 -0
- package/dist/tools/validation/extractors/javascript.js.map +1 -0
- package/dist/tools/validation/extractors/python.d.ts +93 -0
- package/dist/tools/validation/extractors/python.d.ts.map +1 -0
- package/dist/tools/validation/extractors/python.js +799 -0
- package/dist/tools/validation/extractors/python.js.map +1 -0
- package/dist/tools/validation/manifest.d.ts +45 -0
- package/dist/tools/validation/manifest.d.ts.map +1 -0
- package/dist/tools/validation/manifest.js +719 -0
- package/dist/tools/validation/manifest.js.map +1 -0
- package/dist/tools/validation/parser.d.ts +58 -0
- package/dist/tools/validation/parser.d.ts.map +1 -0
- package/dist/tools/validation/parser.js +232 -0
- package/dist/tools/validation/parser.js.map +1 -0
- package/dist/tools/validation/registry.d.ts +15 -0
- package/dist/tools/validation/registry.d.ts.map +1 -0
- package/dist/tools/validation/registry.js +169 -0
- package/dist/tools/validation/registry.js.map +1 -0
- package/dist/tools/validation/scoring.d.ts +54 -0
- package/dist/tools/validation/scoring.d.ts.map +1 -0
- package/dist/tools/validation/scoring.js +242 -0
- package/dist/tools/validation/scoring.js.map +1 -0
- package/dist/tools/validation/types.d.ts +120 -0
- package/dist/tools/validation/types.d.ts.map +1 -0
- package/dist/tools/validation/types.js +11 -0
- package/dist/tools/validation/types.js.map +1 -0
- package/dist/tools/validation/unusedLocals.d.ts +36 -0
- package/dist/tools/validation/unusedLocals.d.ts.map +1 -0
- package/dist/tools/validation/unusedLocals.js +333 -0
- package/dist/tools/validation/unusedLocals.js.map +1 -0
- package/dist/tools/validation/validation.d.ts +98 -0
- package/dist/tools/validation/validation.d.ts.map +1 -0
- package/dist/tools/validation/validation.js +1837 -0
- package/dist/tools/validation/validation.js.map +1 -0
- package/dist/types/codeGraph.d.ts +163 -0
- package/dist/types/codeGraph.d.ts.map +1 -0
- package/dist/types/codeGraph.js +9 -0
- package/dist/types/codeGraph.js.map +1 -0
- package/dist/types/symbolGraph.d.ts +68 -0
- package/dist/types/symbolGraph.d.ts.map +1 -0
- package/dist/types/symbolGraph.js +10 -0
- package/dist/types/symbolGraph.js.map +1 -0
- package/dist/types/tools.d.ts +43 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +7 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/utils/fileFilter.d.ts +37 -0
- package/dist/utils/fileFilter.d.ts.map +1 -0
- package/dist/utils/fileFilter.js +91 -0
- package/dist/utils/fileFilter.js.map +1 -0
- package/dist/utils/gitUtils.d.ts +28 -0
- package/dist/utils/gitUtils.d.ts.map +1 -0
- package/dist/utils/gitUtils.js +81 -0
- package/dist/utils/gitUtils.js.map +1 -0
- package/dist/utils/logger.d.ts +15 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +38 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/serialization.d.ts +25 -0
- package/dist/utils/serialization.d.ts.map +1 -0
- package/dist/utils/serialization.js +53 -0
- package/dist/utils/serialization.js.map +1 -0
- 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
|