@archpilotlabs/archpilot 0.2.2 → 0.2.3
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/dist/archpilot-cli.js +516 -105
- package/dist/cli/src/archpilot-cli.js +5523 -0
- package/dist/cli/src/archpilot-cli.js.map +1 -0
- package/dist/core/src/adr/adrLoader.js +97 -0
- package/dist/core/src/adr/adrLoader.js.map +1 -0
- package/dist/core/src/adr/adrParser.js +77 -0
- package/dist/core/src/adr/adrParser.js.map +1 -0
- package/dist/core/src/adr/adrTemplate.js +45 -0
- package/dist/core/src/adr/adrTemplate.js.map +1 -0
- package/dist/core/src/adr/adrTypes.js +3 -0
- package/dist/core/src/adr/adrTypes.js.map +1 -0
- package/dist/core/src/adr/adrValidation.js +160 -0
- package/dist/core/src/adr/adrValidation.js.map +1 -0
- package/dist/core/src/adr/index.js +9 -0
- package/dist/core/src/adr/index.js.map +1 -0
- package/dist/core/src/adrManagement.js +174 -0
- package/dist/core/src/adrManagement.js.map +1 -0
- package/dist/core/src/adrRequirementStatus.js +62 -0
- package/dist/core/src/adrRequirementStatus.js.map +1 -0
- package/dist/core/src/adrScaffoldGeneration.js +325 -0
- package/dist/core/src/adrScaffoldGeneration.js.map +1 -0
- package/dist/core/src/apiDesignAnalysis.js +869 -0
- package/dist/core/src/apiDesignAnalysis.js.map +1 -0
- package/dist/core/src/applicationLayeringAnalysis.js +920 -0
- package/dist/core/src/applicationLayeringAnalysis.js.map +1 -0
- package/dist/core/src/applyRulePack.js +117 -0
- package/dist/core/src/applyRulePack.js.map +1 -0
- package/dist/core/src/architectureBaseline.js +271 -0
- package/dist/core/src/architectureBaseline.js.map +1 -0
- package/dist/core/src/architectureComponentDiscoveryDefaults.js +139 -0
- package/dist/core/src/architectureComponentDiscoveryDefaults.js.map +1 -0
- package/dist/core/src/architectureDependencyContext.js +316 -0
- package/dist/core/src/architectureDependencyContext.js.map +1 -0
- package/dist/core/src/architectureDriftDetection.js +96 -0
- package/dist/core/src/architectureDriftDetection.js.map +1 -0
- package/dist/core/src/architectureDriftTimeline.js +221 -0
- package/dist/core/src/architectureDriftTimeline.js.map +1 -0
- package/dist/core/src/architectureGovernanceBaseline.js +469 -0
- package/dist/core/src/architectureGovernanceBaseline.js.map +1 -0
- package/dist/core/src/architectureHealthHistory.js +208 -0
- package/dist/core/src/architectureHealthHistory.js.map +1 -0
- package/dist/core/src/architectureHealthScore.js +657 -0
- package/dist/core/src/architectureHealthScore.js.map +1 -0
- package/dist/core/src/architectureInit.js +826 -0
- package/dist/core/src/architectureInit.js.map +1 -0
- package/dist/core/src/architectureMap.js +727 -0
- package/dist/core/src/architectureMap.js.map +1 -0
- package/dist/core/src/architectureMigrationPreview.js +467 -0
- package/dist/core/src/architectureMigrationPreview.js.map +1 -0
- package/dist/core/src/architectureModelRefresh.js +209 -0
- package/dist/core/src/architectureModelRefresh.js.map +1 -0
- package/dist/core/src/architectureReportWorkflow.js +139 -0
- package/dist/core/src/architectureReportWorkflow.js.map +1 -0
- package/dist/core/src/architectureValidation.js +3879 -0
- package/dist/core/src/architectureValidation.js.map +1 -0
- package/dist/core/src/architectureWorkspaceModel.js +166 -0
- package/dist/core/src/architectureWorkspaceModel.js.map +1 -0
- package/dist/core/src/authorizationTenantAnalysis.js +652 -0
- package/dist/core/src/authorizationTenantAnalysis.js.map +1 -0
- package/dist/core/src/bootstrapArchitectureConfig.js +150 -0
- package/dist/core/src/bootstrapArchitectureConfig.js.map +1 -0
- package/dist/core/src/changeRisk/changeRiskAssessment.js +251 -0
- package/dist/core/src/changeRisk/changeRiskAssessment.js.map +1 -0
- package/dist/core/src/changeRisk/changeRiskTypes.js +3 -0
- package/dist/core/src/changeRisk/changeRiskTypes.js.map +1 -0
- package/dist/core/src/changeRisk/index.js +6 -0
- package/dist/core/src/changeRisk/index.js.map +1 -0
- package/dist/core/src/ci/githubInlineAnnotations.js +245 -0
- package/dist/core/src/ci/githubInlineAnnotations.js.map +1 -0
- package/dist/core/src/ci/githubPrCommentAdapter.js +232 -0
- package/dist/core/src/ci/githubPrCommentAdapter.js.map +1 -0
- package/dist/core/src/ciWorkflowTemplates.js +1042 -0
- package/dist/core/src/ciWorkflowTemplates.js.map +1 -0
- package/dist/core/src/compliance/complianceExportBuilder.js +122 -0
- package/dist/core/src/compliance/complianceExportBuilder.js.map +1 -0
- package/dist/core/src/compliance/complianceExportTypes.js +3 -0
- package/dist/core/src/compliance/complianceExportTypes.js.map +1 -0
- package/dist/core/src/compliance/complianceExportWriter.js +48 -0
- package/dist/core/src/compliance/complianceExportWriter.js.map +1 -0
- package/dist/core/src/compliance/complianceRulePackResolver.js +62 -0
- package/dist/core/src/compliance/complianceRulePackResolver.js.map +1 -0
- package/dist/core/src/config/index.js +17 -0
- package/dist/core/src/config/index.js.map +1 -0
- package/dist/core/src/config/projectConfig.js +224 -0
- package/dist/core/src/config/projectConfig.js.map +1 -0
- package/dist/core/src/config/projectIdentity.js +160 -0
- package/dist/core/src/config/projectIdentity.js.map +1 -0
- package/dist/core/src/dataQueryRiskDetection.js +1203 -0
- package/dist/core/src/dataQueryRiskDetection.js.map +1 -0
- package/dist/core/src/dependencyValidation.js +2557 -0
- package/dist/core/src/dependencyValidation.js.map +1 -0
- package/dist/core/src/discoverAdrs.js +15 -0
- package/dist/core/src/discoverAdrs.js.map +1 -0
- package/dist/core/src/drift/driftComparator.js +49 -0
- package/dist/core/src/drift/driftComparator.js.map +1 -0
- package/dist/core/src/drift/driftSnapshotLoader.js +136 -0
- package/dist/core/src/drift/driftSnapshotLoader.js.map +1 -0
- package/dist/core/src/drift/driftSnapshotTypes.js +3 -0
- package/dist/core/src/drift/driftSnapshotTypes.js.map +1 -0
- package/dist/core/src/drift/driftSnapshotWriter.js +48 -0
- package/dist/core/src/drift/driftSnapshotWriter.js.map +1 -0
- package/dist/core/src/drift/index.js +6 -0
- package/dist/core/src/drift/index.js.map +1 -0
- package/dist/core/src/eventArchitectureAnalysis.js +925 -0
- package/dist/core/src/eventArchitectureAnalysis.js.map +1 -0
- package/dist/core/src/findingTargetResolution.js +117 -0
- package/dist/core/src/findingTargetResolution.js.map +1 -0
- package/dist/core/src/generateArchitectureDiagrams.js +57 -0
- package/dist/core/src/generateArchitectureDiagrams.js.map +1 -0
- package/dist/core/src/generateImpactAnalysis.js +366 -0
- package/dist/core/src/generateImpactAnalysis.js.map +1 -0
- package/dist/core/src/generateOnboardingDocs.js +262 -0
- package/dist/core/src/generateOnboardingDocs.js.map +1 -0
- package/dist/core/src/governance/cloudConnection.js +203 -0
- package/dist/core/src/governance/cloudConnection.js.map +1 -0
- package/dist/core/src/governance/governanceContracts.js +129 -0
- package/dist/core/src/governance/governanceContracts.js.map +1 -0
- package/dist/core/src/governance/governanceGitMetadata.js +78 -0
- package/dist/core/src/governance/governanceGitMetadata.js.map +1 -0
- package/dist/core/src/governance/governanceUploadClient.js +119 -0
- package/dist/core/src/governance/governanceUploadClient.js.map +1 -0
- package/dist/core/src/governance/governanceUploadLimitErrorFormatting.js +155 -0
- package/dist/core/src/governance/governanceUploadLimitErrorFormatting.js.map +1 -0
- package/dist/core/src/governance/governanceUploadPayload.js +439 -0
- package/dist/core/src/governance/governanceUploadPayload.js.map +1 -0
- package/dist/core/src/governance/runGovernanceUpload.js +103 -0
- package/dist/core/src/governance/runGovernanceUpload.js.map +1 -0
- package/dist/core/src/impactAnalysisEngine.js +376 -0
- package/dist/core/src/impactAnalysisEngine.js.map +1 -0
- package/dist/core/src/initializationState.js +99 -0
- package/dist/core/src/initializationState.js.map +1 -0
- package/dist/core/src/managedArtifacts.js +151 -0
- package/dist/core/src/managedArtifacts.js.map +1 -0
- package/dist/core/src/moduleContractValidation.js +266 -0
- package/dist/core/src/moduleContractValidation.js.map +1 -0
- package/dist/core/src/moduleContractsGenerator.js +337 -0
- package/dist/core/src/moduleContractsGenerator.js.map +1 -0
- package/dist/core/src/moduleDiscovery.js +416 -0
- package/dist/core/src/moduleDiscovery.js.map +1 -0
- package/dist/core/src/moduleScope.js +426 -0
- package/dist/core/src/moduleScope.js.map +1 -0
- package/dist/core/src/onboardingGuidance.js +74 -0
- package/dist/core/src/onboardingGuidance.js.map +1 -0
- package/dist/core/src/operationalLogging.js +7 -0
- package/dist/core/src/operationalLogging.js.map +1 -0
- package/dist/core/src/packageVersion.js +52 -0
- package/dist/core/src/packageVersion.js.map +1 -0
- package/dist/core/src/policy/effectivePolicyResolver.js +91 -0
- package/dist/core/src/policy/effectivePolicyResolver.js.map +1 -0
- package/dist/core/src/policy/exceptionLoader.js +285 -0
- package/dist/core/src/policy/exceptionLoader.js.map +1 -0
- package/dist/core/src/policy/remotePolicyLoader.js +227 -0
- package/dist/core/src/policy/remotePolicyLoader.js.map +1 -0
- package/dist/core/src/policy/remotePolicyTypes.js +3 -0
- package/dist/core/src/policy/remotePolicyTypes.js.map +1 -0
- package/dist/core/src/policy/riskPolicyConfig.js +206 -0
- package/dist/core/src/policy/riskPolicyConfig.js.map +1 -0
- package/dist/core/src/policy/riskPolicyEnforcement.js +203 -0
- package/dist/core/src/policy/riskPolicyEnforcement.js.map +1 -0
- package/dist/core/src/policy/riskPolicyExitCode.js +16 -0
- package/dist/core/src/policy/riskPolicyExitCode.js.map +1 -0
- package/dist/core/src/policy/riskPolicyInitializer.js +90 -0
- package/dist/core/src/policy/riskPolicyInitializer.js.map +1 -0
- package/dist/core/src/quickFix/quickFixEngine.js +132 -0
- package/dist/core/src/quickFix/quickFixEngine.js.map +1 -0
- package/dist/core/src/quickFix/quickFixMessaging.js +15 -0
- package/dist/core/src/quickFix/quickFixMessaging.js.map +1 -0
- package/dist/core/src/quickFix/quickFixProviders.js +87 -0
- package/dist/core/src/quickFix/quickFixProviders.js.map +1 -0
- package/dist/core/src/quickFix/quickFixTypes.js +3 -0
- package/dist/core/src/quickFix/quickFixTypes.js.map +1 -0
- package/dist/core/src/remediation/fileHelpers.js +86 -0
- package/dist/core/src/remediation/fileHelpers.js.map +1 -0
- package/dist/core/src/remediation/fixRegistry.js +645 -0
- package/dist/core/src/remediation/fixRegistry.js.map +1 -0
- package/dist/core/src/remediation/index.js +8 -0
- package/dist/core/src/remediation/index.js.map +1 -0
- package/dist/core/src/remediation/remediationEngine.js +207 -0
- package/dist/core/src/remediation/remediationEngine.js.map +1 -0
- package/dist/core/src/reporting/adrReporter.js +59 -0
- package/dist/core/src/reporting/adrReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureBaselineReporter.js +64 -0
- package/dist/core/src/reporting/architectureBaselineReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureDiagramReporter.js +148 -0
- package/dist/core/src/reporting/architectureDiagramReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureDriftTimelineReporter.js +44 -0
- package/dist/core/src/reporting/architectureDriftTimelineReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureHealthHistoryReporter.js +51 -0
- package/dist/core/src/reporting/architectureHealthHistoryReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureMapReporter.js +228 -0
- package/dist/core/src/reporting/architectureMapReporter.js.map +1 -0
- package/dist/core/src/reporting/architecturePrCommentReporter.js +324 -0
- package/dist/core/src/reporting/architecturePrCommentReporter.js.map +1 -0
- package/dist/core/src/reporting/architecturePrReviewReporter.js +389 -0
- package/dist/core/src/reporting/architecturePrReviewReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureReportArtifacts.js +166 -0
- package/dist/core/src/reporting/architectureReportArtifacts.js.map +1 -0
- package/dist/core/src/reporting/architectureReviewReportGenerator.js +382 -0
- package/dist/core/src/reporting/architectureReviewReportGenerator.js.map +1 -0
- package/dist/core/src/reporting/architectureReviewReporter.js +1633 -0
- package/dist/core/src/reporting/architectureReviewReporter.js.map +1 -0
- package/dist/core/src/reporting/architectureRiskAssessment.js +101 -0
- package/dist/core/src/reporting/architectureRiskAssessment.js.map +1 -0
- package/dist/core/src/reporting/architectureScoreBadge.js +177 -0
- package/dist/core/src/reporting/architectureScoreBadge.js.map +1 -0
- package/dist/core/src/reporting/architectureSupplementalReporters.js +124 -0
- package/dist/core/src/reporting/architectureSupplementalReporters.js.map +1 -0
- package/dist/core/src/reporting/changeRiskAssessment.js +6 -0
- package/dist/core/src/reporting/changeRiskAssessment.js.map +1 -0
- package/dist/core/src/reporting/changeRiskTypes.js +3 -0
- package/dist/core/src/reporting/changeRiskTypes.js.map +1 -0
- package/dist/core/src/reporting/impactAnalysisReporter.js +111 -0
- package/dist/core/src/reporting/impactAnalysisReporter.js.map +1 -0
- package/dist/core/src/reporting/onboardingDocsReporter.js +113 -0
- package/dist/core/src/reporting/onboardingDocsReporter.js.map +1 -0
- package/dist/core/src/reporting/rulePackReporter.js +61 -0
- package/dist/core/src/reporting/rulePackReporter.js.map +1 -0
- package/dist/core/src/reporting/safetyControlsDetection.js +343 -0
- package/dist/core/src/reporting/safetyControlsDetection.js.map +1 -0
- package/dist/core/src/resolveRuleAdrs.js +31 -0
- package/dist/core/src/resolveRuleAdrs.js.map +1 -0
- package/dist/core/src/reviewGenerationWorkflow.js +220 -0
- package/dist/core/src/reviewGenerationWorkflow.js.map +1 -0
- package/dist/core/src/rulePacks/index.js +17 -0
- package/dist/core/src/rulePacks/index.js.map +1 -0
- package/dist/core/src/rulePacks/layeredMonolithRulePack.js +45 -0
- package/dist/core/src/rulePacks/layeredMonolithRulePack.js.map +1 -0
- package/dist/core/src/rulePacks/modularMonolithRulePack.js +41 -0
- package/dist/core/src/rulePacks/modularMonolithRulePack.js.map +1 -0
- package/dist/core/src/rulePacks/types.js +3 -0
- package/dist/core/src/rulePacks/types.js.map +1 -0
- package/dist/core/src/setupNextSteps.js +939 -0
- package/dist/core/src/setupNextSteps.js.map +1 -0
- package/dist/core/src/setupRefresh.js +152 -0
- package/dist/core/src/setupRefresh.js.map +1 -0
- package/dist/core/src/smartInit/apiDbHintDetection.js +836 -0
- package/dist/core/src/smartInit/apiDbHintDetection.js.map +1 -0
- package/dist/core/src/smartInit/architectureProposalBuilder.js +1215 -0
- package/dist/core/src/smartInit/architectureProposalBuilder.js.map +1 -0
- package/dist/core/src/smartInit/architectureStyleDetection.js +472 -0
- package/dist/core/src/smartInit/architectureStyleDetection.js.map +1 -0
- package/dist/core/src/smartInit/archpilotIgnore.js +139 -0
- package/dist/core/src/smartInit/archpilotIgnore.js.map +1 -0
- package/dist/core/src/smartInit/componentDetection.js +1084 -0
- package/dist/core/src/smartInit/componentDetection.js.map +1 -0
- package/dist/core/src/smartInit/implementationProfileResolution.js +53 -0
- package/dist/core/src/smartInit/implementationProfileResolution.js.map +1 -0
- package/dist/core/src/smartInit/index.js +31 -0
- package/dist/core/src/smartInit/index.js.map +1 -0
- package/dist/core/src/smartInit/moduleDetection.js +982 -0
- package/dist/core/src/smartInit/moduleDetection.js.map +1 -0
- package/dist/core/src/smartInit/projectKindDetection.js +437 -0
- package/dist/core/src/smartInit/projectKindDetection.js.map +1 -0
- package/dist/core/src/smartInit/repoTopologyDetection.js +470 -0
- package/dist/core/src/smartInit/repoTopologyDetection.js.map +1 -0
- package/dist/core/src/smartInit/runContext.js +112 -0
- package/dist/core/src/smartInit/runContext.js.map +1 -0
- package/dist/core/src/smartInit/smartInitEngine.js +94 -0
- package/dist/core/src/smartInit/smartInitEngine.js.map +1 -0
- package/dist/core/src/smartInit/stackDetection.js +503 -0
- package/dist/core/src/smartInit/stackDetection.js.map +1 -0
- package/dist/core/src/smartInit/toolingHintDetection.js +345 -0
- package/dist/core/src/smartInit/toolingHintDetection.js.map +1 -0
- package/dist/core/src/sourceScopeClassification.js +83 -0
- package/dist/core/src/sourceScopeClassification.js.map +1 -0
- package/dist/core/src/stackAdapters.js +2475 -0
- package/dist/core/src/stackAdapters.js.map +1 -0
- package/dist/core/src/stackMetadata.js +157 -0
- package/dist/core/src/stackMetadata.js.map +1 -0
- package/dist/core/src/suppressions/index.js +3 -0
- package/dist/core/src/suppressions/index.js.map +1 -0
- package/dist/core/src/suppressions/suppressionReporter.js +38 -0
- package/dist/core/src/suppressions/suppressionReporter.js.map +1 -0
- package/dist/core/src/suppressions/suppressionTypes.js +3 -0
- package/dist/core/src/suppressions/suppressionTypes.js.map +1 -0
- package/dist/core/src/suppressions/suppressionValidator.js +119 -0
- package/dist/core/src/suppressions/suppressionValidator.js.map +1 -0
- package/dist/core/src/transactionDomainAnalysis.js +1416 -0
- package/dist/core/src/transactionDomainAnalysis.js.map +1 -0
- package/dist/core/src/validation/changedFilesResolver.js +165 -0
- package/dist/core/src/validation/changedFilesResolver.js.map +1 -0
- package/dist/core/src/validationProfiling.js +33 -0
- package/dist/core/src/validationProfiling.js.map +1 -0
- package/dist/core/src/validationRuleExplanations.js +69 -0
- package/dist/core/src/validationRuleExplanations.js.map +1 -0
- package/dist/core/src/validationSourceInventory.js +200 -0
- package/dist/core/src/validationSourceInventory.js.map +1 -0
- package/dist/core/src/validationSuppressions.js +231 -0
- package/dist/core/src/validationSuppressions.js.map +1 -0
- package/dist/core/src/validationTypeScriptAstCache.js +82 -0
- package/dist/core/src/validationTypeScriptAstCache.js.map +1 -0
- package/dist/core/src/validationWorkflow.js +222 -0
- package/dist/core/src/validationWorkflow.js.map +1 -0
- package/dist/core/src/workspaceRoot.js +68 -0
- package/dist/core/src/workspaceRoot.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.detectApiDesignFindings = detectApiDesignFindings;
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const node_fs_1 = require("node:fs");
|
|
39
|
+
const moduleDiscovery_1 = require("./moduleDiscovery");
|
|
40
|
+
const archpilotIgnore_1 = require("./smartInit/archpilotIgnore");
|
|
41
|
+
const sourceScopeClassification_1 = require("./sourceScopeClassification");
|
|
42
|
+
const validationProfiling_1 = require("./validationProfiling");
|
|
43
|
+
const RuleIds = {
|
|
44
|
+
MISSING_PAGINATION: 'AP-API-010',
|
|
45
|
+
BULK_OPERATION_RISK: 'AP-API-011',
|
|
46
|
+
DTO_ENTITY_LEAKAGE: 'AP-API-012',
|
|
47
|
+
INCONSISTENT_RESOURCE_DESIGN: 'AP-API-013',
|
|
48
|
+
VERSIONING_INCONSISTENCY: 'AP-API-014',
|
|
49
|
+
CONTRACT_IMPLEMENTATION_DRIFT: 'AP-API-015',
|
|
50
|
+
};
|
|
51
|
+
const maxFileSizeBytes = 384 * 1024;
|
|
52
|
+
const supportedExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.java', '.cs', '.py', '.php', '.go']);
|
|
53
|
+
const ignoredDirectoryNames = new Set([
|
|
54
|
+
'.git',
|
|
55
|
+
'.archpilot',
|
|
56
|
+
'.next',
|
|
57
|
+
'.vscode-test',
|
|
58
|
+
'node_modules',
|
|
59
|
+
'dist',
|
|
60
|
+
'build',
|
|
61
|
+
'out',
|
|
62
|
+
'coverage',
|
|
63
|
+
'test',
|
|
64
|
+
'tests',
|
|
65
|
+
'__tests__',
|
|
66
|
+
'fixtures',
|
|
67
|
+
'smoke',
|
|
68
|
+
'examples',
|
|
69
|
+
'docs',
|
|
70
|
+
'reports',
|
|
71
|
+
'generated',
|
|
72
|
+
]);
|
|
73
|
+
const httpMethods = new Set(['get', 'post', 'put', 'patch', 'delete']);
|
|
74
|
+
const nonBusinessEndpointPattern = /\b(health|ready|live|status|metrics|docs|swagger|openapi|login|logout|callback|oauth|auth\/callback)\b/iu;
|
|
75
|
+
const paginationPattern = /\b(page|limit|offset|cursor|take|skip|size|Pageable|Pagination|batchSize|top)\b/u;
|
|
76
|
+
const authEvidencePattern = /@(?:UseGuards|Roles|Permissions|RequirePermission|PreAuthorize|Secured|RolesAllowed)\b|\b(?:AuthGuard|RolesGuard|PermissionGuard|admin|ADMIN)\b/u;
|
|
77
|
+
const safeguardPattern = /\b(?:dryRun|confirm|confirmation|batchSize|limit|reason|audit|queue|job|enqueue|FeatureFlag|featureFlag|staged)\b/u;
|
|
78
|
+
const mapperPattern = /\b(?:mapTo|toDto|toResponse|serializer|serialize|plainToInstance|mapper)\b/u;
|
|
79
|
+
const entityTypePattern = /\b\w*(?:Entity|Model|Document)\b/u;
|
|
80
|
+
const dtoTypePattern = /\b\w*(?:Dto|DTO|Response|View|Resource|Contract)\b/u;
|
|
81
|
+
const routeStringPattern = /['"`]([^'"`]*)['"`]/u;
|
|
82
|
+
function normalizePath(value) {
|
|
83
|
+
return value.replace(/\\/g, '/');
|
|
84
|
+
}
|
|
85
|
+
function normalizeRelativePath(value) {
|
|
86
|
+
return normalizePath(value).replace(/^\/+/, '');
|
|
87
|
+
}
|
|
88
|
+
function toPosixRelative(workspaceRoot, absolutePath) {
|
|
89
|
+
return normalizePath(path.relative(workspaceRoot, absolutePath));
|
|
90
|
+
}
|
|
91
|
+
async function pathExists(targetPath) {
|
|
92
|
+
try {
|
|
93
|
+
await node_fs_1.promises.access(targetPath);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function readTextFileIfReasonable(filePath, sourceInventory) {
|
|
101
|
+
if (sourceInventory) {
|
|
102
|
+
return sourceInventory.readText(filePath, { maxFileSizeBytes });
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const stats = await node_fs_1.promises.stat(filePath);
|
|
106
|
+
if (stats.size > maxFileSizeBytes) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return await node_fs_1.promises.readFile(filePath, { encoding: 'utf8' });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function collectSourceFiles(workspaceRoot, sourceInventory) {
|
|
116
|
+
if (sourceInventory) {
|
|
117
|
+
const files = await sourceInventory.getTextFiles({
|
|
118
|
+
extensions: [...supportedExtensions],
|
|
119
|
+
ignoredDirectoryNames: [...ignoredDirectoryNames],
|
|
120
|
+
maxFileSizeBytes,
|
|
121
|
+
});
|
|
122
|
+
return files.map((file) => ({
|
|
123
|
+
relativePath: file.relativePath,
|
|
124
|
+
content: file.content,
|
|
125
|
+
extension: file.extension,
|
|
126
|
+
})).filter((file) => (0, sourceScopeClassification_1.isProductionBackendGovernanceScope)((0, sourceScopeClassification_1.classifyValidationSourceScope)(file.relativePath)));
|
|
127
|
+
}
|
|
128
|
+
const ignoreMatcher = (0, archpilotIgnore_1.loadArchpilotIgnoreMatcher)(workspaceRoot);
|
|
129
|
+
const collected = [];
|
|
130
|
+
const stack = [workspaceRoot];
|
|
131
|
+
while (stack.length > 0) {
|
|
132
|
+
const current = stack.pop();
|
|
133
|
+
if (!current) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
let entries;
|
|
137
|
+
try {
|
|
138
|
+
entries = await node_fs_1.promises.readdir(current, { withFileTypes: true });
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const absoluteEntryPath = path.join(current, entry.name);
|
|
145
|
+
const relativeEntryPath = toPosixRelative(workspaceRoot, absoluteEntryPath);
|
|
146
|
+
if (ignoreMatcher.isIgnored(relativeEntryPath)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
if (entry.name.startsWith('.') || ignoredDirectoryNames.has(entry.name)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
stack.push(absoluteEntryPath);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
157
|
+
if (!supportedExtensions.has(extension)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!(0, sourceScopeClassification_1.isProductionBackendGovernanceScope)((0, sourceScopeClassification_1.classifyValidationSourceScope)(relativeEntryPath))) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const content = await readTextFileIfReasonable(absoluteEntryPath, sourceInventory);
|
|
164
|
+
if (content === undefined) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
collected.push({ relativePath: relativeEntryPath, content, extension });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return collected.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
171
|
+
}
|
|
172
|
+
function lineNumberForOffset(content, offset) {
|
|
173
|
+
let line = 1;
|
|
174
|
+
for (let index = 0; index < offset && index < content.length; index += 1) {
|
|
175
|
+
if (content[index] === '\n') {
|
|
176
|
+
line += 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return line;
|
|
180
|
+
}
|
|
181
|
+
function findMatchingBrace(content, openBraceIndex) {
|
|
182
|
+
let depth = 0;
|
|
183
|
+
for (let index = openBraceIndex; index < content.length; index += 1) {
|
|
184
|
+
if (content[index] === '{') {
|
|
185
|
+
depth += 1;
|
|
186
|
+
}
|
|
187
|
+
if (content[index] === '}') {
|
|
188
|
+
depth -= 1;
|
|
189
|
+
if (depth === 0) {
|
|
190
|
+
return index;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return Math.min(content.length - 1, openBraceIndex + 5000);
|
|
195
|
+
}
|
|
196
|
+
function findMatchingParen(content, openParenIndex) {
|
|
197
|
+
let depth = 0;
|
|
198
|
+
for (let index = openParenIndex; index < content.length; index += 1) {
|
|
199
|
+
if (content[index] === '(') {
|
|
200
|
+
depth += 1;
|
|
201
|
+
}
|
|
202
|
+
if (content[index] === ')') {
|
|
203
|
+
depth -= 1;
|
|
204
|
+
if (depth === 0) {
|
|
205
|
+
return index;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return Math.min(content.length - 1, openParenIndex + 1000);
|
|
210
|
+
}
|
|
211
|
+
function findPythonBlockEnd(content, blockStart) {
|
|
212
|
+
const lineStart = content.lastIndexOf('\n', blockStart) + 1;
|
|
213
|
+
const lineEnd = content.indexOf('\n', blockStart);
|
|
214
|
+
const line = content.slice(lineStart, lineEnd === -1 ? content.length : lineEnd);
|
|
215
|
+
const baseIndent = /^\s*/u.exec(line)?.[0].length ?? 0;
|
|
216
|
+
let offset = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
217
|
+
while (offset < content.length) {
|
|
218
|
+
const nextLineEnd = content.indexOf('\n', offset);
|
|
219
|
+
const nextEnd = nextLineEnd === -1 ? content.length : nextLineEnd;
|
|
220
|
+
const nextLine = content.slice(offset, nextEnd);
|
|
221
|
+
if (nextLine.trim().length > 0) {
|
|
222
|
+
const indent = /^\s*/u.exec(nextLine)?.[0].length ?? 0;
|
|
223
|
+
if (indent <= baseIndent) {
|
|
224
|
+
return offset;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (nextLineEnd === -1) {
|
|
228
|
+
return content.length;
|
|
229
|
+
}
|
|
230
|
+
offset = nextLineEnd + 1;
|
|
231
|
+
}
|
|
232
|
+
return content.length;
|
|
233
|
+
}
|
|
234
|
+
function inferModuleForPath(contract, relativePath) {
|
|
235
|
+
const normalized = normalizePath(relativePath);
|
|
236
|
+
const modulesRoot = (0, moduleDiscovery_1.resolveModulesRootFromContract)(contract);
|
|
237
|
+
for (const [moduleName, reference] of Object.entries(contract.modules ?? {})) {
|
|
238
|
+
const modulePath = normalizePath(reference.path ?? `${modulesRoot}/${moduleName}`);
|
|
239
|
+
if (normalized === modulePath || normalized.startsWith(`${modulePath}/`)) {
|
|
240
|
+
return moduleName;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
function resolveOpenApiRelativePath(contract) {
|
|
246
|
+
if (typeof contract.artifacts?.openApiSpec === 'string' && contract.artifacts.openApiSpec.trim().length > 0) {
|
|
247
|
+
return normalizeRelativePath(contract.artifacts.openApiSpec);
|
|
248
|
+
}
|
|
249
|
+
return `${(0, moduleDiscovery_1.resolveApiRootFromContract)(contract)}/openapi.yaml`;
|
|
250
|
+
}
|
|
251
|
+
function stringLiteralFromDecorator(value) {
|
|
252
|
+
const match = routeStringPattern.exec(value);
|
|
253
|
+
return match?.[1] ?? '';
|
|
254
|
+
}
|
|
255
|
+
function stringLiteralFromRouteCall(value) {
|
|
256
|
+
const match = routeStringPattern.exec(value);
|
|
257
|
+
return match?.[1] ?? '';
|
|
258
|
+
}
|
|
259
|
+
function joinEndpointPath(basePath, methodPath) {
|
|
260
|
+
const joined = `/${[basePath, methodPath].filter((part) => part.trim().length > 0).join('/')}`;
|
|
261
|
+
return joined.replace(/\/+/gu, '/').replace(/\/$/u, '') || '/';
|
|
262
|
+
}
|
|
263
|
+
function extractControllerBasePath(file) {
|
|
264
|
+
if (file.extension === '.java') {
|
|
265
|
+
const match = /@RequestMapping\s*\(([^)]*)\)/u.exec(file.content);
|
|
266
|
+
return match ? stringLiteralFromDecorator(match[1]) : '';
|
|
267
|
+
}
|
|
268
|
+
const match = /@Controller\s*\(([^)]*)\)/u.exec(file.content);
|
|
269
|
+
return match ? stringLiteralFromDecorator(match[1]) : '';
|
|
270
|
+
}
|
|
271
|
+
function httpMethodFromDecorator(decorators, extension) {
|
|
272
|
+
const pattern = extension === '.java'
|
|
273
|
+
? /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\b/u
|
|
274
|
+
: /@(Get|Post|Put|Patch|Delete)\b/u;
|
|
275
|
+
const match = pattern.exec(decorators);
|
|
276
|
+
if (!match?.[1]) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
const value = match[1].replace('Mapping', '').toLowerCase();
|
|
280
|
+
if (value === 'request') {
|
|
281
|
+
const methodMatch = /method\s*=\s*RequestMethod\.(GET|POST|PUT|PATCH|DELETE)/u.exec(decorators);
|
|
282
|
+
return methodMatch?.[1]?.toLowerCase();
|
|
283
|
+
}
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
function methodPathFromDecorators(decorators, extension) {
|
|
287
|
+
const pattern = extension === '.java'
|
|
288
|
+
? /@(?:GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*(?:\(([^)]*)\))?/u
|
|
289
|
+
: /@(?:Get|Post|Put|Patch|Delete)\s*(?:\(([^)]*)\))?/u;
|
|
290
|
+
const match = pattern.exec(decorators);
|
|
291
|
+
return match?.[1] ? stringLiteralFromDecorator(match[1]) : '';
|
|
292
|
+
}
|
|
293
|
+
function extractDecoratorEndpoints(file, contract) {
|
|
294
|
+
const endpoints = [];
|
|
295
|
+
const basePath = extractControllerBasePath(file);
|
|
296
|
+
const pattern = file.extension === '.java'
|
|
297
|
+
? /((?:\s*@(?:GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping|PreAuthorize|Secured|RolesAllowed)\b[^\n]*\n)+)\s*(public|private|protected)\s+([\w<>, ?\[\]]+)\s+(\w+)\s*\([^)]*\)\s*\{/gu
|
|
298
|
+
: /((?:\s*@(?:Get|Post|Put|Patch|Delete|UseGuards|Roles|Permissions|RequirePermission)\b[^\n]*\n)+)\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*([^{]+))?\s*\{/gu;
|
|
299
|
+
for (const match of file.content.matchAll(pattern)) {
|
|
300
|
+
const decorators = match[1] ?? '';
|
|
301
|
+
const httpMethod = httpMethodFromDecorator(decorators, file.extension);
|
|
302
|
+
if (!httpMethod) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const name = file.extension === '.java' ? match[4] ?? 'handler' : match[2] ?? 'handler';
|
|
306
|
+
const returnType = file.extension === '.java' ? match[3] ?? '' : match[3] ?? '';
|
|
307
|
+
const openBrace = (match.index ?? 0) + match[0].lastIndexOf('{');
|
|
308
|
+
const end = findMatchingBrace(file.content, openBrace);
|
|
309
|
+
endpoints.push({
|
|
310
|
+
httpMethod,
|
|
311
|
+
path: joinEndpointPath(basePath, methodPathFromDecorators(decorators, file.extension)),
|
|
312
|
+
name,
|
|
313
|
+
line: lineNumberForOffset(file.content, match.index ?? 0),
|
|
314
|
+
decorators,
|
|
315
|
+
signature: `${returnType} ${match[0]}`,
|
|
316
|
+
body: file.content.slice(openBrace, end + 1),
|
|
317
|
+
file,
|
|
318
|
+
moduleName: inferModuleForPath(contract, file.relativePath),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return endpoints;
|
|
322
|
+
}
|
|
323
|
+
function extractFastApiEndpoints(file, contract) {
|
|
324
|
+
const endpoints = [];
|
|
325
|
+
const pattern = /((?:\s*@(?:app|router)\.(?:get|post|put|patch|delete)\s*\([^)]*\)\s*\n)+)\s*(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)\s*:/gu;
|
|
326
|
+
for (const match of file.content.matchAll(pattern)) {
|
|
327
|
+
const decorators = match[1] ?? '';
|
|
328
|
+
const methodMatch = /@(?:app|router)\.(get|post|put|patch|delete)\s*\(([^)]*)\)/u.exec(decorators);
|
|
329
|
+
if (!methodMatch?.[1]) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const blockStart = (match.index ?? 0) + match[0].length;
|
|
333
|
+
const end = findPythonBlockEnd(file.content, blockStart);
|
|
334
|
+
endpoints.push({
|
|
335
|
+
httpMethod: methodMatch[1].toLowerCase(),
|
|
336
|
+
path: joinEndpointPath('', stringLiteralFromRouteCall(methodMatch[2] ?? '')),
|
|
337
|
+
name: match[2] ?? 'handler',
|
|
338
|
+
line: lineNumberForOffset(file.content, match.index ?? 0),
|
|
339
|
+
decorators,
|
|
340
|
+
signature: match[3] ?? '',
|
|
341
|
+
body: file.content.slice(blockStart, end),
|
|
342
|
+
file,
|
|
343
|
+
moduleName: inferModuleForPath(contract, file.relativePath),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return endpoints;
|
|
347
|
+
}
|
|
348
|
+
function extractAspNetControllerBasePath(content, classOffset, controllerName) {
|
|
349
|
+
const prefix = content.slice(Math.max(0, classOffset - 600), classOffset);
|
|
350
|
+
const routeMatches = [...prefix.matchAll(/\[Route\s*\(([^)]*)\)\]/gu)];
|
|
351
|
+
const routeValue = routeMatches.at(-1)?.[1];
|
|
352
|
+
if (!routeValue) {
|
|
353
|
+
return '';
|
|
354
|
+
}
|
|
355
|
+
return stringLiteralFromDecorator(routeValue).replace(/\[controller\]/giu, controllerName);
|
|
356
|
+
}
|
|
357
|
+
function extractAspNetEndpoints(file, contract) {
|
|
358
|
+
const endpoints = [];
|
|
359
|
+
const classPattern = /class\s+(\w+Controller)\b[^{]*\{/gu;
|
|
360
|
+
for (const classMatch of file.content.matchAll(classPattern)) {
|
|
361
|
+
const classStart = classMatch.index ?? 0;
|
|
362
|
+
const classOpenBrace = classStart + classMatch[0].lastIndexOf('{');
|
|
363
|
+
const classEnd = findMatchingBrace(file.content, classOpenBrace);
|
|
364
|
+
const classBody = file.content.slice(classOpenBrace + 1, classEnd);
|
|
365
|
+
const controllerName = (classMatch[1] ?? '').replace(/Controller$/u, '').toLowerCase();
|
|
366
|
+
const basePath = extractAspNetControllerBasePath(file.content, classStart, controllerName).replace(/\/$/u, '') || controllerName;
|
|
367
|
+
const methodPattern = /((?:\s*\[(?:HttpGet|HttpPost|HttpPut|HttpPatch|HttpDelete|Route|Authorize|AllowAnonymous)[^\]]*\]\s*)+)\s*(?:public|private|protected|internal)\s+(?:async\s+)?[\w<>, ?\[\].]+\s+(\w+)\s*\(([^)]*)\)\s*\{/gu;
|
|
368
|
+
for (const methodMatch of classBody.matchAll(methodPattern)) {
|
|
369
|
+
const attributes = methodMatch[1] ?? '';
|
|
370
|
+
const httpMatch = /\[(HttpGet|HttpPost|HttpPut|HttpPatch|HttpDelete)\s*(?:\(([^)]*)\))?\]/u.exec(attributes);
|
|
371
|
+
if (!httpMatch?.[1]) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const openBrace = classOpenBrace + 1 + (methodMatch.index ?? 0) + methodMatch[0].lastIndexOf('{');
|
|
375
|
+
const end = findMatchingBrace(file.content, openBrace);
|
|
376
|
+
endpoints.push({
|
|
377
|
+
httpMethod: httpMatch[1].replace('Http', '').toLowerCase(),
|
|
378
|
+
path: joinEndpointPath(basePath, httpMatch[2] ? stringLiteralFromDecorator(httpMatch[2]) : ''),
|
|
379
|
+
name: methodMatch[2] ?? 'handler',
|
|
380
|
+
line: lineNumberForOffset(file.content, classOpenBrace + 1 + (methodMatch.index ?? 0)),
|
|
381
|
+
decorators: attributes,
|
|
382
|
+
signature: methodMatch[3] ?? '',
|
|
383
|
+
body: file.content.slice(openBrace, end + 1),
|
|
384
|
+
file,
|
|
385
|
+
moduleName: inferModuleForPath(contract, file.relativePath),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return endpoints;
|
|
390
|
+
}
|
|
391
|
+
function extractLaravelEndpoints(file, contract) {
|
|
392
|
+
const endpoints = [];
|
|
393
|
+
const pattern = /\bRoute::(get|post|put|patch|delete)\s*\(/gu;
|
|
394
|
+
for (const match of file.content.matchAll(pattern)) {
|
|
395
|
+
const openParen = (match.index ?? 0) + match[0].lastIndexOf('(');
|
|
396
|
+
const endParen = findMatchingParen(file.content, openParen);
|
|
397
|
+
const args = file.content.slice(openParen + 1, endParen);
|
|
398
|
+
const routePath = stringLiteralFromRouteCall(args);
|
|
399
|
+
if (!routePath) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const body = args.includes('function') ? args : match[0];
|
|
403
|
+
endpoints.push({
|
|
404
|
+
httpMethod: (match[1] ?? 'get').toLowerCase(),
|
|
405
|
+
path: joinEndpointPath('', routePath),
|
|
406
|
+
name: routePath,
|
|
407
|
+
line: lineNumberForOffset(file.content, match.index ?? 0),
|
|
408
|
+
decorators: file.content.slice(match.index ?? 0, Math.min(file.content.length, endParen + 1)),
|
|
409
|
+
signature: args,
|
|
410
|
+
body,
|
|
411
|
+
file,
|
|
412
|
+
moduleName: inferModuleForPath(contract, file.relativePath),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const resourcePattern = /\bRoute::(?:apiResource|resource)\s*\(/gu;
|
|
416
|
+
for (const match of file.content.matchAll(resourcePattern)) {
|
|
417
|
+
const openParen = (match.index ?? 0) + match[0].lastIndexOf('(');
|
|
418
|
+
const endParen = findMatchingParen(file.content, openParen);
|
|
419
|
+
const args = file.content.slice(openParen + 1, endParen);
|
|
420
|
+
const routePath = stringLiteralFromRouteCall(args);
|
|
421
|
+
if (!routePath) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
endpoints.push({
|
|
425
|
+
httpMethod: 'get',
|
|
426
|
+
path: joinEndpointPath('', routePath),
|
|
427
|
+
name: routePath,
|
|
428
|
+
line: lineNumberForOffset(file.content, match.index ?? 0),
|
|
429
|
+
decorators: file.content.slice(match.index ?? 0, Math.min(file.content.length, endParen + 1)),
|
|
430
|
+
signature: args,
|
|
431
|
+
body: args,
|
|
432
|
+
file,
|
|
433
|
+
moduleName: inferModuleForPath(contract, file.relativePath),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return endpoints;
|
|
437
|
+
}
|
|
438
|
+
function extractGoEndpoints(file, contract) {
|
|
439
|
+
const endpoints = [];
|
|
440
|
+
const pattern = /\b(?:\w+\.)?(GET|Get|POST|Post|PUT|Put|PATCH|Patch|DELETE|Delete)\s*\(/gu;
|
|
441
|
+
for (const match of file.content.matchAll(pattern)) {
|
|
442
|
+
const openParen = (match.index ?? 0) + match[0].lastIndexOf('(');
|
|
443
|
+
const endParen = findMatchingParen(file.content, openParen);
|
|
444
|
+
const callBody = file.content.slice(openParen + 1, endParen);
|
|
445
|
+
const call = file.content.slice(match.index ?? 0, Math.min(file.content.length, endParen + 1));
|
|
446
|
+
const method = (match[1] ?? 'get').toLowerCase();
|
|
447
|
+
const routePath = stringLiteralFromRouteCall(callBody);
|
|
448
|
+
if (!routePath) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
endpoints.push({
|
|
452
|
+
httpMethod: method,
|
|
453
|
+
path: joinEndpointPath('', routePath),
|
|
454
|
+
name: routePath,
|
|
455
|
+
line: lineNumberForOffset(file.content, match.index ?? 0),
|
|
456
|
+
decorators: call,
|
|
457
|
+
signature: callBody,
|
|
458
|
+
body: callBody,
|
|
459
|
+
file,
|
|
460
|
+
moduleName: inferModuleForPath(contract, file.relativePath),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return endpoints;
|
|
464
|
+
}
|
|
465
|
+
function extractExpressEndpoints(file, contract) {
|
|
466
|
+
const endpoints = [];
|
|
467
|
+
const pattern = /\b(?:app|router)\.(get|post|put|patch|delete)\s*\(/gu;
|
|
468
|
+
for (const match of file.content.matchAll(pattern)) {
|
|
469
|
+
const openParen = (match.index ?? 0) + match[0].lastIndexOf('(');
|
|
470
|
+
const endParen = findMatchingParen(file.content, openParen);
|
|
471
|
+
const callBody = file.content.slice(openParen + 1, endParen);
|
|
472
|
+
const routePath = stringLiteralFromRouteCall(callBody);
|
|
473
|
+
if (!routePath) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
endpoints.push({
|
|
477
|
+
httpMethod: (match[1] ?? 'get').toLowerCase(),
|
|
478
|
+
path: joinEndpointPath('', routePath),
|
|
479
|
+
name: routePath,
|
|
480
|
+
line: lineNumberForOffset(file.content, match.index ?? 0),
|
|
481
|
+
decorators: file.content.slice(match.index ?? 0, Math.min(file.content.length, endParen + 1)),
|
|
482
|
+
signature: callBody,
|
|
483
|
+
body: callBody,
|
|
484
|
+
file,
|
|
485
|
+
moduleName: inferModuleForPath(contract, file.relativePath),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return endpoints;
|
|
489
|
+
}
|
|
490
|
+
function extractEndpoints(file, contract) {
|
|
491
|
+
if (file.extension === '.py') {
|
|
492
|
+
return extractFastApiEndpoints(file, contract);
|
|
493
|
+
}
|
|
494
|
+
if (file.extension === '.cs') {
|
|
495
|
+
return extractAspNetEndpoints(file, contract);
|
|
496
|
+
}
|
|
497
|
+
if (file.extension === '.php') {
|
|
498
|
+
return extractLaravelEndpoints(file, contract);
|
|
499
|
+
}
|
|
500
|
+
if (file.extension === '.go') {
|
|
501
|
+
return extractGoEndpoints(file, contract);
|
|
502
|
+
}
|
|
503
|
+
if (['.js', '.jsx', '.mjs', '.cjs'].includes(file.extension)) {
|
|
504
|
+
return extractExpressEndpoints(file, contract);
|
|
505
|
+
}
|
|
506
|
+
return [...extractDecoratorEndpoints(file, contract), ...extractExpressEndpoints(file, contract)];
|
|
507
|
+
}
|
|
508
|
+
function createFinding(input) {
|
|
509
|
+
return {
|
|
510
|
+
id: input.id,
|
|
511
|
+
severity: 'warning',
|
|
512
|
+
passed: false,
|
|
513
|
+
message: input.message,
|
|
514
|
+
findingType: 'api-scope',
|
|
515
|
+
filePath: input.endpoint.file.relativePath,
|
|
516
|
+
sourceFile: input.endpoint.file.relativePath,
|
|
517
|
+
sourceLine: input.endpoint.line,
|
|
518
|
+
api: `${input.endpoint.httpMethod.toUpperCase()} ${input.endpoint.path}`,
|
|
519
|
+
target: input.target ?? input.endpoint.name,
|
|
520
|
+
...(input.endpoint.moduleName ? { module: input.endpoint.moduleName } : {}),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function createContractFinding(input) {
|
|
524
|
+
return {
|
|
525
|
+
id: RuleIds.CONTRACT_IMPLEMENTATION_DRIFT,
|
|
526
|
+
severity: 'warning',
|
|
527
|
+
passed: false,
|
|
528
|
+
message: input.message,
|
|
529
|
+
findingType: 'api-scope',
|
|
530
|
+
filePath: input.filePath,
|
|
531
|
+
sourceFile: input.filePath,
|
|
532
|
+
api: input.api,
|
|
533
|
+
target: input.target,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function normalizeApiPath(value) {
|
|
537
|
+
let normalized = value.trim().replace(/\\/gu, '/').replace(/\/+/gu, '/').replace(/\/$/u, '');
|
|
538
|
+
if (!normalized.startsWith('/')) {
|
|
539
|
+
normalized = `/${normalized}`;
|
|
540
|
+
}
|
|
541
|
+
normalized = normalized.replace(/^\/api(?=\/)/u, '');
|
|
542
|
+
normalized = normalized
|
|
543
|
+
.replace(/:([A-Za-z0-9_]+)/gu, '{$1}')
|
|
544
|
+
.replace(/<([A-Za-z0-9_]+)>/gu, '{$1}')
|
|
545
|
+
.replace(/\[([A-Za-z0-9_]+)\]/gu, '{$1}')
|
|
546
|
+
.replace(/\{[^/}]+\}/gu, '{}');
|
|
547
|
+
return normalized || '/';
|
|
548
|
+
}
|
|
549
|
+
function contractKey(method, apiPath) {
|
|
550
|
+
return `${method.toLowerCase()} ${normalizeApiPath(apiPath)}`;
|
|
551
|
+
}
|
|
552
|
+
function isSkippedEndpoint(endpoint) {
|
|
553
|
+
return nonBusinessEndpointPattern.test(endpoint.path) || nonBusinessEndpointPattern.test(endpoint.name);
|
|
554
|
+
}
|
|
555
|
+
function isCollectionGet(endpoint) {
|
|
556
|
+
if (endpoint.httpMethod !== 'get' || isSkippedEndpoint(endpoint)) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
const normalized = normalizeApiPath(endpoint.path);
|
|
560
|
+
if (normalized.includes('{}')) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
const lastSegment = normalized.split('/').filter(Boolean).at(-1) ?? '';
|
|
564
|
+
return /s$/u.test(lastSegment) || /\[\]|Array<|List</u.test(endpoint.signature);
|
|
565
|
+
}
|
|
566
|
+
function hasPaginationEvidence(endpoint, contractEndpoints) {
|
|
567
|
+
const contractEndpoint = contractEndpoints.get(contractKey(endpoint.httpMethod, endpoint.path));
|
|
568
|
+
return contractEndpoint?.hasPagination === true || paginationPattern.test(`${endpoint.decorators}\n${endpoint.signature}\n${endpoint.body}`);
|
|
569
|
+
}
|
|
570
|
+
function stripStaticLiteralNoise(value) {
|
|
571
|
+
return value
|
|
572
|
+
.replace(/\/\*[\s\S]*?\*\//gu, ' ')
|
|
573
|
+
.replace(/\/\/.*$/gmu, ' ')
|
|
574
|
+
.replace(/(['"`])(?:\\.|(?!\1)[\s\S])*\1/gu, '""');
|
|
575
|
+
}
|
|
576
|
+
function isStaticCollectionExpression(expression) {
|
|
577
|
+
const normalized = expression.trim().replace(/;$/u, '').trim();
|
|
578
|
+
return (/^(?:Object\.)?(?:freeze|values|entries|keys)\s*\(/u.test(normalized) ||
|
|
579
|
+
/^(?:\[[\s\S]*\]|\{[\s\S]*\})$/u.test(normalized) ||
|
|
580
|
+
/^(?:[A-Z][A-Za-z0-9_]*|[a-z][A-Za-z0-9_]*(?:Config|Configs|Catalog|Catalogs|Metadata|Definitions|Types|Widgets|Constants))$/u.test(normalized));
|
|
581
|
+
}
|
|
582
|
+
function hasStaticDeclarationEvidence(fileContent, identifier) {
|
|
583
|
+
const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
584
|
+
return (new RegExp(`\\bimport\\s+(?:\\{[^}]*\\b${escaped}\\b[^}]*\\}|${escaped}\\b)`, 'u').test(fileContent) ||
|
|
585
|
+
new RegExp(`\\b(?:const|readonly)\\s+${escaped}\\s*=\\s*(?:Object\\.freeze\\s*\\(|\\[|\\{)`, 'u').test(fileContent) ||
|
|
586
|
+
new RegExp(`\\benum\\s+${escaped}\\b`, 'u').test(fileContent));
|
|
587
|
+
}
|
|
588
|
+
function isStaticCollectionBackedEndpoint(endpoint) {
|
|
589
|
+
const body = endpoint.body.trim();
|
|
590
|
+
if (/\b(?:await|repository|Repository|createQueryBuilder|findMany|findAll|find\s*\(|query\s*\(|execute\s*\(|service\.)\b/iu.test(body)) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
const returnMatch = /\breturn\s+([\s\S]{0,500}?);/u.exec(body) ??
|
|
594
|
+
/\bres\.(?:json|send)\s*\(\s*([\s\S]{0,500}?)\s*\)\s*;?/u.exec(body) ??
|
|
595
|
+
/\bOk\s*\(\s*([\s\S]{0,500}?)\s*\)\s*;?/u.exec(body);
|
|
596
|
+
if (!returnMatch?.[1]) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
const expression = returnMatch[1].trim();
|
|
600
|
+
if (/^[A-Za-z_$][\w$]*$/u.test(expression)) {
|
|
601
|
+
return hasStaticDeclarationEvidence(endpoint.file.content, expression);
|
|
602
|
+
}
|
|
603
|
+
return isStaticCollectionExpression(stripStaticLiteralNoise(expression));
|
|
604
|
+
}
|
|
605
|
+
function detectPaginationFindings(endpoints, contractEndpoints) {
|
|
606
|
+
return endpoints
|
|
607
|
+
.filter((endpoint) => isCollectionGet(endpoint) && !hasPaginationEvidence(endpoint, contractEndpoints) && !isStaticCollectionBackedEndpoint(endpoint))
|
|
608
|
+
.map((endpoint) => createFinding({
|
|
609
|
+
id: RuleIds.MISSING_PAGINATION,
|
|
610
|
+
message: `Collection endpoint ${endpoint.httpMethod.toUpperCase()} ${endpoint.path} lacks visible pagination evidence.`,
|
|
611
|
+
endpoint,
|
|
612
|
+
}));
|
|
613
|
+
}
|
|
614
|
+
function detectBulkFindings(endpoints) {
|
|
615
|
+
const findings = [];
|
|
616
|
+
for (const endpoint of endpoints) {
|
|
617
|
+
if (isSkippedEndpoint(endpoint)) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const routeEvidence = `${endpoint.path} ${endpoint.decorators} ${endpoint.signature}`;
|
|
621
|
+
const safetyEvidence = `${routeEvidence} ${endpoint.body}`;
|
|
622
|
+
const looksBulk = /\b(?:bulk|batch|mass|import|export|delete-all|update-all|deleteAll|updateAll|deleteMany|updateMany)\b/iu.test(routeEvidence);
|
|
623
|
+
const destructive = endpoint.httpMethod === 'delete' || (endpoint.httpMethod !== 'get' && /\b(?:delete|update|remove|process|import)\b/iu.test(routeEvidence));
|
|
624
|
+
if (!looksBulk || !destructive) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const hasSafeguard = authEvidencePattern.test(safetyEvidence) || safeguardPattern.test(safetyEvidence);
|
|
628
|
+
if (!hasSafeguard) {
|
|
629
|
+
findings.push(createFinding({
|
|
630
|
+
id: RuleIds.BULK_OPERATION_RISK,
|
|
631
|
+
message: `Bulk/destructive endpoint ${endpoint.httpMethod.toUpperCase()} ${endpoint.path} lacks visible guard or batch safety evidence.`,
|
|
632
|
+
endpoint,
|
|
633
|
+
}));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return findings;
|
|
637
|
+
}
|
|
638
|
+
function detectEntityLeakageFindings(endpoints) {
|
|
639
|
+
const findings = [];
|
|
640
|
+
for (const endpoint of endpoints) {
|
|
641
|
+
if (isSkippedEndpoint(endpoint) || endpoint.name.startsWith('_')) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const evidence = `${endpoint.signature}\n${endpoint.body}`;
|
|
645
|
+
if (dtoTypePattern.test(endpoint.signature) || mapperPattern.test(endpoint.body)) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const leaksByType = entityTypePattern.test(endpoint.signature);
|
|
649
|
+
const leaksByDirectReturn = /\breturn\s+(?:await\s+)?(?:this\.)?\w*(?:Repository|Repo|Dao|Model)\.\w+\s*\(/u.test(endpoint.body) || /\breturn\s+(?:await\s+)?prisma\.\w+\.\w+\s*\(/u.test(endpoint.body);
|
|
650
|
+
if (leaksByType || leaksByDirectReturn) {
|
|
651
|
+
findings.push(createFinding({
|
|
652
|
+
id: RuleIds.DTO_ENTITY_LEAKAGE,
|
|
653
|
+
message: `Endpoint '${endpoint.name}' appears to expose persistence/domain entities instead of an API DTO or response contract.`,
|
|
654
|
+
endpoint,
|
|
655
|
+
}));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return findings;
|
|
659
|
+
}
|
|
660
|
+
function detectResourceDesignFindings(endpoints) {
|
|
661
|
+
const findings = [];
|
|
662
|
+
const byFile = new Map();
|
|
663
|
+
for (const endpoint of endpoints.filter((entry) => !isSkippedEndpoint(entry))) {
|
|
664
|
+
byFile.set(endpoint.file.relativePath, [...(byFile.get(endpoint.file.relativePath) ?? []), endpoint]);
|
|
665
|
+
}
|
|
666
|
+
for (const entries of byFile.values()) {
|
|
667
|
+
if (entries.length < 3) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const hasDirectId = entries.some((entry) => /\/\{\}$/u.test(normalizeApiPath(entry.path)));
|
|
671
|
+
const hasById = entries.some((entry) => /\/by-id\/\{\}/u.test(normalizeApiPath(entry.path)));
|
|
672
|
+
const actionRoutes = entries.filter((entry) => /\/(?:get|create|update|delete|remove|find)[A-Z_a-z-]/u.test(entry.path));
|
|
673
|
+
if ((hasDirectId && hasById) || actionRoutes.length >= 2) {
|
|
674
|
+
const first = entries[0];
|
|
675
|
+
if (first) {
|
|
676
|
+
findings.push(createFinding({
|
|
677
|
+
id: RuleIds.INCONSISTENT_RESOURCE_DESIGN,
|
|
678
|
+
message: `Controller mixes resource route conventions across ${entries.length} endpoints.`,
|
|
679
|
+
endpoint: first,
|
|
680
|
+
target: first.file.relativePath,
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return findings;
|
|
686
|
+
}
|
|
687
|
+
function versionKind(apiPath) {
|
|
688
|
+
const normalized = normalizeApiPath(apiPath);
|
|
689
|
+
if (/^\/api\/v\d+(\/|$)/u.test(normalized)) {
|
|
690
|
+
return 'api-versioned';
|
|
691
|
+
}
|
|
692
|
+
if (/^\/v\d+(\/|$)/u.test(normalized)) {
|
|
693
|
+
return 'root-versioned';
|
|
694
|
+
}
|
|
695
|
+
return 'none';
|
|
696
|
+
}
|
|
697
|
+
function detectVersioningFindings(endpoints) {
|
|
698
|
+
const relevant = endpoints.filter((entry) => !isSkippedEndpoint(entry));
|
|
699
|
+
const kinds = new Set(relevant.map((entry) => versionKind(entry.path)));
|
|
700
|
+
if (!kinds.has('none') || (kinds.size === 1 && kinds.has('none'))) {
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
const versionedCount = relevant.filter((entry) => versionKind(entry.path) !== 'none').length;
|
|
704
|
+
const unversionedCount = relevant.length - versionedCount;
|
|
705
|
+
if (versionedCount < 1 || unversionedCount < 1 || relevant.length < 3) {
|
|
706
|
+
return [];
|
|
707
|
+
}
|
|
708
|
+
const endpoint = relevant.find((entry) => versionKind(entry.path) === 'none') ?? relevant[0];
|
|
709
|
+
return endpoint
|
|
710
|
+
? [createFinding({
|
|
711
|
+
id: RuleIds.VERSIONING_INCONSISTENCY,
|
|
712
|
+
message: 'API surface mixes versioned and unversioned routes in the same backend.',
|
|
713
|
+
endpoint,
|
|
714
|
+
})]
|
|
715
|
+
: [];
|
|
716
|
+
}
|
|
717
|
+
function parseJsonOpenApi(content) {
|
|
718
|
+
try {
|
|
719
|
+
const parsed = JSON.parse(content);
|
|
720
|
+
return Object.entries(parsed.paths ?? {}).flatMap(([apiPath, operations]) => Object.entries(operations)
|
|
721
|
+
.filter(([method]) => httpMethods.has(method.toLowerCase()))
|
|
722
|
+
.map(([method, operation]) => ({
|
|
723
|
+
httpMethod: method.toLowerCase(),
|
|
724
|
+
path: apiPath,
|
|
725
|
+
hasPagination: paginationPattern.test(JSON.stringify(operation)),
|
|
726
|
+
})));
|
|
727
|
+
}
|
|
728
|
+
catch {
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function parseYamlOpenApi(content) {
|
|
733
|
+
const endpoints = [];
|
|
734
|
+
const lines = content.split(/\r?\n/u);
|
|
735
|
+
let inPaths = false;
|
|
736
|
+
let currentPath;
|
|
737
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
738
|
+
const line = lines[index] ?? '';
|
|
739
|
+
if (/^paths:\s*$/u.test(line)) {
|
|
740
|
+
inPaths = true;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (!inPaths) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const pathMatch = /^\s{2}(["']?\/[^:"']+[^"']*["']?):\s*$/u.exec(line);
|
|
747
|
+
if (pathMatch?.[1]) {
|
|
748
|
+
currentPath = pathMatch[1].replace(/^['"]|['"]$/gu, '');
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const methodMatch = /^\s{4}(get|post|put|patch|delete):\s*$/iu.exec(line);
|
|
752
|
+
if (currentPath && methodMatch?.[1]) {
|
|
753
|
+
const block = lines.slice(index, Math.min(lines.length, index + 30)).join('\n');
|
|
754
|
+
endpoints.push({
|
|
755
|
+
httpMethod: methodMatch[1].toLowerCase(),
|
|
756
|
+
path: currentPath,
|
|
757
|
+
hasPagination: paginationPattern.test(block),
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return endpoints;
|
|
762
|
+
}
|
|
763
|
+
async function loadContractEndpoints(workspaceRoot, contract) {
|
|
764
|
+
const openApiRelativePath = resolveOpenApiRelativePath(contract);
|
|
765
|
+
const openApiPath = path.join(workspaceRoot, ...openApiRelativePath.split('/'));
|
|
766
|
+
if (!(await pathExists(openApiPath))) {
|
|
767
|
+
return { endpoints: new Map() };
|
|
768
|
+
}
|
|
769
|
+
const content = await readTextFileIfReasonable(openApiPath);
|
|
770
|
+
if (!content) {
|
|
771
|
+
return { endpoints: new Map() };
|
|
772
|
+
}
|
|
773
|
+
const parsed = parseJsonOpenApi(content) ?? parseYamlOpenApi(content);
|
|
774
|
+
return {
|
|
775
|
+
contractPath: openApiRelativePath,
|
|
776
|
+
endpoints: new Map(parsed.map((endpoint) => [contractKey(endpoint.httpMethod, endpoint.path), endpoint])),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function detectContractDriftFindings(endpoints, contractInput) {
|
|
780
|
+
if (!contractInput.contractPath || contractInput.endpoints.size === 0 || endpoints.length === 0) {
|
|
781
|
+
return [];
|
|
782
|
+
}
|
|
783
|
+
const businessContractEndpoints = [...contractInput.endpoints.entries()].filter(([, endpoint]) => !isSkippedEndpoint({ path: endpoint.path, name: endpoint.path }));
|
|
784
|
+
if (businessContractEndpoints.length === 0) {
|
|
785
|
+
return [];
|
|
786
|
+
}
|
|
787
|
+
const implementationEndpoints = new Map(endpoints
|
|
788
|
+
.filter((endpoint) => !isSkippedEndpoint(endpoint))
|
|
789
|
+
.map((endpoint) => [contractKey(endpoint.httpMethod, endpoint.path), endpoint]));
|
|
790
|
+
const findings = [];
|
|
791
|
+
if (contractInput.apiStyle !== 'graphql') {
|
|
792
|
+
for (const [key, endpoint] of implementationEndpoints) {
|
|
793
|
+
if (!contractInput.endpoints.has(key)) {
|
|
794
|
+
findings.push(createFinding({
|
|
795
|
+
id: RuleIds.CONTRACT_IMPLEMENTATION_DRIFT,
|
|
796
|
+
message: `Implementation endpoint ${endpoint.httpMethod.toUpperCase()} ${endpoint.path} is missing from the OpenAPI contract.`,
|
|
797
|
+
endpoint,
|
|
798
|
+
}));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
for (const [key, endpoint] of businessContractEndpoints) {
|
|
803
|
+
if (!implementationEndpoints.has(key)) {
|
|
804
|
+
findings.push(createContractFinding({
|
|
805
|
+
message: `OpenAPI contract endpoint ${endpoint.httpMethod.toUpperCase()} ${endpoint.path} has no matching implementation endpoint.`,
|
|
806
|
+
filePath: contractInput.contractPath,
|
|
807
|
+
api: `${endpoint.httpMethod.toUpperCase()} ${endpoint.path}`,
|
|
808
|
+
target: key,
|
|
809
|
+
}));
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return findings;
|
|
813
|
+
}
|
|
814
|
+
async function detectApiDesignFindings(input) {
|
|
815
|
+
if (input.config.apiStyle === 'none') {
|
|
816
|
+
return [];
|
|
817
|
+
}
|
|
818
|
+
const disabled = input.disabledRuleIds ?? new Set();
|
|
819
|
+
const started = Date.now();
|
|
820
|
+
const [files, contractEndpoints] = await Promise.all([
|
|
821
|
+
collectSourceFiles(input.workspaceRoot, input.sourceInventory),
|
|
822
|
+
loadContractEndpoints(input.workspaceRoot, input.contract),
|
|
823
|
+
]);
|
|
824
|
+
(0, validationProfiling_1.emitValidationProfile)('AP-API analyzer file scan', Date.now() - started, [
|
|
825
|
+
{ name: 'files', value: files.length },
|
|
826
|
+
{ name: 'contractEndpoints', value: contractEndpoints.endpoints.size },
|
|
827
|
+
]);
|
|
828
|
+
const endpoints = files.flatMap((file) => extractEndpoints(file, input.contract));
|
|
829
|
+
const dynamicEndpoints = input.contract.apiStyle === 'graphql'
|
|
830
|
+
? [...contractEndpoints.endpoints.values()]
|
|
831
|
+
.filter((endpoint) => contractKey(endpoint.httpMethod, endpoint.path) === contractKey('post', '/graphql'))
|
|
832
|
+
.map((endpoint) => ({
|
|
833
|
+
httpMethod: endpoint.httpMethod,
|
|
834
|
+
path: endpoint.path,
|
|
835
|
+
name: 'configured GraphQL endpoint',
|
|
836
|
+
line: 1,
|
|
837
|
+
decorators: 'configured GraphQL API style',
|
|
838
|
+
signature: 'configured GraphQL API style',
|
|
839
|
+
body: '',
|
|
840
|
+
file: {
|
|
841
|
+
relativePath: contractEndpoints.contractPath ?? resolveOpenApiRelativePath(input.contract),
|
|
842
|
+
content: '',
|
|
843
|
+
extension: '.yaml',
|
|
844
|
+
},
|
|
845
|
+
isConfiguredDynamic: true,
|
|
846
|
+
}))
|
|
847
|
+
: [];
|
|
848
|
+
const implementationEndpoints = [...endpoints, ...dynamicEndpoints];
|
|
849
|
+
return [
|
|
850
|
+
...detectPaginationFindings(endpoints, contractEndpoints.endpoints),
|
|
851
|
+
...detectBulkFindings(endpoints),
|
|
852
|
+
...detectEntityLeakageFindings(endpoints),
|
|
853
|
+
...detectResourceDesignFindings(endpoints),
|
|
854
|
+
...detectVersioningFindings(endpoints),
|
|
855
|
+
...detectContractDriftFindings(implementationEndpoints, {
|
|
856
|
+
...contractEndpoints,
|
|
857
|
+
apiStyle: input.contract.apiStyle,
|
|
858
|
+
}),
|
|
859
|
+
]
|
|
860
|
+
.filter((finding) => !disabled.has(finding.id))
|
|
861
|
+
.sort((left, right) => {
|
|
862
|
+
const fileCompare = (left.sourceFile ?? '').localeCompare(right.sourceFile ?? '');
|
|
863
|
+
if (fileCompare !== 0) {
|
|
864
|
+
return fileCompare;
|
|
865
|
+
}
|
|
866
|
+
return left.id.localeCompare(right.id);
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
//# sourceMappingURL=apiDesignAnalysis.js.map
|