@archpilotlabs/archpilot 0.2.2 → 0.2.4
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/CHANGELOG.md +538 -0
- package/README.md +1 -0
- package/dist/archpilot-cli.js +850 -159
- 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 +3 -2
|
@@ -0,0 +1,1416 @@
|
|
|
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.detectTransactionDomainFindings = detectTransactionDomainFindings;
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const node_fs_1 = require("node:fs");
|
|
39
|
+
const ts = __importStar(require("typescript"));
|
|
40
|
+
const moduleDiscovery_1 = require("./moduleDiscovery");
|
|
41
|
+
const archpilotIgnore_1 = require("./smartInit/archpilotIgnore");
|
|
42
|
+
const validationProfiling_1 = require("./validationProfiling");
|
|
43
|
+
const sourceScopeClassification_1 = require("./sourceScopeClassification");
|
|
44
|
+
const RuleIds = {
|
|
45
|
+
TXN_BOUNDARY_TOO_BROAD: 'AP-TXN-001',
|
|
46
|
+
TXN_IN_CONTROLLER: 'AP-TXN-002',
|
|
47
|
+
TXN_NESTED_RISK: 'AP-TXN-003',
|
|
48
|
+
TXN_LONG_RUNNING_WORKFLOW: 'AP-TXN-004',
|
|
49
|
+
DOM_ANEMIC_MODEL: 'AP-DOM-001',
|
|
50
|
+
DOM_AGGREGATE_BOUNDARY_VIOLATION: 'AP-DOM-002',
|
|
51
|
+
DOM_OVERSIZED_BOUNDARY: 'AP-DOM-003',
|
|
52
|
+
DOM_LOGIC_SCATTERED: 'AP-DOM-004',
|
|
53
|
+
DOM_CROSS_AGGREGATE_MODIFICATION: 'AP-DOM-005',
|
|
54
|
+
};
|
|
55
|
+
const maxFileSizeBytes = 384 * 1024;
|
|
56
|
+
const supportedExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.java', '.cs', '.py', '.php', '.go']);
|
|
57
|
+
const ignoredDirectoryNames = new Set([
|
|
58
|
+
'.git',
|
|
59
|
+
'.archpilot',
|
|
60
|
+
'.next',
|
|
61
|
+
'.vscode-test',
|
|
62
|
+
'node_modules',
|
|
63
|
+
'dist',
|
|
64
|
+
'build',
|
|
65
|
+
'out',
|
|
66
|
+
'coverage',
|
|
67
|
+
'test',
|
|
68
|
+
'tests',
|
|
69
|
+
'__tests__',
|
|
70
|
+
'fixtures',
|
|
71
|
+
'smoke',
|
|
72
|
+
'examples',
|
|
73
|
+
'docs',
|
|
74
|
+
'reports',
|
|
75
|
+
'generated',
|
|
76
|
+
]);
|
|
77
|
+
const transactionEvidencePattern = /@Transactional\b|\b(?:transactional|transaction)\s*\(|\b(?:\$transaction|startTransaction|beginTransaction|commitTransaction|rollbackTransaction|TransactionTemplate|PlatformTransactionManager|transactionManager|QueryRunner|EntityManager|TransactionScope|BeginTransaction|BeginTransactionAsync|Commit|CommitAsync|Rollback|RollbackAsync|DB::transaction|beginTransaction|commit|rollBack|session\.begin|BeginTx|Begin|db\.Transaction)\b/iu;
|
|
78
|
+
const longRunningPattern = /\b(?:HttpService|httpClient|http_client|RestTemplate|WebClient|axios|fetch|Kafka|Rabbit|Sqs|Queue|Mailer|Mail::|Http::|mailClient|emailClient|sendEmail|send\w*Email|Sms|upload|putObject|sleep|setTimeout|retry)\b/iu;
|
|
79
|
+
const transactionBoundaryDecoratorPattern = /@(?:Transactional|Transaction)\b|\[(?:Transaction|Transactional)\b[^\]]*\]/u;
|
|
80
|
+
const explicitTransactionBoundaryPattern = /\b(?:\$transaction|DB::transaction)\s*\(|\b(?:knex|sequelize|typeorm|dataSource|connection|manager|transactionManager)\.transaction\s*\(|\b(?:this\.)?connection\.withTransaction\s*\(|\b(?:startTransaction|beginTransaction|BeginTransaction|BeginTransactionAsync|BeginTx)\s*\(|\bnew\s+TransactionScope\b|\bsession\.begin\s*\(|\bdb\.Transaction\s*\(/iu;
|
|
81
|
+
const separateTransactionPropagationPattern = /\b(?:requiresNew|REQUIRES_NEW|Propagation\.REQUIRES_NEW)\b/u;
|
|
82
|
+
const businessRulePattern = /\b(?:approve|reject|authorize|eligible|eligibility|policy|discount|limit|quota|balance|risk|compliance|settle|state|status|transition)\b/iu;
|
|
83
|
+
const transactionCallbackStartPattern = /\b(?:this\.)?(?:[\w.$:]+\.)?(?:\$transaction|transaction|Transaction)\s*\(\s*(?:async\s*)?(?:function\s*)?\(?\s*\w*\s*\)?\s*(?:=>)?\s*\{/gu;
|
|
84
|
+
const explicitDomainModelEvidencePattern = /@(?:Entity|Embeddable|MappedSuperclass|Column|PrimaryGeneratedColumn|ObjectType|Field)\b|\[(?:Table|Key|Required|Column|StringLength|MaxLength)\b[^\]]*\]|\b(?:BaseModel|SQLModel|declarative_base|Mapped\[|Column\s*\(|@dataclass\b|extends\s+Model\b|\$fillable\b|\$casts\b|gorm\.Model|`[^`]*(?:gorm|db):)/u;
|
|
85
|
+
const domainFrameworkLeakPattern = /\b(?:ControllerBase|IActionResult|HttpContext|Microsoft\.AspNetCore|APIRouter|FastAPI|Request|Response|Depends|Illuminate\\Http|Illuminate\\Routing|Route::|http\.ResponseWriter|http\.Request|gin\.Context|express|Router|org\.springframework\.web|jakarta\.servlet|HttpServletRequest|@Controller|@RestController|@Get|@Post|DbContext|EntityManager|Session|Repository)\b/u;
|
|
86
|
+
function maskNonCodeEvidence(value) {
|
|
87
|
+
let masked = '';
|
|
88
|
+
let index = 0;
|
|
89
|
+
let previousSignificant = '';
|
|
90
|
+
const pushMasked = (count) => {
|
|
91
|
+
masked += ' '.repeat(count);
|
|
92
|
+
};
|
|
93
|
+
const pushCode = (character) => {
|
|
94
|
+
masked += character;
|
|
95
|
+
if (!/\s/u.test(character)) {
|
|
96
|
+
previousSignificant = character;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
while (index < value.length) {
|
|
100
|
+
const character = value[index] ?? '';
|
|
101
|
+
const next = value[index + 1] ?? '';
|
|
102
|
+
if (character === '/' && next === '/') {
|
|
103
|
+
const end = value.indexOf('\n', index + 2);
|
|
104
|
+
const length = end === -1 ? value.length - index : end - index;
|
|
105
|
+
pushMasked(length);
|
|
106
|
+
index += length;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (character === '/' && next === '*') {
|
|
110
|
+
const end = value.indexOf('*/', index + 2);
|
|
111
|
+
const length = end === -1 ? value.length - index : end + 2 - index;
|
|
112
|
+
pushMasked(length);
|
|
113
|
+
index += length;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (character === '\'' || character === '"' || character === '`') {
|
|
117
|
+
const quote = character;
|
|
118
|
+
let cursor = index + 1;
|
|
119
|
+
while (cursor < value.length) {
|
|
120
|
+
const current = value[cursor] ?? '';
|
|
121
|
+
if (current === '\\') {
|
|
122
|
+
cursor += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (current === quote) {
|
|
126
|
+
cursor += 1;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
cursor += 1;
|
|
130
|
+
}
|
|
131
|
+
pushMasked(cursor - index);
|
|
132
|
+
index = cursor;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (character === '/' &&
|
|
136
|
+
(previousSignificant === '' ||
|
|
137
|
+
/[([{:;,=!?&|]/u.test(previousSignificant) ||
|
|
138
|
+
/\b(?:return|case|throw|yield)\s*$/u.test(value.slice(Math.max(0, index - 24), index)))) {
|
|
139
|
+
let cursor = index + 1;
|
|
140
|
+
let inCharacterClass = false;
|
|
141
|
+
while (cursor < value.length) {
|
|
142
|
+
const current = value[cursor] ?? '';
|
|
143
|
+
if (current === '\\') {
|
|
144
|
+
cursor += 2;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (current === '[') {
|
|
148
|
+
inCharacterClass = true;
|
|
149
|
+
}
|
|
150
|
+
else if (current === ']') {
|
|
151
|
+
inCharacterClass = false;
|
|
152
|
+
}
|
|
153
|
+
else if (current === '/' && !inCharacterClass) {
|
|
154
|
+
cursor += 1;
|
|
155
|
+
while (/[a-z]/iu.test(value[cursor] ?? '')) {
|
|
156
|
+
cursor += 1;
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
cursor += 1;
|
|
161
|
+
}
|
|
162
|
+
pushMasked(cursor - index);
|
|
163
|
+
index = cursor;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
pushCode(character);
|
|
167
|
+
index += 1;
|
|
168
|
+
}
|
|
169
|
+
return masked;
|
|
170
|
+
}
|
|
171
|
+
function normalizePath(value) {
|
|
172
|
+
return value.replace(/\\/g, '/');
|
|
173
|
+
}
|
|
174
|
+
function toPosixRelative(workspaceRoot, absolutePath) {
|
|
175
|
+
return normalizePath(path.relative(workspaceRoot, absolutePath));
|
|
176
|
+
}
|
|
177
|
+
async function readTextFileIfReasonable(filePath, sourceInventory) {
|
|
178
|
+
if (sourceInventory) {
|
|
179
|
+
return sourceInventory.readText(filePath, { maxFileSizeBytes });
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const stats = await node_fs_1.promises.stat(filePath);
|
|
183
|
+
if (stats.size > maxFileSizeBytes) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
return await node_fs_1.promises.readFile(filePath, { encoding: 'utf8' });
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function collectSourceFiles(workspaceRoot, sourceInventory) {
|
|
193
|
+
if (sourceInventory) {
|
|
194
|
+
const files = await sourceInventory.getTextFiles({
|
|
195
|
+
extensions: [...supportedExtensions],
|
|
196
|
+
ignoredDirectoryNames: [...ignoredDirectoryNames],
|
|
197
|
+
maxFileSizeBytes,
|
|
198
|
+
});
|
|
199
|
+
return files.map((file) => ({
|
|
200
|
+
relativePath: file.relativePath,
|
|
201
|
+
content: file.content,
|
|
202
|
+
extension: file.extension,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
const ignoreMatcher = (0, archpilotIgnore_1.loadArchpilotIgnoreMatcher)(workspaceRoot);
|
|
206
|
+
const collected = [];
|
|
207
|
+
const stack = [workspaceRoot];
|
|
208
|
+
while (stack.length > 0) {
|
|
209
|
+
const current = stack.pop();
|
|
210
|
+
if (!current) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
let entries;
|
|
214
|
+
try {
|
|
215
|
+
entries = await node_fs_1.promises.readdir(current, { withFileTypes: true });
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
const absoluteEntryPath = path.join(current, entry.name);
|
|
222
|
+
const relativeEntryPath = toPosixRelative(workspaceRoot, absoluteEntryPath);
|
|
223
|
+
if (ignoreMatcher.isIgnored(relativeEntryPath)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (entry.isDirectory()) {
|
|
227
|
+
if (entry.name.startsWith('.') || ignoredDirectoryNames.has(entry.name)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
stack.push(absoluteEntryPath);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
234
|
+
if (!supportedExtensions.has(extension)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const content = await readTextFileIfReasonable(absoluteEntryPath, sourceInventory);
|
|
238
|
+
if (content === undefined) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
collected.push({ relativePath: relativeEntryPath, content, extension });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return collected.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
245
|
+
}
|
|
246
|
+
function lineNumberForOffset(content, offset) {
|
|
247
|
+
let line = 1;
|
|
248
|
+
for (let index = 0; index < offset && index < content.length; index += 1) {
|
|
249
|
+
if (content[index] === '\n') {
|
|
250
|
+
line += 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return line;
|
|
254
|
+
}
|
|
255
|
+
function getLineStarts(file) {
|
|
256
|
+
if (file.lineStarts) {
|
|
257
|
+
return file.lineStarts;
|
|
258
|
+
}
|
|
259
|
+
const lineStarts = [0];
|
|
260
|
+
for (let index = 0; index < file.content.length; index += 1) {
|
|
261
|
+
if (file.content[index] === '\n') {
|
|
262
|
+
lineStarts.push(index + 1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
file.lineStarts = lineStarts;
|
|
266
|
+
return lineStarts;
|
|
267
|
+
}
|
|
268
|
+
function lineNumberForFileOffset(file, offset) {
|
|
269
|
+
const lineStarts = getLineStarts(file);
|
|
270
|
+
let low = 0;
|
|
271
|
+
let high = lineStarts.length - 1;
|
|
272
|
+
while (low <= high) {
|
|
273
|
+
const middle = Math.floor((low + high) / 2);
|
|
274
|
+
const lineStart = lineStarts[middle] ?? 0;
|
|
275
|
+
if (lineStart <= offset) {
|
|
276
|
+
low = middle + 1;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
high = middle - 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return Math.max(1, high + 1);
|
|
283
|
+
}
|
|
284
|
+
function findMatchingBrace(content, openBraceIndex) {
|
|
285
|
+
let depth = 0;
|
|
286
|
+
for (let index = openBraceIndex; index < content.length; index += 1) {
|
|
287
|
+
const char = content[index];
|
|
288
|
+
if (char === '{') {
|
|
289
|
+
depth += 1;
|
|
290
|
+
}
|
|
291
|
+
else if (char === '}') {
|
|
292
|
+
depth -= 1;
|
|
293
|
+
if (depth === 0) {
|
|
294
|
+
return index;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return Math.min(content.length - 1, openBraceIndex + 5000);
|
|
299
|
+
}
|
|
300
|
+
function createMethodEntry(input) {
|
|
301
|
+
let cachedBody;
|
|
302
|
+
return {
|
|
303
|
+
name: input.name,
|
|
304
|
+
...(input.className ? { className: input.className } : {}),
|
|
305
|
+
decorators: input.decorators,
|
|
306
|
+
bodyStartOffset: input.bodyStartOffset,
|
|
307
|
+
bodyEndOffset: input.bodyEndOffset,
|
|
308
|
+
line: lineNumberForFileOffset(input.file, input.lineOffset),
|
|
309
|
+
file: input.file,
|
|
310
|
+
classification: input.classification,
|
|
311
|
+
get body() {
|
|
312
|
+
cachedBody ??= input.file.content.slice(input.bodyStartOffset, input.bodyEndOffset);
|
|
313
|
+
return cachedBody;
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function extractTransactionCallbackBodies(body) {
|
|
318
|
+
const callbackBodies = [];
|
|
319
|
+
for (const match of body.matchAll(transactionCallbackStartPattern)) {
|
|
320
|
+
const openBraceIndex = (match.index ?? 0) + match[0].lastIndexOf('{');
|
|
321
|
+
const closeBraceIndex = findMatchingBrace(body, openBraceIndex);
|
|
322
|
+
callbackBodies.push(body.slice(openBraceIndex, closeBraceIndex + 1));
|
|
323
|
+
}
|
|
324
|
+
return callbackBodies;
|
|
325
|
+
}
|
|
326
|
+
function inferModuleForPath(contract, relativePath) {
|
|
327
|
+
const normalized = normalizePath(relativePath);
|
|
328
|
+
const modulesRoot = (0, moduleDiscovery_1.resolveModulesRootFromContract)(contract);
|
|
329
|
+
for (const [moduleName, reference] of Object.entries(contract.modules ?? {})) {
|
|
330
|
+
const modulePath = normalizePath(reference.path ?? `${modulesRoot}/${moduleName}`);
|
|
331
|
+
if (normalized === modulePath || normalized.startsWith(`${modulePath}/`)) {
|
|
332
|
+
return moduleName;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
function parseSourceFile(file, astCache) {
|
|
338
|
+
return astCache
|
|
339
|
+
? astCache.parseSourceFile({
|
|
340
|
+
filePath: file.relativePath,
|
|
341
|
+
sourceText: file.content,
|
|
342
|
+
scriptTarget: ts.ScriptTarget.Latest,
|
|
343
|
+
setParentNodes: true,
|
|
344
|
+
})
|
|
345
|
+
: ts.createSourceFile(file.relativePath, file.content, ts.ScriptTarget.Latest, true);
|
|
346
|
+
}
|
|
347
|
+
function getTypeScriptDeclaredRoles(file, astCache) {
|
|
348
|
+
const sourceFile = parseSourceFile(file, astCache);
|
|
349
|
+
const roles = new Set();
|
|
350
|
+
if (explicitDomainModelEvidencePattern.test(maskNonCodeEvidence(file.content))) {
|
|
351
|
+
roles.add('entity');
|
|
352
|
+
}
|
|
353
|
+
const visit = (node) => {
|
|
354
|
+
if ((ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && node.name) {
|
|
355
|
+
const name = node.name.text;
|
|
356
|
+
if (/Controller$/u.test(name)) {
|
|
357
|
+
roles.add('controller');
|
|
358
|
+
}
|
|
359
|
+
if (/Service$/u.test(name)) {
|
|
360
|
+
roles.add('service');
|
|
361
|
+
}
|
|
362
|
+
if (/(Repository|Dao)$/u.test(name)) {
|
|
363
|
+
roles.add('repository');
|
|
364
|
+
}
|
|
365
|
+
if (/(Entity|Aggregate|Model)$/u.test(name)) {
|
|
366
|
+
roles.add('entity');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
ts.forEachChild(node, visit);
|
|
370
|
+
};
|
|
371
|
+
visit(sourceFile);
|
|
372
|
+
return [...roles];
|
|
373
|
+
}
|
|
374
|
+
function getDeclaredRoles(file, astCache) {
|
|
375
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(file.extension)) {
|
|
376
|
+
return getTypeScriptDeclaredRoles(file, astCache);
|
|
377
|
+
}
|
|
378
|
+
const code = maskNonCodeEvidence(file.content);
|
|
379
|
+
const roles = new Set();
|
|
380
|
+
if (/\bclass\s+\w*Controller\b/u.test(code) || /@(?:Controller|RestController)\b|\[ApiController\]/u.test(code)) {
|
|
381
|
+
roles.add('controller');
|
|
382
|
+
}
|
|
383
|
+
if (/\bclass\s+\w*Service\b/u.test(code) || /@Service\b/u.test(code)) {
|
|
384
|
+
roles.add('service');
|
|
385
|
+
}
|
|
386
|
+
if (/\bclass\s+\w*(?:Repository|Dao)\b/u.test(code) || /@Repository\b/u.test(code)) {
|
|
387
|
+
roles.add('repository');
|
|
388
|
+
}
|
|
389
|
+
if (/\bclass\s+\w*(?:Entity|Aggregate|Model)\b/u.test(code) || explicitDomainModelEvidencePattern.test(code)) {
|
|
390
|
+
roles.add('entity');
|
|
391
|
+
}
|
|
392
|
+
return [...roles];
|
|
393
|
+
}
|
|
394
|
+
function classifySourceFile(file, contract, astCache) {
|
|
395
|
+
const scope = (0, sourceScopeClassification_1.classifyValidationSourceScope)(file.relativePath);
|
|
396
|
+
const roles = new Set(getDeclaredRoles(file, astCache));
|
|
397
|
+
const lowerPath = file.relativePath.toLowerCase();
|
|
398
|
+
if (/(^|[./_-])controller\.(ts|tsx|js|jsx|mjs|cjs|java|cs|py|php|go)$/u.test(lowerPath) || /(^|\/)(controllers?|routers?|routes?|handlers?)\//u.test(lowerPath)) {
|
|
399
|
+
roles.add('controller');
|
|
400
|
+
}
|
|
401
|
+
if (/(^|[./_-])service\.(ts|tsx|js|jsx|mjs|cjs|java|cs|py|php|go)$/u.test(lowerPath) || /(^|\/)(services?)\//u.test(lowerPath)) {
|
|
402
|
+
roles.add('service');
|
|
403
|
+
}
|
|
404
|
+
if (/(^|[./_-])(?:repository|repo|dao)\.(ts|tsx|js|jsx|mjs|cjs|java|cs|py|php|go)$/u.test(lowerPath) || /(^|\/)(repositories?|repository|dao|data)\//u.test(lowerPath)) {
|
|
405
|
+
roles.add('repository');
|
|
406
|
+
}
|
|
407
|
+
if (/(^|\/)(?:domain|entities|entity|models)\//u.test(lowerPath) && !/(dto|request|response|viewmodel)/u.test(lowerPath)) {
|
|
408
|
+
roles.add('entity');
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
roles: [...roles],
|
|
412
|
+
moduleName: inferModuleForPath(contract, file.relativePath),
|
|
413
|
+
scope,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function getNodeNameText(name) {
|
|
417
|
+
if (!name) {
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) {
|
|
421
|
+
return name.text;
|
|
422
|
+
}
|
|
423
|
+
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
424
|
+
return name.text;
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
function createTypeScriptMethodEntry(input) {
|
|
429
|
+
const bodyStartOffset = input.body.getStart(input.sourceFile, false);
|
|
430
|
+
const bodyEndOffset = input.body.getEnd();
|
|
431
|
+
return createMethodEntry({
|
|
432
|
+
name: input.name,
|
|
433
|
+
className: input.className,
|
|
434
|
+
decorators: input.file.content.slice(input.node.getFullStart(), bodyStartOffset),
|
|
435
|
+
bodyStartOffset,
|
|
436
|
+
bodyEndOffset,
|
|
437
|
+
lineOffset: input.node.getStart(input.sourceFile, false),
|
|
438
|
+
file: input.file,
|
|
439
|
+
classification: input.classification,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function extractTypeScriptMethods(file, classification, astCache) {
|
|
443
|
+
const sourceFile = parseSourceFile(file, astCache);
|
|
444
|
+
const methods = [];
|
|
445
|
+
const visit = (node, className) => {
|
|
446
|
+
if (ts.isClassDeclaration(node)) {
|
|
447
|
+
const currentClassName = node.name?.text ?? className;
|
|
448
|
+
ts.forEachChild(node, (child) => visit(child, currentClassName));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (ts.isMethodDeclaration(node) && node.body) {
|
|
452
|
+
methods.push(createTypeScriptMethodEntry({
|
|
453
|
+
file,
|
|
454
|
+
sourceFile,
|
|
455
|
+
node,
|
|
456
|
+
name: getNodeNameText(node.name) ?? 'method',
|
|
457
|
+
className,
|
|
458
|
+
body: node.body,
|
|
459
|
+
classification,
|
|
460
|
+
}));
|
|
461
|
+
ts.forEachChild(node.body, (child) => visit(child, className));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (ts.isConstructorDeclaration(node) && node.body) {
|
|
465
|
+
methods.push(createTypeScriptMethodEntry({
|
|
466
|
+
file,
|
|
467
|
+
sourceFile,
|
|
468
|
+
node,
|
|
469
|
+
name: 'constructor',
|
|
470
|
+
className,
|
|
471
|
+
body: node.body,
|
|
472
|
+
classification,
|
|
473
|
+
}));
|
|
474
|
+
ts.forEachChild(node.body, (child) => visit(child, className));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (ts.isPropertyDeclaration(node) &&
|
|
478
|
+
node.initializer &&
|
|
479
|
+
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
|
|
480
|
+
node.initializer.body) {
|
|
481
|
+
methods.push(createTypeScriptMethodEntry({
|
|
482
|
+
file,
|
|
483
|
+
sourceFile,
|
|
484
|
+
node,
|
|
485
|
+
name: getNodeNameText(node.name) ?? 'method',
|
|
486
|
+
className,
|
|
487
|
+
body: node.initializer.body,
|
|
488
|
+
classification,
|
|
489
|
+
}));
|
|
490
|
+
ts.forEachChild(node.initializer.body, (child) => visit(child, className));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (ts.isFunctionDeclaration(node) && node.body) {
|
|
494
|
+
methods.push(createTypeScriptMethodEntry({
|
|
495
|
+
file,
|
|
496
|
+
sourceFile,
|
|
497
|
+
node,
|
|
498
|
+
name: node.name?.text ?? 'function',
|
|
499
|
+
className,
|
|
500
|
+
body: node.body,
|
|
501
|
+
classification,
|
|
502
|
+
}));
|
|
503
|
+
ts.forEachChild(node.body, (child) => visit(child, className));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (ts.isVariableDeclaration(node) &&
|
|
507
|
+
node.initializer &&
|
|
508
|
+
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
|
|
509
|
+
node.initializer.body) {
|
|
510
|
+
methods.push(createTypeScriptMethodEntry({
|
|
511
|
+
file,
|
|
512
|
+
sourceFile,
|
|
513
|
+
node,
|
|
514
|
+
name: getNodeNameText(node.name) ?? 'function',
|
|
515
|
+
className,
|
|
516
|
+
body: node.initializer.body,
|
|
517
|
+
classification,
|
|
518
|
+
}));
|
|
519
|
+
ts.forEachChild(node.initializer.body, (child) => visit(child, className));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
ts.forEachChild(node, (child) => visit(child, className));
|
|
523
|
+
};
|
|
524
|
+
visit(sourceFile);
|
|
525
|
+
return methods;
|
|
526
|
+
}
|
|
527
|
+
function getLineEndOffset(content, lineStart) {
|
|
528
|
+
const newlineOffset = content.indexOf('\n', lineStart);
|
|
529
|
+
return newlineOffset === -1 ? content.length : newlineOffset;
|
|
530
|
+
}
|
|
531
|
+
function countJavaAnnotationParenDelta(line) {
|
|
532
|
+
let delta = 0;
|
|
533
|
+
for (const char of line) {
|
|
534
|
+
if (char === '(') {
|
|
535
|
+
delta += 1;
|
|
536
|
+
}
|
|
537
|
+
else if (char === ')') {
|
|
538
|
+
delta -= 1;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return delta;
|
|
542
|
+
}
|
|
543
|
+
function findJavaOpeningBrace(file, startLineIndex, maxSignatureLines = 30) {
|
|
544
|
+
const lineStarts = getLineStarts(file);
|
|
545
|
+
const signatureLines = [];
|
|
546
|
+
for (let lineIndex = startLineIndex; lineIndex < lineStarts.length && lineIndex < startLineIndex + maxSignatureLines; lineIndex += 1) {
|
|
547
|
+
const lineStart = lineStarts[lineIndex] ?? 0;
|
|
548
|
+
const lineEnd = getLineEndOffset(file.content, lineStart);
|
|
549
|
+
const line = file.content.slice(lineStart, lineEnd);
|
|
550
|
+
signatureLines.push(line);
|
|
551
|
+
const openBraceColumn = line.indexOf('{');
|
|
552
|
+
const semicolonColumn = line.indexOf(';');
|
|
553
|
+
if (openBraceColumn >= 0 && (semicolonColumn < 0 || openBraceColumn < semicolonColumn)) {
|
|
554
|
+
return {
|
|
555
|
+
openBraceOffset: lineStart + openBraceColumn,
|
|
556
|
+
signature: signatureLines.join('\n'),
|
|
557
|
+
endLineIndex: lineIndex,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (semicolonColumn >= 0) {
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
function extractJavaMethodName(signature, currentClassName) {
|
|
567
|
+
const beforeParameters = signature.slice(0, signature.indexOf('(')).replace(/<[^<>]*(?:<[^<>]*>[^<>]*)*>/gu, ' ');
|
|
568
|
+
const nameMatch = /([A-Za-z_$][\w$]*)\s*$/u.exec(beforeParameters);
|
|
569
|
+
const name = nameMatch?.[1];
|
|
570
|
+
if (!name) {
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
573
|
+
if (['if', 'for', 'while', 'switch', 'catch', 'new', 'return'].includes(name)) {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
if (currentClassName && name === currentClassName) {
|
|
577
|
+
return name;
|
|
578
|
+
}
|
|
579
|
+
if (!/\b(?:public|private|protected)\b/u.test(beforeParameters)) {
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
return name;
|
|
583
|
+
}
|
|
584
|
+
function isJavaMethodSignatureCandidate(line) {
|
|
585
|
+
const trimmed = line.trimStart();
|
|
586
|
+
if (!/^(?:public|private|protected)\b/u.test(trimmed)) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
if (/\b(?:class|interface|enum|record)\b/u.test(trimmed)) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
if (!trimmed.includes('(')) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
function extractJavaMethods(file, classification) {
|
|
598
|
+
const methods = [];
|
|
599
|
+
const lineStarts = getLineStarts(file);
|
|
600
|
+
let pendingAnnotationStart;
|
|
601
|
+
let pendingAnnotationEnd;
|
|
602
|
+
let annotationParenDepth = 0;
|
|
603
|
+
let currentClassName;
|
|
604
|
+
for (let lineIndex = 0; lineIndex < lineStarts.length; lineIndex += 1) {
|
|
605
|
+
const lineStart = lineStarts[lineIndex] ?? 0;
|
|
606
|
+
const lineEnd = getLineEndOffset(file.content, lineStart);
|
|
607
|
+
const line = file.content.slice(lineStart, lineEnd);
|
|
608
|
+
const trimmed = line.trimStart();
|
|
609
|
+
const classMatch = /\b(?:class|interface|enum|record)\s+([A-Za-z_$][\w$]*)\b/u.exec(line);
|
|
610
|
+
if (classMatch?.[1]) {
|
|
611
|
+
currentClassName = classMatch[1];
|
|
612
|
+
}
|
|
613
|
+
if (trimmed.startsWith('@') || annotationParenDepth > 0) {
|
|
614
|
+
pendingAnnotationStart ??= lineStart;
|
|
615
|
+
pendingAnnotationEnd = lineEnd;
|
|
616
|
+
annotationParenDepth = Math.max(0, annotationParenDepth + countJavaAnnotationParenDelta(line));
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (trimmed.length === 0) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (!isJavaMethodSignatureCandidate(line)) {
|
|
623
|
+
pendingAnnotationStart = undefined;
|
|
624
|
+
pendingAnnotationEnd = undefined;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const openingBrace = findJavaOpeningBrace(file, lineIndex);
|
|
628
|
+
if (!openingBrace) {
|
|
629
|
+
pendingAnnotationStart = undefined;
|
|
630
|
+
pendingAnnotationEnd = undefined;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const methodName = extractJavaMethodName(openingBrace.signature, currentClassName);
|
|
634
|
+
if (!methodName) {
|
|
635
|
+
pendingAnnotationStart = undefined;
|
|
636
|
+
pendingAnnotationEnd = undefined;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const end = findMatchingBrace(file.content, openingBrace.openBraceOffset);
|
|
640
|
+
methods.push(createMethodEntry({
|
|
641
|
+
name: methodName,
|
|
642
|
+
...(currentClassName ? { className: currentClassName } : {}),
|
|
643
|
+
decorators: pendingAnnotationStart === undefined || pendingAnnotationEnd === undefined
|
|
644
|
+
? ''
|
|
645
|
+
: file.content.slice(pendingAnnotationStart, pendingAnnotationEnd),
|
|
646
|
+
bodyStartOffset: openingBrace.openBraceOffset,
|
|
647
|
+
bodyEndOffset: end + 1,
|
|
648
|
+
lineOffset: lineStart,
|
|
649
|
+
file,
|
|
650
|
+
classification,
|
|
651
|
+
}));
|
|
652
|
+
pendingAnnotationStart = undefined;
|
|
653
|
+
pendingAnnotationEnd = undefined;
|
|
654
|
+
lineIndex = openingBrace.endLineIndex;
|
|
655
|
+
}
|
|
656
|
+
return methods;
|
|
657
|
+
}
|
|
658
|
+
function isBraceMethodSignatureCandidate(line, extension) {
|
|
659
|
+
const trimmed = line.trimStart();
|
|
660
|
+
if (extension === '.cs') {
|
|
661
|
+
return /^(?:\[[^\]]+\]\s*)*(?:public|private|protected|internal)\b/u.test(trimmed) &&
|
|
662
|
+
trimmed.includes('(') &&
|
|
663
|
+
!/\b(?:class|interface|enum|record)\b/u.test(trimmed);
|
|
664
|
+
}
|
|
665
|
+
if (extension === '.php') {
|
|
666
|
+
return /^(?:(?:public|private|protected|static|final|abstract)\s+)*function\s+\w+\s*\(/u.test(trimmed);
|
|
667
|
+
}
|
|
668
|
+
if (extension === '.go') {
|
|
669
|
+
return /^func\s+(?:\([^)]*\)\s*)?[A-Za-z_]\w*\s*\(/u.test(trimmed);
|
|
670
|
+
}
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
function extractBraceMethodName(signature, extension) {
|
|
674
|
+
const trimmed = signature.trimStart();
|
|
675
|
+
if (extension === '.php') {
|
|
676
|
+
return /function\s+(\w+)\s*\(/u.exec(trimmed)?.[1];
|
|
677
|
+
}
|
|
678
|
+
if (extension === '.go') {
|
|
679
|
+
return /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/u.exec(trimmed)?.[1];
|
|
680
|
+
}
|
|
681
|
+
const beforeParameters = signature.slice(0, signature.indexOf('('));
|
|
682
|
+
const name = /([A-Za-z_]\w*)\s*$/u.exec(beforeParameters)?.[1];
|
|
683
|
+
return name && !['if', 'for', 'while', 'switch', 'catch', 'using', 'return'].includes(name) ? name : undefined;
|
|
684
|
+
}
|
|
685
|
+
function extractBraceMethods(file, classification) {
|
|
686
|
+
const methods = [];
|
|
687
|
+
const lineStarts = getLineStarts(file);
|
|
688
|
+
let pendingDecoratorStart;
|
|
689
|
+
let pendingDecoratorEnd;
|
|
690
|
+
let currentClassName;
|
|
691
|
+
for (let lineIndex = 0; lineIndex < lineStarts.length; lineIndex += 1) {
|
|
692
|
+
const lineStart = lineStarts[lineIndex] ?? 0;
|
|
693
|
+
const lineEnd = getLineEndOffset(file.content, lineStart);
|
|
694
|
+
const line = file.content.slice(lineStart, lineEnd);
|
|
695
|
+
const trimmed = line.trimStart();
|
|
696
|
+
const classMatch = /\bclass\s+([A-Za-z_]\w*)\b/u.exec(line);
|
|
697
|
+
if (classMatch?.[1]) {
|
|
698
|
+
currentClassName = classMatch[1];
|
|
699
|
+
}
|
|
700
|
+
if (/^(?:\[|#\[|@)/u.test(trimmed)) {
|
|
701
|
+
pendingDecoratorStart ??= lineStart;
|
|
702
|
+
pendingDecoratorEnd = lineEnd;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (trimmed.length === 0) {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (!isBraceMethodSignatureCandidate(line, file.extension)) {
|
|
709
|
+
pendingDecoratorStart = undefined;
|
|
710
|
+
pendingDecoratorEnd = undefined;
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const openingBrace = findJavaOpeningBrace(file, lineIndex, 12);
|
|
714
|
+
if (!openingBrace) {
|
|
715
|
+
pendingDecoratorStart = undefined;
|
|
716
|
+
pendingDecoratorEnd = undefined;
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const methodName = extractBraceMethodName(openingBrace.signature, file.extension);
|
|
720
|
+
if (!methodName) {
|
|
721
|
+
pendingDecoratorStart = undefined;
|
|
722
|
+
pendingDecoratorEnd = undefined;
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const end = findMatchingBrace(file.content, openingBrace.openBraceOffset);
|
|
726
|
+
methods.push(createMethodEntry({
|
|
727
|
+
name: methodName,
|
|
728
|
+
...(currentClassName ? { className: currentClassName } : {}),
|
|
729
|
+
decorators: pendingDecoratorStart === undefined || pendingDecoratorEnd === undefined
|
|
730
|
+
? ''
|
|
731
|
+
: file.content.slice(pendingDecoratorStart, pendingDecoratorEnd),
|
|
732
|
+
bodyStartOffset: openingBrace.openBraceOffset,
|
|
733
|
+
bodyEndOffset: end + 1,
|
|
734
|
+
lineOffset: lineStart,
|
|
735
|
+
file,
|
|
736
|
+
classification,
|
|
737
|
+
}));
|
|
738
|
+
pendingDecoratorStart = undefined;
|
|
739
|
+
pendingDecoratorEnd = undefined;
|
|
740
|
+
lineIndex = openingBrace.endLineIndex;
|
|
741
|
+
}
|
|
742
|
+
return methods;
|
|
743
|
+
}
|
|
744
|
+
function leadingWhitespaceLength(value) {
|
|
745
|
+
return value.length - value.trimStart().length;
|
|
746
|
+
}
|
|
747
|
+
function extractPythonMethods(file, classification) {
|
|
748
|
+
const methods = [];
|
|
749
|
+
const lineStarts = getLineStarts(file);
|
|
750
|
+
let pendingDecoratorStart;
|
|
751
|
+
let pendingDecoratorEnd;
|
|
752
|
+
let currentClassName;
|
|
753
|
+
for (let lineIndex = 0; lineIndex < lineStarts.length; lineIndex += 1) {
|
|
754
|
+
const lineStart = lineStarts[lineIndex] ?? 0;
|
|
755
|
+
const lineEnd = getLineEndOffset(file.content, lineStart);
|
|
756
|
+
const line = file.content.slice(lineStart, lineEnd);
|
|
757
|
+
const trimmed = line.trimStart();
|
|
758
|
+
const classMatch = /^class\s+([A-Za-z_]\w*)\b/u.exec(trimmed);
|
|
759
|
+
if (classMatch?.[1]) {
|
|
760
|
+
currentClassName = classMatch[1];
|
|
761
|
+
}
|
|
762
|
+
if (trimmed.startsWith('@')) {
|
|
763
|
+
pendingDecoratorStart ??= lineStart;
|
|
764
|
+
pendingDecoratorEnd = lineEnd;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
const methodMatch = /^(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\([^)]*\)\s*:/u.exec(trimmed);
|
|
768
|
+
if (!methodMatch?.[1]) {
|
|
769
|
+
if (trimmed.length > 0) {
|
|
770
|
+
pendingDecoratorStart = undefined;
|
|
771
|
+
pendingDecoratorEnd = undefined;
|
|
772
|
+
}
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const definitionIndent = leadingWhitespaceLength(line);
|
|
776
|
+
let bodyEndOffset = file.content.length;
|
|
777
|
+
for (let cursor = lineIndex + 1; cursor < lineStarts.length; cursor += 1) {
|
|
778
|
+
const cursorStart = lineStarts[cursor] ?? 0;
|
|
779
|
+
const cursorEnd = getLineEndOffset(file.content, cursorStart);
|
|
780
|
+
const cursorLine = file.content.slice(cursorStart, cursorEnd);
|
|
781
|
+
if (cursorLine.trim().length === 0) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (leadingWhitespaceLength(cursorLine) <= definitionIndent) {
|
|
785
|
+
bodyEndOffset = cursorStart;
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
methods.push(createMethodEntry({
|
|
790
|
+
name: methodMatch[1],
|
|
791
|
+
...(currentClassName ? { className: currentClassName } : {}),
|
|
792
|
+
decorators: pendingDecoratorStart === undefined || pendingDecoratorEnd === undefined
|
|
793
|
+
? ''
|
|
794
|
+
: file.content.slice(pendingDecoratorStart, pendingDecoratorEnd),
|
|
795
|
+
bodyStartOffset: lineStart,
|
|
796
|
+
bodyEndOffset,
|
|
797
|
+
lineOffset: lineStart,
|
|
798
|
+
file,
|
|
799
|
+
classification,
|
|
800
|
+
}));
|
|
801
|
+
pendingDecoratorStart = undefined;
|
|
802
|
+
pendingDecoratorEnd = undefined;
|
|
803
|
+
}
|
|
804
|
+
return methods;
|
|
805
|
+
}
|
|
806
|
+
function extractMethods(file, classification, astCache) {
|
|
807
|
+
if (file.extension === '.java') {
|
|
808
|
+
return extractJavaMethods(file, classification);
|
|
809
|
+
}
|
|
810
|
+
if (file.extension === '.py') {
|
|
811
|
+
return extractPythonMethods(file, classification);
|
|
812
|
+
}
|
|
813
|
+
if (file.extension === '.cs' || file.extension === '.php' || file.extension === '.go') {
|
|
814
|
+
return extractBraceMethods(file, classification);
|
|
815
|
+
}
|
|
816
|
+
return extractTypeScriptMethods(file, classification, astCache);
|
|
817
|
+
}
|
|
818
|
+
function emitSlowTransactionDomainFileProfile(label, file, durationMs, details) {
|
|
819
|
+
if (durationMs < 100) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
(0, validationProfiling_1.emitValidationProfile)(`AP-TXN/AP-DOM ${label} ${file.relativePath}`, durationMs, details);
|
|
823
|
+
}
|
|
824
|
+
function createFinding(input) {
|
|
825
|
+
return {
|
|
826
|
+
id: input.id,
|
|
827
|
+
severity: 'warning',
|
|
828
|
+
passed: false,
|
|
829
|
+
message: input.message,
|
|
830
|
+
findingType: 'file-scope',
|
|
831
|
+
filePath: input.file.relativePath,
|
|
832
|
+
sourceFile: input.file.relativePath,
|
|
833
|
+
...(input.line ? { sourceLine: input.line } : {}),
|
|
834
|
+
...(input.module ? { module: input.module } : {}),
|
|
835
|
+
...(input.target ? { target: input.target } : {}),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function hasRole(classification, role) {
|
|
839
|
+
return classification.roles.includes(role);
|
|
840
|
+
}
|
|
841
|
+
function hasExplicitDomainModelEvidence(file) {
|
|
842
|
+
const code = maskNonCodeEvidence(file.content);
|
|
843
|
+
return explicitDomainModelEvidencePattern.test(code) || /`[^`]*(?:gorm|db):/u.test(file.content);
|
|
844
|
+
}
|
|
845
|
+
function hasDomainFrameworkLeakEvidence(file) {
|
|
846
|
+
return domainFrameworkLeakPattern.test(maskNonCodeEvidence(file.content));
|
|
847
|
+
}
|
|
848
|
+
function hasTransactionEvidence(method) {
|
|
849
|
+
return transactionEvidencePattern.test(method.decorators) || transactionEvidencePattern.test(maskNonCodeEvidence(method.body));
|
|
850
|
+
}
|
|
851
|
+
function hasTransactionBoundaryDecorator(method) {
|
|
852
|
+
return transactionBoundaryDecoratorPattern.test(method.decorators);
|
|
853
|
+
}
|
|
854
|
+
function hasExplicitTransactionBoundaryCreation(method) {
|
|
855
|
+
return explicitTransactionBoundaryPattern.test(maskNonCodeEvidence(method.body));
|
|
856
|
+
}
|
|
857
|
+
function isApplicationTransactionScope(method) {
|
|
858
|
+
return hasTransactionBoundaryDecorator(method) || hasExplicitTransactionBoundaryCreation(method);
|
|
859
|
+
}
|
|
860
|
+
function isApplicationTransactionRuleScope(method) {
|
|
861
|
+
return (0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(method.classification.scope);
|
|
862
|
+
}
|
|
863
|
+
function getTransactionBoundaryMethodKey(method) {
|
|
864
|
+
if (!method.className || !hasTransactionBoundaryDecorator(method)) {
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
return `${method.file.relativePath}#${method.className}.${method.name}`;
|
|
868
|
+
}
|
|
869
|
+
function callsKnownLocalTransactionBoundaryMethod(method, transactionBoundaryMethodKeys) {
|
|
870
|
+
if (!method.className) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
const codeBody = maskNonCodeEvidence(method.body);
|
|
874
|
+
for (const key of transactionBoundaryMethodKeys) {
|
|
875
|
+
const prefix = `${method.file.relativePath}#${method.className}.`;
|
|
876
|
+
if (!key.startsWith(prefix)) {
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const name = key.slice(prefix.length);
|
|
880
|
+
if (name === method.name) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
if (new RegExp(`\\bthis\\.${name}\\s*\\(`, 'u').test(codeBody)) {
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
function hasNestedTransactionBoundaryEvidence(method, transactionBoundaryMethodKeys) {
|
|
890
|
+
const codeBody = maskNonCodeEvidence(method.body);
|
|
891
|
+
return (separateTransactionPropagationPattern.test(codeBody) ||
|
|
892
|
+
(hasTransactionBoundaryDecorator(method) && hasExplicitTransactionBoundaryCreation(method)) ||
|
|
893
|
+
callsKnownLocalTransactionBoundaryMethod(method, transactionBoundaryMethodKeys));
|
|
894
|
+
}
|
|
895
|
+
function isConstructorEntry(method) {
|
|
896
|
+
return method.name === 'constructor' || (!!method.className && method.name === method.className);
|
|
897
|
+
}
|
|
898
|
+
function hasExplicitConstructorTransactionEvidence(method) {
|
|
899
|
+
return isConstructorEntry(method) && transactionEvidencePattern.test(method.decorators);
|
|
900
|
+
}
|
|
901
|
+
function hasLongRunningTransactionWorkflowEvidence(method) {
|
|
902
|
+
const codeBody = maskNonCodeEvidence(method.body);
|
|
903
|
+
if (hasTransactionBoundaryDecorator(method) && longRunningPattern.test(codeBody)) {
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
if (hasExplicitTransactionBoundaryCreation(method) && longRunningPattern.test(codeBody)) {
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
const callbackBodies = extractTransactionCallbackBodies(codeBody);
|
|
910
|
+
return callbackBodies.some((callbackBody) => longRunningPattern.test(callbackBody));
|
|
911
|
+
}
|
|
912
|
+
function extractRepositoryStems(value) {
|
|
913
|
+
const stems = new Set();
|
|
914
|
+
for (const match of value.matchAll(/\b(?:this\.)?(\w+?)(?:Repository|Repo|Dao)\b/gu)) {
|
|
915
|
+
const stem = match[1];
|
|
916
|
+
if (stem && !/^(base|generic|crud)$/iu.test(stem)) {
|
|
917
|
+
stems.add(stem.toLowerCase());
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return [...stems];
|
|
921
|
+
}
|
|
922
|
+
function extractMutatedRepositoryStems(value) {
|
|
923
|
+
const stems = new Set();
|
|
924
|
+
const pattern = /\b(?:this\.)?(\w+?)(?:Repository|Repo|Dao)\.(\w+)\s*\(/gu;
|
|
925
|
+
for (const match of value.matchAll(pattern)) {
|
|
926
|
+
const operation = match[2] ?? '';
|
|
927
|
+
if (/^(save|create|insert|update|delete|remove|persist|merge)/iu.test(operation)) {
|
|
928
|
+
const stem = match[1];
|
|
929
|
+
if (stem) {
|
|
930
|
+
stems.add(stem.toLowerCase());
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return [...stems];
|
|
935
|
+
}
|
|
936
|
+
function mapStemToModule(stem, contract) {
|
|
937
|
+
const normalizedStem = stem.replace(/[^a-z0-9]/giu, '').toLowerCase();
|
|
938
|
+
for (const moduleName of Object.keys(contract.modules ?? {})) {
|
|
939
|
+
const normalizedModule = moduleName.replace(/[^a-z0-9]/giu, '').toLowerCase();
|
|
940
|
+
if (normalizedModule === normalizedStem || normalizedModule.endsWith(normalizedStem) || normalizedStem.endsWith(normalizedModule)) {
|
|
941
|
+
return moduleName;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return undefined;
|
|
945
|
+
}
|
|
946
|
+
function isCrossAggregateModificationRisk(method, contract) {
|
|
947
|
+
const mutatedStems = extractMutatedRepositoryStems(method.body);
|
|
948
|
+
const mutatedModules = new Set(mutatedStems.map((stem) => mapStemToModule(stem, contract)).filter(Boolean));
|
|
949
|
+
return (hasTransactionEvidence(method) &&
|
|
950
|
+
hasRole(method.classification, 'service') &&
|
|
951
|
+
(mutatedStems.length >= 3 || mutatedModules.size >= 2));
|
|
952
|
+
}
|
|
953
|
+
function detectTransactionFindings(methods, contract) {
|
|
954
|
+
const transactionBoundaryMethodKeys = new Set(methods.map(getTransactionBoundaryMethodKey).filter((key) => !!key));
|
|
955
|
+
const findings = [];
|
|
956
|
+
for (const method of methods) {
|
|
957
|
+
const isTransactional = hasTransactionEvidence(method);
|
|
958
|
+
if (!isTransactional) {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (!isApplicationTransactionRuleScope(method)) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const isConstructor = isConstructorEntry(method);
|
|
965
|
+
if (isConstructor && !hasExplicitConstructorTransactionEvidence(method)) {
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (hasRole(method.classification, 'controller')) {
|
|
969
|
+
findings.push(createFinding({
|
|
970
|
+
id: RuleIds.TXN_IN_CONTROLLER,
|
|
971
|
+
message: `Endpoint/controller method '${method.name}' appears to own a transaction boundary.`,
|
|
972
|
+
file: method.file,
|
|
973
|
+
line: method.line,
|
|
974
|
+
module: method.classification.moduleName,
|
|
975
|
+
target: method.name,
|
|
976
|
+
}));
|
|
977
|
+
}
|
|
978
|
+
const repositoryStems = extractRepositoryStems(method.body);
|
|
979
|
+
if (!isConstructor &&
|
|
980
|
+
hasRole(method.classification, 'service') &&
|
|
981
|
+
(repositoryStems.length >= 4 || (repositoryStems.length >= 3 && method.body.split(/\r?\n/u).length >= 80))) {
|
|
982
|
+
findings.push(createFinding({
|
|
983
|
+
id: RuleIds.TXN_BOUNDARY_TOO_BROAD,
|
|
984
|
+
message: `Transactional method '${method.name}' coordinates many repository/aggregate collaborators (${repositoryStems.join(', ')}).`,
|
|
985
|
+
file: method.file,
|
|
986
|
+
line: method.line,
|
|
987
|
+
module: method.classification.moduleName,
|
|
988
|
+
target: method.name,
|
|
989
|
+
}));
|
|
990
|
+
}
|
|
991
|
+
if (!isConstructor &&
|
|
992
|
+
isApplicationTransactionScope(method) &&
|
|
993
|
+
hasNestedTransactionBoundaryEvidence(method, transactionBoundaryMethodKeys)) {
|
|
994
|
+
findings.push(createFinding({
|
|
995
|
+
id: RuleIds.TXN_NESTED_RISK,
|
|
996
|
+
message: `Transactional method '${method.name}' appears to start or call another transaction scope.`,
|
|
997
|
+
file: method.file,
|
|
998
|
+
line: method.line,
|
|
999
|
+
module: method.classification.moduleName,
|
|
1000
|
+
target: method.name,
|
|
1001
|
+
}));
|
|
1002
|
+
}
|
|
1003
|
+
if (!isConstructor && hasLongRunningTransactionWorkflowEvidence(method)) {
|
|
1004
|
+
findings.push(createFinding({
|
|
1005
|
+
id: RuleIds.TXN_LONG_RUNNING_WORKFLOW,
|
|
1006
|
+
message: `Transactional method '${method.name}' contains external I/O or long-running workflow evidence.`,
|
|
1007
|
+
file: method.file,
|
|
1008
|
+
line: method.line,
|
|
1009
|
+
module: method.classification.moduleName,
|
|
1010
|
+
target: method.name,
|
|
1011
|
+
}));
|
|
1012
|
+
}
|
|
1013
|
+
const mutatedStems = extractMutatedRepositoryStems(method.body);
|
|
1014
|
+
if (!isConstructor && isCrossAggregateModificationRisk(method, contract)) {
|
|
1015
|
+
findings.push(createFinding({
|
|
1016
|
+
id: RuleIds.DOM_CROSS_AGGREGATE_MODIFICATION,
|
|
1017
|
+
message: `Transactional method '${method.name}' mutates multiple aggregate/repository roots (${mutatedStems.join(', ')}).`,
|
|
1018
|
+
file: method.file,
|
|
1019
|
+
line: method.line,
|
|
1020
|
+
module: method.classification.moduleName,
|
|
1021
|
+
target: method.name,
|
|
1022
|
+
}));
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return findings;
|
|
1026
|
+
}
|
|
1027
|
+
function countTypeScriptClassMembers(file, astCache) {
|
|
1028
|
+
const sourceFile = parseSourceFile(file, astCache);
|
|
1029
|
+
let fields = 0;
|
|
1030
|
+
let methods = 0;
|
|
1031
|
+
const visit = (node) => {
|
|
1032
|
+
if (ts.isClassDeclaration(node)) {
|
|
1033
|
+
for (const member of node.members) {
|
|
1034
|
+
if (ts.isPropertyDeclaration(member)) {
|
|
1035
|
+
fields += 1;
|
|
1036
|
+
}
|
|
1037
|
+
if (ts.isMethodDeclaration(member)) {
|
|
1038
|
+
methods += 1;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
ts.forEachChild(node, visit);
|
|
1043
|
+
};
|
|
1044
|
+
visit(sourceFile);
|
|
1045
|
+
return { fields, methods };
|
|
1046
|
+
}
|
|
1047
|
+
function countJavaClassMembers(content) {
|
|
1048
|
+
return {
|
|
1049
|
+
fields: [...content.matchAll(/\b(?:private|protected|public)\s+(?!class\b)[\w<>, ?\[\]]+\s+\w+\s*;/gu)].length,
|
|
1050
|
+
methods: [...content.matchAll(/\b(?:public|private|protected)\s+(?!class\b)[\w<>, ?\[\]]+\s+\w+\s*\([^)]*\)\s*\{/gu)].length,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function countDomainModelMembers(file, astCache) {
|
|
1054
|
+
if (file.extension === '.java') {
|
|
1055
|
+
return countJavaClassMembers(file.content);
|
|
1056
|
+
}
|
|
1057
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(file.extension)) {
|
|
1058
|
+
return countTypeScriptClassMembers(file, astCache);
|
|
1059
|
+
}
|
|
1060
|
+
if (file.extension === '.cs') {
|
|
1061
|
+
return {
|
|
1062
|
+
fields: [...file.content.matchAll(/\b(?:public|private|protected|internal)\s+[\w<>?\[\]]+\s+\w+\s*(?:\{\s*get;|\s*[;=])/gu)].length,
|
|
1063
|
+
methods: [...file.content.matchAll(/\b(?:public|private|protected|internal)\s+[\w<>?\[\]]+\s+\w+\s*\([^)]*\)\s*\{/gu)].length,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
if (file.extension === '.py') {
|
|
1067
|
+
return {
|
|
1068
|
+
fields: [...file.content.matchAll(/^\s{4}[A-Za-z_]\w*\s*:/gmu)].length,
|
|
1069
|
+
methods: [...file.content.matchAll(/^\s{4}(?:async\s+)?def\s+\w+\s*\(/gmu)].length,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
if (file.extension === '.php') {
|
|
1073
|
+
const fillableMatch = /\$fillable\s*=\s*\[([\s\S]*?)\]/u.exec(file.content);
|
|
1074
|
+
const fillableFields = fillableMatch?.[1]
|
|
1075
|
+
? [...fillableMatch[1].matchAll(/['"][^'"]+['"]/gu)].length
|
|
1076
|
+
: 0;
|
|
1077
|
+
return {
|
|
1078
|
+
fields: [...file.content.matchAll(/\b(?:public|private|protected)\s+(?:[\w\\]+\s+)?\$\w+\s*(?:=|;)/gu)].length + fillableFields,
|
|
1079
|
+
methods: [...file.content.matchAll(/\bfunction\s+\w+\s*\(/gu)].length,
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
if (file.extension === '.go') {
|
|
1083
|
+
const structMatch = /\bstruct\s*\{([\s\S]*?)\n\}/u.exec(file.content);
|
|
1084
|
+
const structBody = structMatch?.[1] ?? '';
|
|
1085
|
+
return {
|
|
1086
|
+
fields: [...structBody.matchAll(/^\s*[A-Z]\w*\s+[\w*.[\]]+/gmu)].length,
|
|
1087
|
+
methods: [...file.content.matchAll(/^\s*func\s+\([^)]*\)\s+\w+\s*\(/gmu)].length,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
return { fields: 0, methods: 0 };
|
|
1091
|
+
}
|
|
1092
|
+
function countLegacyTypeScriptOperationMatches(file) {
|
|
1093
|
+
file.legacyOperationCount ??= [...file.content.matchAll(/((?:\s*@\w+(?:\([^)]*\))?[^\n]*\n)*)\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{/gu)].length;
|
|
1094
|
+
return file.legacyOperationCount;
|
|
1095
|
+
}
|
|
1096
|
+
function countServiceOperations(file, serviceMethods) {
|
|
1097
|
+
return file.extension === '.java' ? serviceMethods.length : countLegacyTypeScriptOperationMatches(file);
|
|
1098
|
+
}
|
|
1099
|
+
function countTypeScriptConstructorParameters(file, astCache) {
|
|
1100
|
+
const sourceFile = parseSourceFile(file, astCache);
|
|
1101
|
+
let count = 0;
|
|
1102
|
+
const visit = (node) => {
|
|
1103
|
+
if (ts.isConstructorDeclaration(node)) {
|
|
1104
|
+
count += node.parameters.length;
|
|
1105
|
+
}
|
|
1106
|
+
ts.forEachChild(node, visit);
|
|
1107
|
+
};
|
|
1108
|
+
visit(sourceFile);
|
|
1109
|
+
return count;
|
|
1110
|
+
}
|
|
1111
|
+
function countJavaInjectedCollaborators(content) {
|
|
1112
|
+
return [...content.matchAll(/@(Autowired|Inject)\b|private\s+final\s+\w+(?:<[^>]+>)?\s+\w+\s*;/gu)].length;
|
|
1113
|
+
}
|
|
1114
|
+
function sortUnique(values) {
|
|
1115
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
1116
|
+
}
|
|
1117
|
+
function isDbModelEvidenceToken(value) {
|
|
1118
|
+
const normalized = value.trim();
|
|
1119
|
+
if (normalized.length <= 1) {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(normalized)) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
const lower = normalized.toLowerCase();
|
|
1126
|
+
const excluded = new Set([
|
|
1127
|
+
'a',
|
|
1128
|
+
'all',
|
|
1129
|
+
'an',
|
|
1130
|
+
'doing',
|
|
1131
|
+
'first',
|
|
1132
|
+
'is',
|
|
1133
|
+
'outside',
|
|
1134
|
+
'resend',
|
|
1135
|
+
'scratch',
|
|
1136
|
+
'swagger',
|
|
1137
|
+
'the',
|
|
1138
|
+
]);
|
|
1139
|
+
if (excluded.has(lower)) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
return !/(?:client|provider|service|controller|guard|swagger|openapi)$/iu.test(normalized);
|
|
1143
|
+
}
|
|
1144
|
+
function isStructuredSqlTableEvidence(value, keyword, quote, following = '') {
|
|
1145
|
+
const normalized = value.trim();
|
|
1146
|
+
const hasStructuralEvidence = /^(?:from|update|into)$/iu.test(keyword) ||
|
|
1147
|
+
quote !== undefined && quote.length > 0 ||
|
|
1148
|
+
/[_.]/u.test(normalized) ||
|
|
1149
|
+
(/^join$/iu.test(keyword) && /\bon\b/iu.test(following));
|
|
1150
|
+
return hasStructuralEvidence && isDbModelEvidenceToken(normalized);
|
|
1151
|
+
}
|
|
1152
|
+
function extractSqlLikeLiterals(content) {
|
|
1153
|
+
return [...content.matchAll(/(['"`])([\s\S]{0,1200}?)\1/gu)]
|
|
1154
|
+
.map((match) => match[2] ?? '')
|
|
1155
|
+
.filter((literal) => /\b(?:select|update|delete|insert)\b[\s\S]{0,400}\b(?:from|join|into|update)\b/iu.test(literal));
|
|
1156
|
+
}
|
|
1157
|
+
function extractDbModelsTouched(content) {
|
|
1158
|
+
const models = new Set();
|
|
1159
|
+
for (const match of content.matchAll(/\b(?:this\.)?prisma\.([a-z]\w*)\./giu)) {
|
|
1160
|
+
if (match[1] && isDbModelEvidenceToken(match[1])) {
|
|
1161
|
+
models.add(match[1]);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
for (const sqlLiteral of extractSqlLikeLiterals(content)) {
|
|
1165
|
+
for (const match of sqlLiteral.matchAll(/\b(?:from|join|update|into)\s+(["`]?)([A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)?)\1/giu)) {
|
|
1166
|
+
const keyword = /\b(from|join|update|into)\b/iu.exec(match[0])?.[1] ?? '';
|
|
1167
|
+
const following = sqlLiteral.slice((match.index ?? 0) + match[0].length, (match.index ?? 0) + match[0].length + 120);
|
|
1168
|
+
if (match[2] && isStructuredSqlTableEvidence(match[2], keyword, match[1], following)) {
|
|
1169
|
+
models.add(match[2]);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return sortUnique(models).slice(0, 6);
|
|
1174
|
+
}
|
|
1175
|
+
function extractExternalIntegrations(content) {
|
|
1176
|
+
const integrations = new Set();
|
|
1177
|
+
const patterns = [
|
|
1178
|
+
[/\b(?:HttpService|httpClient|RestTemplate|WebClient|axios|fetch)\b/iu, 'http'],
|
|
1179
|
+
[/\b(?:Mailer|mailClient|emailClient|sendEmail|send\w*Email)\b/iu, 'email'],
|
|
1180
|
+
[/\b(?:Kafka|Rabbit|Sqs|Queue|EventEmitter|ApplicationEventPublisher)\b/iu, 'messaging'],
|
|
1181
|
+
[/\b(?:Paddle|Stripe|Checkout|PortalSession)\b/iu, 'billing-provider'],
|
|
1182
|
+
[/\b(?:S3|putObject|upload)\b/iu, 'object-storage'],
|
|
1183
|
+
];
|
|
1184
|
+
for (const [pattern, label] of patterns) {
|
|
1185
|
+
if (pattern.test(content)) {
|
|
1186
|
+
integrations.add(label);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return sortUnique(integrations);
|
|
1190
|
+
}
|
|
1191
|
+
function countPersistenceCollaborators(content) {
|
|
1192
|
+
const collaborators = new Set();
|
|
1193
|
+
for (const match of content.matchAll(/\b(?:private\s+readonly\s+|private\s+|readonly\s+)?(\w*(?:Repository|Repo|Dao|Prisma|DataSource|EntityManager|JdbcTemplate))\b/gu)) {
|
|
1194
|
+
if (match[1]) {
|
|
1195
|
+
collaborators.add(match[1]);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return collaborators.size;
|
|
1199
|
+
}
|
|
1200
|
+
function summarizeServiceEvidence(file, serviceMethods, repositoryCollaborators, astCache) {
|
|
1201
|
+
const methodCount = countServiceOperations(file, serviceMethods);
|
|
1202
|
+
const injectedCollaborators = file.extension === '.java'
|
|
1203
|
+
? countJavaInjectedCollaborators(file.content)
|
|
1204
|
+
: countTypeScriptConstructorParameters(file, astCache);
|
|
1205
|
+
return {
|
|
1206
|
+
lines: file.content.split(/\r?\n/u).length,
|
|
1207
|
+
publicMethods: methodCount,
|
|
1208
|
+
injectedCollaborators,
|
|
1209
|
+
repositoryCollaborators: repositoryCollaborators.length,
|
|
1210
|
+
persistenceCollaborators: countPersistenceCollaborators(file.content),
|
|
1211
|
+
dbModelsTouched: extractDbModelsTouched(file.content),
|
|
1212
|
+
externalIntegrations: extractExternalIntegrations(file.content),
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
function formatServiceEvidence(evidence) {
|
|
1216
|
+
const parts = [
|
|
1217
|
+
`loc=${evidence.lines}`,
|
|
1218
|
+
`methods=${evidence.publicMethods}`,
|
|
1219
|
+
`collaborators=${evidence.injectedCollaborators}`,
|
|
1220
|
+
`repositories=${evidence.repositoryCollaborators}`,
|
|
1221
|
+
`persistence=${evidence.persistenceCollaborators}`,
|
|
1222
|
+
];
|
|
1223
|
+
if (evidence.dbModelsTouched.length > 0) {
|
|
1224
|
+
parts.push(`db=${evidence.dbModelsTouched.join('|')}`);
|
|
1225
|
+
}
|
|
1226
|
+
if (evidence.externalIntegrations.length > 0) {
|
|
1227
|
+
parts.push(`external=${evidence.externalIntegrations.join('|')}`);
|
|
1228
|
+
}
|
|
1229
|
+
return parts.join(', ');
|
|
1230
|
+
}
|
|
1231
|
+
function shouldSkipAnemicDomainModelFinding(file) {
|
|
1232
|
+
const baseName = path.basename(file.relativePath, path.extname(file.relativePath));
|
|
1233
|
+
return (/^BaseEntity$/u.test(baseName) ||
|
|
1234
|
+
/(?:View|Projection|Dto|Response|Translation|Record|Cache|Job|Schedule|Scheduler|Session|History|Log|Audit)$/u.test(baseName) ||
|
|
1235
|
+
/(^|\/)(?:dto|dtos|view|views|projection|projections|response|responses|records?|cache|job-queue|scheduler)\//iu.test(file.relativePath));
|
|
1236
|
+
}
|
|
1237
|
+
function getDomainEntityNames(file, astCache) {
|
|
1238
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(file.extension)) {
|
|
1239
|
+
const match = /\b(?:class|interface|type|struct)\s+([A-Z]\w*)\b/u.exec(file.content);
|
|
1240
|
+
return match?.[1] ? [match[1]] : [];
|
|
1241
|
+
}
|
|
1242
|
+
const sourceFile = parseSourceFile(file, astCache);
|
|
1243
|
+
const names = new Set();
|
|
1244
|
+
const visit = (node) => {
|
|
1245
|
+
if ((ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && node.name) {
|
|
1246
|
+
names.add(node.name.text);
|
|
1247
|
+
}
|
|
1248
|
+
ts.forEachChild(node, visit);
|
|
1249
|
+
};
|
|
1250
|
+
visit(sourceFile);
|
|
1251
|
+
return [...names];
|
|
1252
|
+
}
|
|
1253
|
+
function hasStrongAggregateEvidence(file, entityNames) {
|
|
1254
|
+
const lowerPath = file.relativePath.toLowerCase();
|
|
1255
|
+
return (/(^|\/)(?:domain|aggregates?)\//u.test(lowerPath) ||
|
|
1256
|
+
entityNames.some((name) => /Aggregate$/u.test(name)) ||
|
|
1257
|
+
/\bAggregateRoot\b|@Aggregate\b/u.test(file.content));
|
|
1258
|
+
}
|
|
1259
|
+
function hasEntitySpecificServiceBusinessRules(entityNames, methods) {
|
|
1260
|
+
if (entityNames.length === 0) {
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
const entityAliases = new Set(entityNames);
|
|
1264
|
+
for (const entityName of entityNames) {
|
|
1265
|
+
if (/(?:Entity|Model|Aggregate)$/u.test(entityName)) {
|
|
1266
|
+
const stem = entityName.replace(/(?:Entity|Model|Aggregate)$/u, '');
|
|
1267
|
+
if (stem.length >= 3) {
|
|
1268
|
+
entityAliases.add(stem);
|
|
1269
|
+
entityAliases.add(stem.charAt(0).toLowerCase() + stem.slice(1));
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return methods.some((method) => {
|
|
1274
|
+
if (!hasRole(method.classification, 'service') || !businessRulePattern.test(method.body)) {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
return [...entityAliases].some((entityName) => new RegExp(`\\b${entityName}\\b`, 'u').test(method.file.content));
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
function detectDomainFindings(files, classifications, methods, contract, astCache) {
|
|
1281
|
+
const findings = [];
|
|
1282
|
+
for (const file of files) {
|
|
1283
|
+
const classification = classifications.get(file.relativePath);
|
|
1284
|
+
if (!classification) {
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
const lowerPath = file.relativePath.toLowerCase();
|
|
1288
|
+
if (hasRole(classification, 'entity') &&
|
|
1289
|
+
(0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(classification.scope) &&
|
|
1290
|
+
hasExplicitDomainModelEvidence(file) &&
|
|
1291
|
+
!/(dto|request|response|viewmodel|projection)/u.test(lowerPath) &&
|
|
1292
|
+
!shouldSkipAnemicDomainModelFinding(file)) {
|
|
1293
|
+
const entityNames = getDomainEntityNames(file, astCache);
|
|
1294
|
+
const counts = countDomainModelMembers(file, astCache);
|
|
1295
|
+
if (counts.fields >= 4 &&
|
|
1296
|
+
counts.methods <= 1 &&
|
|
1297
|
+
hasStrongAggregateEvidence(file, entityNames) &&
|
|
1298
|
+
hasEntitySpecificServiceBusinessRules(entityNames, methods)) {
|
|
1299
|
+
findings.push(createFinding({
|
|
1300
|
+
id: RuleIds.DOM_ANEMIC_MODEL,
|
|
1301
|
+
message: 'Domain/entity model is mostly state while business rule evidence lives in services.',
|
|
1302
|
+
file,
|
|
1303
|
+
line: 1,
|
|
1304
|
+
module: classification.moduleName,
|
|
1305
|
+
target: 'domain-model',
|
|
1306
|
+
}));
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (hasRole(classification, 'entity') &&
|
|
1310
|
+
(0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(classification.scope) &&
|
|
1311
|
+
hasExplicitDomainModelEvidence(file) &&
|
|
1312
|
+
hasDomainFrameworkLeakEvidence(file) &&
|
|
1313
|
+
!/(dto|request|response|viewmodel|projection)/u.test(lowerPath)) {
|
|
1314
|
+
findings.push(createFinding({
|
|
1315
|
+
id: RuleIds.DOM_LOGIC_SCATTERED,
|
|
1316
|
+
message: 'Domain/entity model depends on web, controller, repository, or infrastructure framework concerns.',
|
|
1317
|
+
file,
|
|
1318
|
+
line: 1,
|
|
1319
|
+
module: classification.moduleName,
|
|
1320
|
+
target: 'domain-model',
|
|
1321
|
+
}));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const oversizedServiceFindingsByFile = new Map();
|
|
1325
|
+
for (const method of methods) {
|
|
1326
|
+
const repositoryStems = extractRepositoryStems(method.body);
|
|
1327
|
+
const mutatedStems = extractMutatedRepositoryStems(method.body);
|
|
1328
|
+
const mutatedModules = new Set(mutatedStems.map((stem) => mapStemToModule(stem, contract)).filter((moduleName) => !!moduleName));
|
|
1329
|
+
if (hasRole(method.classification, 'service') &&
|
|
1330
|
+
method.classification.moduleName &&
|
|
1331
|
+
[...mutatedModules].some((moduleName) => moduleName !== method.classification.moduleName) &&
|
|
1332
|
+
!isCrossAggregateModificationRisk(method, contract)) {
|
|
1333
|
+
findings.push(createFinding({
|
|
1334
|
+
id: RuleIds.DOM_AGGREGATE_BOUNDARY_VIOLATION,
|
|
1335
|
+
message: `Service method '${method.name}' appears to reach into another aggregate/module repository directly.`,
|
|
1336
|
+
file: method.file,
|
|
1337
|
+
line: method.line,
|
|
1338
|
+
module: method.classification.moduleName,
|
|
1339
|
+
target: method.name,
|
|
1340
|
+
}));
|
|
1341
|
+
}
|
|
1342
|
+
if (hasRole(method.classification, 'service')) {
|
|
1343
|
+
if (!(0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(method.classification.scope)) {
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
const serviceMethods = methods.filter((candidate) => candidate.file.relativePath === method.file.relativePath);
|
|
1347
|
+
const serviceRepositoryStems = sortUnique(serviceMethods.flatMap((candidate) => extractRepositoryStems(candidate.body)));
|
|
1348
|
+
const publicOperationCount = countServiceOperations(method.file, serviceMethods);
|
|
1349
|
+
if ((serviceRepositoryStems.length >= 6 || publicOperationCount >= 18) &&
|
|
1350
|
+
!oversizedServiceFindingsByFile.has(method.file.relativePath)) {
|
|
1351
|
+
const evidence = summarizeServiceEvidence(method.file, serviceMethods, serviceRepositoryStems, astCache);
|
|
1352
|
+
oversizedServiceFindingsByFile.set(method.file.relativePath, createFinding({
|
|
1353
|
+
id: RuleIds.DOM_OVERSIZED_BOUNDARY,
|
|
1354
|
+
message: `Domain/application service has enough aggregate collaborators or operations to suggest an oversized ownership boundary (${formatServiceEvidence(evidence)}).`,
|
|
1355
|
+
file: method.file,
|
|
1356
|
+
line: 1,
|
|
1357
|
+
module: method.classification.moduleName,
|
|
1358
|
+
target: 'service',
|
|
1359
|
+
}));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
if ((0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(method.classification.scope) &&
|
|
1363
|
+
(hasRole(method.classification, 'controller') || hasRole(method.classification, 'repository')) &&
|
|
1364
|
+
businessRulePattern.test(method.body) &&
|
|
1365
|
+
/\bif\s*\(|\bswitch\s*\(|\bthrow\s+new\b/u.test(method.body)) {
|
|
1366
|
+
findings.push(createFinding({
|
|
1367
|
+
id: RuleIds.DOM_LOGIC_SCATTERED,
|
|
1368
|
+
message: `Business rule evidence appears in a ${hasRole(method.classification, 'controller') ? 'controller' : 'repository'} method '${method.name}'.`,
|
|
1369
|
+
file: method.file,
|
|
1370
|
+
line: method.line,
|
|
1371
|
+
module: method.classification.moduleName,
|
|
1372
|
+
target: method.name,
|
|
1373
|
+
}));
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
findings.push(...oversizedServiceFindingsByFile.values());
|
|
1377
|
+
return findings;
|
|
1378
|
+
}
|
|
1379
|
+
async function detectTransactionDomainFindings(input) {
|
|
1380
|
+
void input.config;
|
|
1381
|
+
const disabled = input.disabledRuleIds ?? new Set();
|
|
1382
|
+
const scanStarted = Date.now();
|
|
1383
|
+
const files = await collectSourceFiles(input.workspaceRoot, input.sourceInventory);
|
|
1384
|
+
(0, validationProfiling_1.emitValidationProfile)('AP-TXN/AP-DOM analyzer file scan', Date.now() - scanStarted, [
|
|
1385
|
+
{ name: 'files', value: files.length },
|
|
1386
|
+
]);
|
|
1387
|
+
const classifications = await (0, validationProfiling_1.profileValidationPhase)('AP-TXN/AP-DOM classify source files', () => Promise.resolve(new Map(files.map((file) => [file.relativePath, classifySourceFile(file, input.contract, input.astCache)]))), (result) => [{ name: 'files', value: result.size }]);
|
|
1388
|
+
const methods = await (0, validationProfiling_1.profileValidationPhase)('AP-TXN/AP-DOM extract methods', () => Promise.resolve(files.flatMap((file) => {
|
|
1389
|
+
const classification = classifications.get(file.relativePath);
|
|
1390
|
+
if (!classification) {
|
|
1391
|
+
return [];
|
|
1392
|
+
}
|
|
1393
|
+
const fileStarted = Date.now();
|
|
1394
|
+
const extracted = extractMethods(file, classification, input.astCache);
|
|
1395
|
+
emitSlowTransactionDomainFileProfile('extract methods', file, Date.now() - fileStarted, [
|
|
1396
|
+
{ name: 'methods', value: extracted.length },
|
|
1397
|
+
{ name: 'bytes', value: file.content.length },
|
|
1398
|
+
]);
|
|
1399
|
+
return extracted;
|
|
1400
|
+
})), (result) => [{ name: 'methods', value: result.length }]);
|
|
1401
|
+
const transactionFindings = await (0, validationProfiling_1.profileValidationPhase)('AP-TXN transaction findings', () => Promise.resolve(detectTransactionFindings(methods, input.contract)), (result) => [{ name: 'findings', value: result.length }]);
|
|
1402
|
+
const domainFindings = await (0, validationProfiling_1.profileValidationPhase)('AP-DOM domain findings', () => Promise.resolve(detectDomainFindings(files, classifications, methods, input.contract, input.astCache)), (result) => [{ name: 'findings', value: result.length }]);
|
|
1403
|
+
return [
|
|
1404
|
+
...transactionFindings,
|
|
1405
|
+
...domainFindings,
|
|
1406
|
+
]
|
|
1407
|
+
.filter((finding) => !disabled.has(finding.id))
|
|
1408
|
+
.sort((left, right) => {
|
|
1409
|
+
const fileCompare = (left.sourceFile ?? '').localeCompare(right.sourceFile ?? '');
|
|
1410
|
+
if (fileCompare !== 0) {
|
|
1411
|
+
return fileCompare;
|
|
1412
|
+
}
|
|
1413
|
+
return left.id.localeCompare(right.id);
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
//# sourceMappingURL=transactionDomainAnalysis.js.map
|