@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.
Files changed (298) hide show
  1. package/CHANGELOG.md +538 -0
  2. package/README.md +1 -0
  3. package/dist/archpilot-cli.js +850 -159
  4. package/dist/cli/src/archpilot-cli.js +5523 -0
  5. package/dist/cli/src/archpilot-cli.js.map +1 -0
  6. package/dist/core/src/adr/adrLoader.js +97 -0
  7. package/dist/core/src/adr/adrLoader.js.map +1 -0
  8. package/dist/core/src/adr/adrParser.js +77 -0
  9. package/dist/core/src/adr/adrParser.js.map +1 -0
  10. package/dist/core/src/adr/adrTemplate.js +45 -0
  11. package/dist/core/src/adr/adrTemplate.js.map +1 -0
  12. package/dist/core/src/adr/adrTypes.js +3 -0
  13. package/dist/core/src/adr/adrTypes.js.map +1 -0
  14. package/dist/core/src/adr/adrValidation.js +160 -0
  15. package/dist/core/src/adr/adrValidation.js.map +1 -0
  16. package/dist/core/src/adr/index.js +9 -0
  17. package/dist/core/src/adr/index.js.map +1 -0
  18. package/dist/core/src/adrManagement.js +174 -0
  19. package/dist/core/src/adrManagement.js.map +1 -0
  20. package/dist/core/src/adrRequirementStatus.js +62 -0
  21. package/dist/core/src/adrRequirementStatus.js.map +1 -0
  22. package/dist/core/src/adrScaffoldGeneration.js +325 -0
  23. package/dist/core/src/adrScaffoldGeneration.js.map +1 -0
  24. package/dist/core/src/apiDesignAnalysis.js +869 -0
  25. package/dist/core/src/apiDesignAnalysis.js.map +1 -0
  26. package/dist/core/src/applicationLayeringAnalysis.js +920 -0
  27. package/dist/core/src/applicationLayeringAnalysis.js.map +1 -0
  28. package/dist/core/src/applyRulePack.js +117 -0
  29. package/dist/core/src/applyRulePack.js.map +1 -0
  30. package/dist/core/src/architectureBaseline.js +271 -0
  31. package/dist/core/src/architectureBaseline.js.map +1 -0
  32. package/dist/core/src/architectureComponentDiscoveryDefaults.js +139 -0
  33. package/dist/core/src/architectureComponentDiscoveryDefaults.js.map +1 -0
  34. package/dist/core/src/architectureDependencyContext.js +316 -0
  35. package/dist/core/src/architectureDependencyContext.js.map +1 -0
  36. package/dist/core/src/architectureDriftDetection.js +96 -0
  37. package/dist/core/src/architectureDriftDetection.js.map +1 -0
  38. package/dist/core/src/architectureDriftTimeline.js +221 -0
  39. package/dist/core/src/architectureDriftTimeline.js.map +1 -0
  40. package/dist/core/src/architectureGovernanceBaseline.js +469 -0
  41. package/dist/core/src/architectureGovernanceBaseline.js.map +1 -0
  42. package/dist/core/src/architectureHealthHistory.js +208 -0
  43. package/dist/core/src/architectureHealthHistory.js.map +1 -0
  44. package/dist/core/src/architectureHealthScore.js +657 -0
  45. package/dist/core/src/architectureHealthScore.js.map +1 -0
  46. package/dist/core/src/architectureInit.js +826 -0
  47. package/dist/core/src/architectureInit.js.map +1 -0
  48. package/dist/core/src/architectureMap.js +727 -0
  49. package/dist/core/src/architectureMap.js.map +1 -0
  50. package/dist/core/src/architectureMigrationPreview.js +467 -0
  51. package/dist/core/src/architectureMigrationPreview.js.map +1 -0
  52. package/dist/core/src/architectureModelRefresh.js +209 -0
  53. package/dist/core/src/architectureModelRefresh.js.map +1 -0
  54. package/dist/core/src/architectureReportWorkflow.js +139 -0
  55. package/dist/core/src/architectureReportWorkflow.js.map +1 -0
  56. package/dist/core/src/architectureValidation.js +3879 -0
  57. package/dist/core/src/architectureValidation.js.map +1 -0
  58. package/dist/core/src/architectureWorkspaceModel.js +166 -0
  59. package/dist/core/src/architectureWorkspaceModel.js.map +1 -0
  60. package/dist/core/src/authorizationTenantAnalysis.js +652 -0
  61. package/dist/core/src/authorizationTenantAnalysis.js.map +1 -0
  62. package/dist/core/src/bootstrapArchitectureConfig.js +150 -0
  63. package/dist/core/src/bootstrapArchitectureConfig.js.map +1 -0
  64. package/dist/core/src/changeRisk/changeRiskAssessment.js +251 -0
  65. package/dist/core/src/changeRisk/changeRiskAssessment.js.map +1 -0
  66. package/dist/core/src/changeRisk/changeRiskTypes.js +3 -0
  67. package/dist/core/src/changeRisk/changeRiskTypes.js.map +1 -0
  68. package/dist/core/src/changeRisk/index.js +6 -0
  69. package/dist/core/src/changeRisk/index.js.map +1 -0
  70. package/dist/core/src/ci/githubInlineAnnotations.js +245 -0
  71. package/dist/core/src/ci/githubInlineAnnotations.js.map +1 -0
  72. package/dist/core/src/ci/githubPrCommentAdapter.js +232 -0
  73. package/dist/core/src/ci/githubPrCommentAdapter.js.map +1 -0
  74. package/dist/core/src/ciWorkflowTemplates.js +1042 -0
  75. package/dist/core/src/ciWorkflowTemplates.js.map +1 -0
  76. package/dist/core/src/compliance/complianceExportBuilder.js +122 -0
  77. package/dist/core/src/compliance/complianceExportBuilder.js.map +1 -0
  78. package/dist/core/src/compliance/complianceExportTypes.js +3 -0
  79. package/dist/core/src/compliance/complianceExportTypes.js.map +1 -0
  80. package/dist/core/src/compliance/complianceExportWriter.js +48 -0
  81. package/dist/core/src/compliance/complianceExportWriter.js.map +1 -0
  82. package/dist/core/src/compliance/complianceRulePackResolver.js +62 -0
  83. package/dist/core/src/compliance/complianceRulePackResolver.js.map +1 -0
  84. package/dist/core/src/config/index.js +17 -0
  85. package/dist/core/src/config/index.js.map +1 -0
  86. package/dist/core/src/config/projectConfig.js +224 -0
  87. package/dist/core/src/config/projectConfig.js.map +1 -0
  88. package/dist/core/src/config/projectIdentity.js +160 -0
  89. package/dist/core/src/config/projectIdentity.js.map +1 -0
  90. package/dist/core/src/dataQueryRiskDetection.js +1203 -0
  91. package/dist/core/src/dataQueryRiskDetection.js.map +1 -0
  92. package/dist/core/src/dependencyValidation.js +2557 -0
  93. package/dist/core/src/dependencyValidation.js.map +1 -0
  94. package/dist/core/src/discoverAdrs.js +15 -0
  95. package/dist/core/src/discoverAdrs.js.map +1 -0
  96. package/dist/core/src/drift/driftComparator.js +49 -0
  97. package/dist/core/src/drift/driftComparator.js.map +1 -0
  98. package/dist/core/src/drift/driftSnapshotLoader.js +136 -0
  99. package/dist/core/src/drift/driftSnapshotLoader.js.map +1 -0
  100. package/dist/core/src/drift/driftSnapshotTypes.js +3 -0
  101. package/dist/core/src/drift/driftSnapshotTypes.js.map +1 -0
  102. package/dist/core/src/drift/driftSnapshotWriter.js +48 -0
  103. package/dist/core/src/drift/driftSnapshotWriter.js.map +1 -0
  104. package/dist/core/src/drift/index.js +6 -0
  105. package/dist/core/src/drift/index.js.map +1 -0
  106. package/dist/core/src/eventArchitectureAnalysis.js +925 -0
  107. package/dist/core/src/eventArchitectureAnalysis.js.map +1 -0
  108. package/dist/core/src/findingTargetResolution.js +117 -0
  109. package/dist/core/src/findingTargetResolution.js.map +1 -0
  110. package/dist/core/src/generateArchitectureDiagrams.js +57 -0
  111. package/dist/core/src/generateArchitectureDiagrams.js.map +1 -0
  112. package/dist/core/src/generateImpactAnalysis.js +366 -0
  113. package/dist/core/src/generateImpactAnalysis.js.map +1 -0
  114. package/dist/core/src/generateOnboardingDocs.js +262 -0
  115. package/dist/core/src/generateOnboardingDocs.js.map +1 -0
  116. package/dist/core/src/governance/cloudConnection.js +203 -0
  117. package/dist/core/src/governance/cloudConnection.js.map +1 -0
  118. package/dist/core/src/governance/governanceContracts.js +129 -0
  119. package/dist/core/src/governance/governanceContracts.js.map +1 -0
  120. package/dist/core/src/governance/governanceGitMetadata.js +78 -0
  121. package/dist/core/src/governance/governanceGitMetadata.js.map +1 -0
  122. package/dist/core/src/governance/governanceUploadClient.js +119 -0
  123. package/dist/core/src/governance/governanceUploadClient.js.map +1 -0
  124. package/dist/core/src/governance/governanceUploadLimitErrorFormatting.js +155 -0
  125. package/dist/core/src/governance/governanceUploadLimitErrorFormatting.js.map +1 -0
  126. package/dist/core/src/governance/governanceUploadPayload.js +439 -0
  127. package/dist/core/src/governance/governanceUploadPayload.js.map +1 -0
  128. package/dist/core/src/governance/runGovernanceUpload.js +103 -0
  129. package/dist/core/src/governance/runGovernanceUpload.js.map +1 -0
  130. package/dist/core/src/impactAnalysisEngine.js +376 -0
  131. package/dist/core/src/impactAnalysisEngine.js.map +1 -0
  132. package/dist/core/src/initializationState.js +99 -0
  133. package/dist/core/src/initializationState.js.map +1 -0
  134. package/dist/core/src/managedArtifacts.js +151 -0
  135. package/dist/core/src/managedArtifacts.js.map +1 -0
  136. package/dist/core/src/moduleContractValidation.js +266 -0
  137. package/dist/core/src/moduleContractValidation.js.map +1 -0
  138. package/dist/core/src/moduleContractsGenerator.js +337 -0
  139. package/dist/core/src/moduleContractsGenerator.js.map +1 -0
  140. package/dist/core/src/moduleDiscovery.js +416 -0
  141. package/dist/core/src/moduleDiscovery.js.map +1 -0
  142. package/dist/core/src/moduleScope.js +426 -0
  143. package/dist/core/src/moduleScope.js.map +1 -0
  144. package/dist/core/src/onboardingGuidance.js +74 -0
  145. package/dist/core/src/onboardingGuidance.js.map +1 -0
  146. package/dist/core/src/operationalLogging.js +7 -0
  147. package/dist/core/src/operationalLogging.js.map +1 -0
  148. package/dist/core/src/packageVersion.js +52 -0
  149. package/dist/core/src/packageVersion.js.map +1 -0
  150. package/dist/core/src/policy/effectivePolicyResolver.js +91 -0
  151. package/dist/core/src/policy/effectivePolicyResolver.js.map +1 -0
  152. package/dist/core/src/policy/exceptionLoader.js +285 -0
  153. package/dist/core/src/policy/exceptionLoader.js.map +1 -0
  154. package/dist/core/src/policy/remotePolicyLoader.js +227 -0
  155. package/dist/core/src/policy/remotePolicyLoader.js.map +1 -0
  156. package/dist/core/src/policy/remotePolicyTypes.js +3 -0
  157. package/dist/core/src/policy/remotePolicyTypes.js.map +1 -0
  158. package/dist/core/src/policy/riskPolicyConfig.js +206 -0
  159. package/dist/core/src/policy/riskPolicyConfig.js.map +1 -0
  160. package/dist/core/src/policy/riskPolicyEnforcement.js +203 -0
  161. package/dist/core/src/policy/riskPolicyEnforcement.js.map +1 -0
  162. package/dist/core/src/policy/riskPolicyExitCode.js +16 -0
  163. package/dist/core/src/policy/riskPolicyExitCode.js.map +1 -0
  164. package/dist/core/src/policy/riskPolicyInitializer.js +90 -0
  165. package/dist/core/src/policy/riskPolicyInitializer.js.map +1 -0
  166. package/dist/core/src/quickFix/quickFixEngine.js +132 -0
  167. package/dist/core/src/quickFix/quickFixEngine.js.map +1 -0
  168. package/dist/core/src/quickFix/quickFixMessaging.js +15 -0
  169. package/dist/core/src/quickFix/quickFixMessaging.js.map +1 -0
  170. package/dist/core/src/quickFix/quickFixProviders.js +87 -0
  171. package/dist/core/src/quickFix/quickFixProviders.js.map +1 -0
  172. package/dist/core/src/quickFix/quickFixTypes.js +3 -0
  173. package/dist/core/src/quickFix/quickFixTypes.js.map +1 -0
  174. package/dist/core/src/remediation/fileHelpers.js +86 -0
  175. package/dist/core/src/remediation/fileHelpers.js.map +1 -0
  176. package/dist/core/src/remediation/fixRegistry.js +645 -0
  177. package/dist/core/src/remediation/fixRegistry.js.map +1 -0
  178. package/dist/core/src/remediation/index.js +8 -0
  179. package/dist/core/src/remediation/index.js.map +1 -0
  180. package/dist/core/src/remediation/remediationEngine.js +207 -0
  181. package/dist/core/src/remediation/remediationEngine.js.map +1 -0
  182. package/dist/core/src/reporting/adrReporter.js +59 -0
  183. package/dist/core/src/reporting/adrReporter.js.map +1 -0
  184. package/dist/core/src/reporting/architectureBaselineReporter.js +64 -0
  185. package/dist/core/src/reporting/architectureBaselineReporter.js.map +1 -0
  186. package/dist/core/src/reporting/architectureDiagramReporter.js +148 -0
  187. package/dist/core/src/reporting/architectureDiagramReporter.js.map +1 -0
  188. package/dist/core/src/reporting/architectureDriftTimelineReporter.js +44 -0
  189. package/dist/core/src/reporting/architectureDriftTimelineReporter.js.map +1 -0
  190. package/dist/core/src/reporting/architectureHealthHistoryReporter.js +51 -0
  191. package/dist/core/src/reporting/architectureHealthHistoryReporter.js.map +1 -0
  192. package/dist/core/src/reporting/architectureMapReporter.js +228 -0
  193. package/dist/core/src/reporting/architectureMapReporter.js.map +1 -0
  194. package/dist/core/src/reporting/architecturePrCommentReporter.js +324 -0
  195. package/dist/core/src/reporting/architecturePrCommentReporter.js.map +1 -0
  196. package/dist/core/src/reporting/architecturePrReviewReporter.js +389 -0
  197. package/dist/core/src/reporting/architecturePrReviewReporter.js.map +1 -0
  198. package/dist/core/src/reporting/architectureReportArtifacts.js +166 -0
  199. package/dist/core/src/reporting/architectureReportArtifacts.js.map +1 -0
  200. package/dist/core/src/reporting/architectureReviewReportGenerator.js +382 -0
  201. package/dist/core/src/reporting/architectureReviewReportGenerator.js.map +1 -0
  202. package/dist/core/src/reporting/architectureReviewReporter.js +1633 -0
  203. package/dist/core/src/reporting/architectureReviewReporter.js.map +1 -0
  204. package/dist/core/src/reporting/architectureRiskAssessment.js +101 -0
  205. package/dist/core/src/reporting/architectureRiskAssessment.js.map +1 -0
  206. package/dist/core/src/reporting/architectureScoreBadge.js +177 -0
  207. package/dist/core/src/reporting/architectureScoreBadge.js.map +1 -0
  208. package/dist/core/src/reporting/architectureSupplementalReporters.js +124 -0
  209. package/dist/core/src/reporting/architectureSupplementalReporters.js.map +1 -0
  210. package/dist/core/src/reporting/changeRiskAssessment.js +6 -0
  211. package/dist/core/src/reporting/changeRiskAssessment.js.map +1 -0
  212. package/dist/core/src/reporting/changeRiskTypes.js +3 -0
  213. package/dist/core/src/reporting/changeRiskTypes.js.map +1 -0
  214. package/dist/core/src/reporting/impactAnalysisReporter.js +111 -0
  215. package/dist/core/src/reporting/impactAnalysisReporter.js.map +1 -0
  216. package/dist/core/src/reporting/onboardingDocsReporter.js +113 -0
  217. package/dist/core/src/reporting/onboardingDocsReporter.js.map +1 -0
  218. package/dist/core/src/reporting/rulePackReporter.js +61 -0
  219. package/dist/core/src/reporting/rulePackReporter.js.map +1 -0
  220. package/dist/core/src/reporting/safetyControlsDetection.js +343 -0
  221. package/dist/core/src/reporting/safetyControlsDetection.js.map +1 -0
  222. package/dist/core/src/resolveRuleAdrs.js +31 -0
  223. package/dist/core/src/resolveRuleAdrs.js.map +1 -0
  224. package/dist/core/src/reviewGenerationWorkflow.js +220 -0
  225. package/dist/core/src/reviewGenerationWorkflow.js.map +1 -0
  226. package/dist/core/src/rulePacks/index.js +17 -0
  227. package/dist/core/src/rulePacks/index.js.map +1 -0
  228. package/dist/core/src/rulePacks/layeredMonolithRulePack.js +45 -0
  229. package/dist/core/src/rulePacks/layeredMonolithRulePack.js.map +1 -0
  230. package/dist/core/src/rulePacks/modularMonolithRulePack.js +41 -0
  231. package/dist/core/src/rulePacks/modularMonolithRulePack.js.map +1 -0
  232. package/dist/core/src/rulePacks/types.js +3 -0
  233. package/dist/core/src/rulePacks/types.js.map +1 -0
  234. package/dist/core/src/setupNextSteps.js +939 -0
  235. package/dist/core/src/setupNextSteps.js.map +1 -0
  236. package/dist/core/src/setupRefresh.js +152 -0
  237. package/dist/core/src/setupRefresh.js.map +1 -0
  238. package/dist/core/src/smartInit/apiDbHintDetection.js +836 -0
  239. package/dist/core/src/smartInit/apiDbHintDetection.js.map +1 -0
  240. package/dist/core/src/smartInit/architectureProposalBuilder.js +1215 -0
  241. package/dist/core/src/smartInit/architectureProposalBuilder.js.map +1 -0
  242. package/dist/core/src/smartInit/architectureStyleDetection.js +472 -0
  243. package/dist/core/src/smartInit/architectureStyleDetection.js.map +1 -0
  244. package/dist/core/src/smartInit/archpilotIgnore.js +139 -0
  245. package/dist/core/src/smartInit/archpilotIgnore.js.map +1 -0
  246. package/dist/core/src/smartInit/componentDetection.js +1084 -0
  247. package/dist/core/src/smartInit/componentDetection.js.map +1 -0
  248. package/dist/core/src/smartInit/implementationProfileResolution.js +53 -0
  249. package/dist/core/src/smartInit/implementationProfileResolution.js.map +1 -0
  250. package/dist/core/src/smartInit/index.js +31 -0
  251. package/dist/core/src/smartInit/index.js.map +1 -0
  252. package/dist/core/src/smartInit/moduleDetection.js +982 -0
  253. package/dist/core/src/smartInit/moduleDetection.js.map +1 -0
  254. package/dist/core/src/smartInit/projectKindDetection.js +437 -0
  255. package/dist/core/src/smartInit/projectKindDetection.js.map +1 -0
  256. package/dist/core/src/smartInit/repoTopologyDetection.js +470 -0
  257. package/dist/core/src/smartInit/repoTopologyDetection.js.map +1 -0
  258. package/dist/core/src/smartInit/runContext.js +112 -0
  259. package/dist/core/src/smartInit/runContext.js.map +1 -0
  260. package/dist/core/src/smartInit/smartInitEngine.js +94 -0
  261. package/dist/core/src/smartInit/smartInitEngine.js.map +1 -0
  262. package/dist/core/src/smartInit/stackDetection.js +503 -0
  263. package/dist/core/src/smartInit/stackDetection.js.map +1 -0
  264. package/dist/core/src/smartInit/toolingHintDetection.js +345 -0
  265. package/dist/core/src/smartInit/toolingHintDetection.js.map +1 -0
  266. package/dist/core/src/sourceScopeClassification.js +83 -0
  267. package/dist/core/src/sourceScopeClassification.js.map +1 -0
  268. package/dist/core/src/stackAdapters.js +2475 -0
  269. package/dist/core/src/stackAdapters.js.map +1 -0
  270. package/dist/core/src/stackMetadata.js +157 -0
  271. package/dist/core/src/stackMetadata.js.map +1 -0
  272. package/dist/core/src/suppressions/index.js +3 -0
  273. package/dist/core/src/suppressions/index.js.map +1 -0
  274. package/dist/core/src/suppressions/suppressionReporter.js +38 -0
  275. package/dist/core/src/suppressions/suppressionReporter.js.map +1 -0
  276. package/dist/core/src/suppressions/suppressionTypes.js +3 -0
  277. package/dist/core/src/suppressions/suppressionTypes.js.map +1 -0
  278. package/dist/core/src/suppressions/suppressionValidator.js +119 -0
  279. package/dist/core/src/suppressions/suppressionValidator.js.map +1 -0
  280. package/dist/core/src/transactionDomainAnalysis.js +1416 -0
  281. package/dist/core/src/transactionDomainAnalysis.js.map +1 -0
  282. package/dist/core/src/validation/changedFilesResolver.js +165 -0
  283. package/dist/core/src/validation/changedFilesResolver.js.map +1 -0
  284. package/dist/core/src/validationProfiling.js +33 -0
  285. package/dist/core/src/validationProfiling.js.map +1 -0
  286. package/dist/core/src/validationRuleExplanations.js +69 -0
  287. package/dist/core/src/validationRuleExplanations.js.map +1 -0
  288. package/dist/core/src/validationSourceInventory.js +200 -0
  289. package/dist/core/src/validationSourceInventory.js.map +1 -0
  290. package/dist/core/src/validationSuppressions.js +231 -0
  291. package/dist/core/src/validationSuppressions.js.map +1 -0
  292. package/dist/core/src/validationTypeScriptAstCache.js +82 -0
  293. package/dist/core/src/validationTypeScriptAstCache.js.map +1 -0
  294. package/dist/core/src/validationWorkflow.js +222 -0
  295. package/dist/core/src/validationWorkflow.js.map +1 -0
  296. package/dist/core/src/workspaceRoot.js +68 -0
  297. package/dist/core/src/workspaceRoot.js.map +1 -0
  298. package/package.json +3 -2
@@ -0,0 +1,3879 @@
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.ValidationRuleCatalog = exports.ValidationRuleIds = void 0;
37
+ exports.getValidationRuleMetadataById = getValidationRuleMetadataById;
38
+ exports.getAllValidationRuleMetadata = getAllValidationRuleMetadata;
39
+ exports.formatValidationRuleLabel = formatValidationRuleLabel;
40
+ exports.isEnforcementRuleCategory = isEnforcementRuleCategory;
41
+ exports.getValidationSeverityPolicyNote = getValidationSeverityPolicyNote;
42
+ exports.getSeveritySortOrder = getSeveritySortOrder;
43
+ exports.getValidationOverallStatus = getValidationOverallStatus;
44
+ exports.classifyValidationResult = classifyValidationResult;
45
+ exports.buildValidationStatusSummary = buildValidationStatusSummary;
46
+ exports.readValidationConfig = readValidationConfig;
47
+ exports.canOverrideSeverity = canOverrideSeverity;
48
+ exports.resolveEffectiveSeverity = resolveEffectiveSeverity;
49
+ exports.getEffectiveValidationConfigSummary = getEffectiveValidationConfigSummary;
50
+ exports.buildValidationStatusExport = buildValidationStatusExport;
51
+ exports.readArchitectureConfig = readArchitectureConfig;
52
+ exports.runArchitectureValidationForWorkspace = runArchitectureValidationForWorkspace;
53
+ const path = __importStar(require("node:path"));
54
+ const node_fs_1 = require("node:fs");
55
+ const architectureInit_1 = require("./architectureInit");
56
+ const architectureDriftDetection_1 = require("./architectureDriftDetection");
57
+ const dependencyValidation_1 = require("./dependencyValidation");
58
+ const moduleContractValidation_1 = require("./moduleContractValidation");
59
+ const validationSuppressions_1 = require("./validationSuppressions");
60
+ const findingTargetResolution_1 = require("./findingTargetResolution");
61
+ const discoverAdrs_1 = require("./discoverAdrs");
62
+ const adrValidation_1 = require("./adr/adrValidation");
63
+ const projectConfig_1 = require("./config/projectConfig");
64
+ const projectIdentity_1 = require("./config/projectIdentity");
65
+ const remotePolicyLoader_1 = require("./policy/remotePolicyLoader");
66
+ const effectivePolicyResolver_1 = require("./policy/effectivePolicyResolver");
67
+ const operationalLogging_1 = require("./operationalLogging");
68
+ const architectureHealthScore_1 = require("./architectureHealthScore");
69
+ const architectureHealthHistory_1 = require("./architectureHealthHistory");
70
+ const architectureHealthHistoryReporter_1 = require("./reporting/architectureHealthHistoryReporter");
71
+ const architectureScoreBadge_1 = require("./reporting/architectureScoreBadge");
72
+ const architectureDriftTimeline_1 = require("./architectureDriftTimeline");
73
+ const driftComparator_1 = require("./drift/driftComparator");
74
+ const driftSnapshotLoader_1 = require("./drift/driftSnapshotLoader");
75
+ const driftSnapshotWriter_1 = require("./drift/driftSnapshotWriter");
76
+ const suppressionValidator_1 = require("./suppressions/suppressionValidator");
77
+ const quickFixProviders_1 = require("./quickFix/quickFixProviders");
78
+ const setupNextSteps_1 = require("./setupNextSteps");
79
+ const changeRiskAssessment_1 = require("./reporting/changeRiskAssessment");
80
+ const exceptionLoader_1 = require("./policy/exceptionLoader");
81
+ const dataQueryRiskDetection_1 = require("./dataQueryRiskDetection");
82
+ const applicationLayeringAnalysis_1 = require("./applicationLayeringAnalysis");
83
+ const authorizationTenantAnalysis_1 = require("./authorizationTenantAnalysis");
84
+ const transactionDomainAnalysis_1 = require("./transactionDomainAnalysis");
85
+ const apiDesignAnalysis_1 = require("./apiDesignAnalysis");
86
+ const eventArchitectureAnalysis_1 = require("./eventArchitectureAnalysis");
87
+ const moduleDiscovery_1 = require("./moduleDiscovery");
88
+ const managedArtifacts_1 = require("./managedArtifacts");
89
+ const validationProfiling_1 = require("./validationProfiling");
90
+ const validationSourceInventory_1 = require("./validationSourceInventory");
91
+ const validationTypeScriptAstCache_1 = require("./validationTypeScriptAstCache");
92
+ exports.ValidationRuleIds = {
93
+ ARCH_MODULAR_STRUCTURE: 'AP-ARCH-001',
94
+ ARCH_PROFILE_NOT_FULLY_SUPPORTED: 'AP-ARCH-002',
95
+ ARCH_OVERVIEW_ARTIFACT_EXISTS: 'AP-ARCH-003',
96
+ ARCH_MODULE_BASELINE_COMPLETE: 'AP-ARCH-004',
97
+ ARCH_MODULE_REGISTRY_PATH_EXISTS: 'AP-ARCH-005',
98
+ ARCH_MODULE_CONTRACT_FILE_EXISTS: 'AP-ARCH-006',
99
+ ARCH_MODULE_CONTRACT_IDENTITY_MATCHES_REGISTRY: 'AP-ARCH-007',
100
+ ARCH_MODULE_PUBLIC_ENTRYPOINTS_ALIGNED: 'AP-ARCH-008',
101
+ ARCH_RULE_CONFIGURATION_VALID: 'AP-ARCH-009',
102
+ ARCH_PROJECT_CONFIGURATION_VALID: 'AP-ARCH-010',
103
+ ARCH_PROJECT_IDENTITY_CONFIGURATION_VALID: 'AP-ARCH-011',
104
+ API_OPENAPI_EXISTS: 'AP-API-001',
105
+ API_GRAPHQL_PATH_PRESENT: 'AP-API-002',
106
+ API_MISSING_COLLECTION_PAGINATION: 'AP-API-010',
107
+ API_BULK_OPERATION_RISK: 'AP-API-011',
108
+ API_DTO_ENTITY_LEAKAGE: 'AP-API-012',
109
+ API_INCONSISTENT_RESOURCE_DESIGN: 'AP-API-013',
110
+ API_VERSIONING_STRATEGY_INCONSISTENCY: 'AP-API-014',
111
+ API_CONTRACT_IMPLEMENTATION_DRIFT: 'AP-API-015',
112
+ DEP_CIRCULAR_MODULE_DEPENDENCY: 'AP-DEP-001',
113
+ DEP_FORBIDDEN_CROSS_MODULE_DEPENDENCY: 'AP-DEP-002',
114
+ DEP_NON_PUBLIC_CROSS_MODULE_IMPORT: 'AP-DEP-003',
115
+ DEP_CROSS_MODULE_DEPENDENCY_DECLARED_IN_CONTRACT: 'AP-DEP-004',
116
+ DEP_CROSS_MODULE_IMPORT_USES_PUBLIC_SURFACE_ONLY: 'AP-DEP-005',
117
+ DEP_DECLARED_MODULE_DEPENDENCY_NOT_USED: 'AP-DEP-006',
118
+ DEP_ISOLATED_MODULE_DETECTED: 'AP-DEP-007',
119
+ DEP_TRANSITIVE_CIRCULAR_MODULE_DEPENDENCY: 'AP-DEP-008',
120
+ DEP_MODULE_HIGH_INBOUND_DEPENDENCY_COUNT: 'AP-DEP-009',
121
+ DEP_LAYER_DIRECTION_VIOLATION: 'AP-DEP-010',
122
+ DEP_CROSS_COMPONENT_POLICY_VIOLATION: 'AP-DEP-011',
123
+ ADR_DIRECTORY_EXISTS: 'AP-ADR-001',
124
+ ADR_FILE_HAS_TITLE: 'AP-ADR-002',
125
+ ADR_STATUS_VALID: 'AP-ADR-003',
126
+ ADR_REQUIRED_COVERAGE_PRESENT: 'AP-ADR-004',
127
+ SUPPRESSION_REASON_PRESENT: 'AP-SUP-001',
128
+ SUPPRESSION_NOT_EXPIRED: 'AP-SUP-002',
129
+ SUPPRESSION_MATCHES_ACTIVE_FINDING: 'AP-SUP-003',
130
+ SUPPRESSION_NOT_DUPLICATED: 'AP-SUP-004',
131
+ SUPPRESSION_EXPIRY_FORMAT_VALID: 'AP-SUP-005',
132
+ DB_TENANT_SAME_SCHEMA_TENANT_ID: 'AP-DB-001',
133
+ DB_SQL_BASELINE_EXISTS: 'AP-DB-002',
134
+ DQR_MISSING_TENANT_FILTER_EVIDENCE: 'AP-DQR-001',
135
+ DQR_BROAD_SELECT_STAR_USAGE: 'AP-DQR-002',
136
+ DQR_UNBOUNDED_QUERY_WITHOUT_PAGINATION: 'AP-DQR-003',
137
+ DQR_CROSS_MODULE_DIRECT_DB_ACCESS: 'AP-DQR-004',
138
+ DQR_N_PLUS_ONE_QUERY_EVIDENCE: 'AP-DQR-005',
139
+ DQR_LARGE_COLLECTION_LOADING: 'AP-DQR-006',
140
+ DQR_MISSING_PROJECTION_SELECT: 'AP-DQR-007',
141
+ DQR_REPEATED_QUERY_PATTERN: 'AP-DQR-008',
142
+ EVT_PUBLISHED_EVENT_HAS_NO_HANDLER: 'AP-EVT-001',
143
+ EVT_CIRCULAR_EVENT_CHAIN_RISK: 'AP-EVT-002',
144
+ EVT_SYNCHRONOUS_DEPENDENCY_DISGUISED_AS_EVENT: 'AP-EVT-003',
145
+ EVT_EVENT_NAMING_INCONSISTENCY: 'AP-EVT-004',
146
+ APP_CONTROLLER_BUSINESS_LOGIC: 'AP-APP-001',
147
+ APP_REPOSITORY_BUSINESS_LOGIC: 'AP-APP-002',
148
+ APP_SERVICE_BYPASSES_REPOSITORY: 'AP-APP-003',
149
+ APP_PACKAGE_RESPONSIBILITY_MISMATCH: 'AP-APP-004',
150
+ APP_SERVICE_GOD_CLASS: 'AP-APP-005',
151
+ AUTH_MISSING_AUTHORIZATION_ENFORCEMENT: 'AP-AUTH-001',
152
+ AUTH_INCONSISTENT_AUTHORIZATION: 'AP-AUTH-002',
153
+ AUTH_AUTHORIZATION_IN_WRONG_LAYER: 'AP-AUTH-003',
154
+ TEN_MISSING_TENANT_CONTEXT_PROPAGATION: 'AP-TEN-001',
155
+ TEN_MISSING_TENANT_ENFORCEMENT: 'AP-TEN-002',
156
+ TEN_CROSS_TENANT_ACCESS_RISK: 'AP-TEN-003',
157
+ TEN_TENANT_MODEL_UNDOCUMENTED: 'AP-TEN-004',
158
+ TXN_TRANSACTION_BOUNDARY_TOO_BROAD: 'AP-TXN-001',
159
+ TXN_TRANSACTION_STARTED_IN_CONTROLLER: 'AP-TXN-002',
160
+ TXN_NESTED_TRANSACTION_RISK: 'AP-TXN-003',
161
+ TXN_LONG_RUNNING_WORKFLOW_INSIDE_TRANSACTION: 'AP-TXN-004',
162
+ DOM_ANEMIC_DOMAIN_MODEL: 'AP-DOM-001',
163
+ DOM_AGGREGATE_BOUNDARY_VIOLATION: 'AP-DOM-002',
164
+ DOM_OVERSIZED_AGGREGATE_OR_SERVICE_BOUNDARY: 'AP-DOM-003',
165
+ DOM_DOMAIN_LOGIC_SCATTERED_ACROSS_LAYERS: 'AP-DOM-004',
166
+ DOM_CROSS_AGGREGATE_MODIFICATION_RISK: 'AP-DOM-005',
167
+ DOC_TENANT_DIFFERENT_SCHEMA_ADR: 'AP-DOC-001',
168
+ DOC_RBAC_MENTIONED_IN_OVERVIEW: 'AP-DOC-002',
169
+ SAFE_MISSING_ADR_COVERAGE: 'AP-SAFE-001',
170
+ SAFE_MISSING_ROLLBACK_PLAN: 'AP-SAFE-002',
171
+ SAFE_MISSING_FEATURE_FLAG_EVIDENCE: 'AP-SAFE-003',
172
+ };
173
+ exports.ValidationRuleCatalog = {
174
+ ARCH_MODULAR_STRUCTURE: {
175
+ id: exports.ValidationRuleIds.ARCH_MODULAR_STRUCTURE,
176
+ title: 'Modular monolith folder structure exists',
177
+ category: 'architecture',
178
+ defaultSeverity: 'error',
179
+ architectureScope: 'module',
180
+ applicability: { requiresArchitectureModules: true },
181
+ description: 'Checks whether a modular monolith has a configured module root with clear top-level module or capability directories.',
182
+ recommendedFix: 'Set a valid module root in architecture.json and organize code into clear top-level module or capability folders under that root.',
183
+ explanation: 'This rule checks that architecture code is organized into explicit module or domain boundaries.',
184
+ whyItMatters: 'Without explicit boundaries, architectural intent becomes implicit and drift is harder to detect early.',
185
+ howToFix: 'Use a stable module root such as src/modules or src, then place clear business or capability folders directly under that configured root.',
186
+ exampleFix: `src/modules/
187
+ auth/
188
+ billing/
189
+ inventory/
190
+
191
+ or
192
+
193
+ src/
194
+ adr/
195
+ reporting/
196
+ remediation/`,
197
+ documentationHint: 'docs/architecture/overview.md',
198
+ relatedAdrs: ['ADR-001'],
199
+ },
200
+ ARCH_PROFILE_NOT_FULLY_SUPPORTED: {
201
+ id: exports.ValidationRuleIds.ARCH_PROFILE_NOT_FULLY_SUPPORTED,
202
+ title: 'Implementation profile not fully supported',
203
+ category: 'architecture',
204
+ defaultSeverity: 'info',
205
+ description: 'Indicates that the selected implementation profile does not yet have dedicated rules and templates.',
206
+ recommendedFix: 'Consider using the typescript-backend profile until dedicated support for this profile is available.',
207
+ },
208
+ ARCH_OVERVIEW_ARTIFACT_EXISTS: {
209
+ id: exports.ValidationRuleIds.ARCH_OVERVIEW_ARTIFACT_EXISTS,
210
+ title: 'Architecture overview artifact exists',
211
+ category: 'architecture',
212
+ defaultSeverity: 'error',
213
+ description: 'Checks whether docs/architecture/overview.md exists as a baseline architecture artifact generated during initialization.',
214
+ recommendedFix: 'Restore or recreate docs/architecture/overview.md so the initialized architecture has an explicit overview artifact.',
215
+ },
216
+ ARCH_MODULE_BASELINE_COMPLETE: {
217
+ id: exports.ValidationRuleIds.ARCH_MODULE_BASELINE_COMPLETE,
218
+ title: 'Module baseline files exist',
219
+ category: 'architecture',
220
+ defaultSeverity: 'warning',
221
+ architectureScope: 'module',
222
+ applicability: { requiresArchitectureModules: true },
223
+ description: 'Modules that expose public entrypoints or are depended on by other modules must provide an index.ts public entrypoint. Internal-only modules do not require index.ts.',
224
+ recommendedFix: 'Ensure each module has README.md. Add index.ts for modules with declared publicEntrypoints or inbound declared dependencies from other modules.',
225
+ },
226
+ ARCH_MODULE_REGISTRY_PATH_EXISTS: {
227
+ id: exports.ValidationRuleIds.ARCH_MODULE_REGISTRY_PATH_EXISTS,
228
+ title: 'Module registry path exists',
229
+ category: 'architecture',
230
+ defaultSeverity: 'error',
231
+ architectureScope: 'module',
232
+ applicability: { requiresArchitectureModules: true },
233
+ description: 'Checks whether each module path declared in architecture.json exists as a directory in the repository.',
234
+ recommendedFix: 'Update the module registry path in .archpilot/architecture.json or restore the missing module directory.',
235
+ whyItMatters: 'This is a setup/configuration mismatch: architecture.json points at a module path that is not present in the workspace, so validation cannot reliably connect the intended architecture to the current code.',
236
+ howToFix: 'If the module should still exist, restore the directory or run setup refresh/bootstrap workflows that generate missing ArchPilot-managed setup artifacts. If the module was renamed or removed, update .archpilot/architecture.json so the registry reflects the current source tree.',
237
+ exampleFix: `{
238
+ "modules": {
239
+ "billing": {
240
+ "path": "src/modules/billing"
241
+ }
242
+ }
243
+ }`,
244
+ documentationHint: '.archpilot/architecture.json modules entries and generated setup artifacts',
245
+ },
246
+ ARCH_MODULE_CONTRACT_FILE_EXISTS: {
247
+ id: exports.ValidationRuleIds.ARCH_MODULE_CONTRACT_FILE_EXISTS,
248
+ title: 'Module contract file exists',
249
+ category: 'architecture',
250
+ defaultSeverity: 'error',
251
+ architectureScope: 'module',
252
+ applicability: { requiresArchitectureModules: true },
253
+ description: 'Checks whether each module contract file referenced by the module registry exists on disk.',
254
+ recommendedFix: 'Generate or restore the missing module contract file, or update the module registry contract path in .archpilot/architecture.json.',
255
+ whyItMatters: 'This is a setup/configuration mismatch: architecture.json references a module contract file that is missing, so ArchPilot cannot validate declared dependencies, public entrypoints, or module ownership for that module.',
256
+ howToFix: 'If the module is still part of the architecture, run setup refresh or generate module contracts to create the missing contract without overwriting user-owned files. If the reference is stale, fix the contract path or remove the stale module entry from .archpilot/architecture.json.',
257
+ exampleFix: `{
258
+ "module": "billing",
259
+ "dependsOn": [],
260
+ "publicEntrypoints": ["src/modules/billing/index.ts"]
261
+ }`,
262
+ documentationHint: '.archpilot/architecture.json module contract paths and .archpilot/contracts/<module>.contract.json',
263
+ },
264
+ ARCH_MODULE_CONTRACT_IDENTITY_MATCHES_REGISTRY: {
265
+ id: exports.ValidationRuleIds.ARCH_MODULE_CONTRACT_IDENTITY_MATCHES_REGISTRY,
266
+ title: 'Module contract identity matches registry',
267
+ category: 'architecture',
268
+ defaultSeverity: 'error',
269
+ architectureScope: 'module',
270
+ applicability: { requiresArchitectureModules: true },
271
+ description: 'Checks whether each module contract JSON is valid, has a matching module identity, and uses valid array field shapes for core contract fields.',
272
+ recommendedFix: 'Fix module contract JSON syntax and ensure contract.module plus contract array fields are valid and aligned with the module registry.',
273
+ },
274
+ ARCH_MODULE_PUBLIC_ENTRYPOINTS_ALIGNED: {
275
+ id: exports.ValidationRuleIds.ARCH_MODULE_PUBLIC_ENTRYPOINTS_ALIGNED,
276
+ title: 'Module public entrypoints are aligned',
277
+ category: 'architecture',
278
+ defaultSeverity: 'error',
279
+ architectureScope: 'module',
280
+ applicability: { requiresArchitectureModules: true },
281
+ description: 'Checks whether module publicEntrypoints are consistent between architecture.json and each module contract file.',
282
+ recommendedFix: 'Align publicEntrypoints in .archpilot/architecture.json and the module contract file so both describe the same public surface.',
283
+ },
284
+ ARCH_RULE_CONFIGURATION_VALID: {
285
+ id: exports.ValidationRuleIds.ARCH_RULE_CONFIGURATION_VALID,
286
+ title: 'Project rule configuration file is valid',
287
+ category: 'architecture',
288
+ defaultSeverity: 'error',
289
+ description: 'Checks whether .archpilot/rules.json is valid JSON and uses a supported structure for runtime rule overrides.',
290
+ recommendedFix: 'Fix .archpilot/rules.json so it is valid JSON and follows the expected { "rules": { "<RULE_ID>": { "enabled": boolean, "severity": "error|warning|info" } } } format.',
291
+ },
292
+ ARCH_PROJECT_CONFIGURATION_VALID: {
293
+ id: exports.ValidationRuleIds.ARCH_PROJECT_CONFIGURATION_VALID,
294
+ title: 'Invalid ArchPilot project configuration JSON',
295
+ category: 'architecture',
296
+ defaultSeverity: 'error',
297
+ description: 'Checks whether .archpilot/config.json is valid JSON and uses supported project-level ArchPilot settings.',
298
+ recommendedFix: 'Fix .archpilot/config.json so it is valid JSON, policySource is a string when configured, Cloud display metadata values are strings when configured, and uploadOnValidate is a boolean when configured.',
299
+ },
300
+ ARCH_PROJECT_IDENTITY_CONFIGURATION_VALID: {
301
+ id: exports.ValidationRuleIds.ARCH_PROJECT_IDENTITY_CONFIGURATION_VALID,
302
+ title: 'Invalid ArchPilot project identity configuration',
303
+ category: 'architecture',
304
+ defaultSeverity: 'error',
305
+ description: 'Checks whether .archpilot/project.json is valid JSON and defines a non-empty repositoryId plus supported optional organization metadata fields.',
306
+ recommendedFix: 'Fix .archpilot/project.json so it is valid JSON, repositoryId is a non-empty string, and organizationId/team/system are strings when configured.',
307
+ },
308
+ API_OPENAPI_EXISTS: {
309
+ id: exports.ValidationRuleIds.API_OPENAPI_EXISTS,
310
+ title: 'OpenAPI contract exists',
311
+ category: 'api',
312
+ defaultSeverity: 'error',
313
+ description: 'Checks whether contracts/openapi.yaml exists for the configured API style.',
314
+ recommendedFix: 'Create a contracts/openapi.yaml file that describes your public API surface, or regenerate it using ArchPilot initialization.',
315
+ },
316
+ API_GRAPHQL_PATH_PRESENT: {
317
+ id: exports.ValidationRuleIds.API_GRAPHQL_PATH_PRESENT,
318
+ title: 'GraphQL endpoint documented in OpenAPI',
319
+ category: 'api',
320
+ defaultSeverity: 'error',
321
+ description: 'Checks whether the OpenAPI contract contains a /graphql path when the API style is GraphQL.',
322
+ recommendedFix: 'Add a /graphql path entry to contracts/openapi.yaml so the GraphQL endpoint is documented.',
323
+ },
324
+ API_MISSING_COLLECTION_PAGINATION: {
325
+ id: exports.ValidationRuleIds.API_MISSING_COLLECTION_PAGINATION,
326
+ title: 'Missing pagination on collection endpoint',
327
+ category: 'api',
328
+ defaultSeverity: 'warning',
329
+ description: 'Detects collection-style GET endpoints that appear to return lists without pagination evidence in source or OpenAPI.',
330
+ recommendedFix: 'Add page/limit/offset/cursor parameters, Pageable support, or another explicit bounded collection contract.',
331
+ whyItMatters: 'Unbounded collection endpoints can become reliability and data-exposure risks as datasets grow.',
332
+ howToFix: 'Expose pagination parameters at the API boundary and pass them to the service/repository query path.',
333
+ documentationHint: 'controller endpoints and OpenAPI parameters',
334
+ },
335
+ API_BULK_OPERATION_RISK: {
336
+ id: exports.ValidationRuleIds.API_BULK_OPERATION_RISK,
337
+ title: 'Bulk operation risk',
338
+ category: 'api',
339
+ defaultSeverity: 'warning',
340
+ description: 'Detects destructive bulk or batch API operations without visible guards, confirmation, batching, dry-run, audit, or async handoff evidence.',
341
+ recommendedFix: 'Add explicit authorization and safety controls such as dryRun, batchSize, confirmation, reason/audit, or queue/job handoff.',
342
+ whyItMatters: 'Bulk write endpoints can cause large blast-radius changes if they are exposed without clear operational safeguards.',
343
+ howToFix: 'Guard bulk endpoints, bound the operation size, require operator intent, and record or stage the action.',
344
+ documentationHint: 'bulk controller endpoints and API operation contracts',
345
+ },
346
+ API_DTO_ENTITY_LEAKAGE: {
347
+ id: exports.ValidationRuleIds.API_DTO_ENTITY_LEAKAGE,
348
+ title: 'DTO/entity leakage through API boundary',
349
+ category: 'api',
350
+ defaultSeverity: 'warning',
351
+ description: 'Detects controller endpoints that appear to return persistence/domain entities directly instead of API DTOs or response contracts.',
352
+ recommendedFix: 'Map persistence/domain entities to DTO, response, resource, or contract types before returning from API handlers.',
353
+ whyItMatters: 'Returning internal entities leaks persistence shape into public contracts and makes API evolution harder.',
354
+ howToFix: 'Introduce explicit response DTOs or serializers and keep repository/domain return values inside the application boundary.',
355
+ documentationHint: 'controller return types, DTOs, serializers, and response schemas',
356
+ },
357
+ API_INCONSISTENT_RESOURCE_DESIGN: {
358
+ id: exports.ValidationRuleIds.API_INCONSISTENT_RESOURCE_DESIGN,
359
+ title: 'Inconsistent resource design within API',
360
+ category: 'api',
361
+ defaultSeverity: 'warning',
362
+ description: 'Detects clear route-shape inconsistencies within the same controller/resource group after multiple examples appear.',
363
+ recommendedFix: 'Choose one resource route convention for the controller and keep action-style exceptions explicit and rare.',
364
+ whyItMatters: 'Inconsistent routes inside one resource make API contracts harder to discover and review.',
365
+ howToFix: 'Align related endpoints around one resource shape, such as /resources/{id}, and document intentional exceptions.',
366
+ documentationHint: 'controller routes and OpenAPI paths',
367
+ },
368
+ API_VERSIONING_STRATEGY_INCONSISTENCY: {
369
+ id: exports.ValidationRuleIds.API_VERSIONING_STRATEGY_INCONSISTENCY,
370
+ title: 'API versioning strategy inconsistency',
371
+ category: 'api',
372
+ defaultSeverity: 'warning',
373
+ description: 'Detects API surfaces that clearly mix versioned and unversioned route strategies in the same backend.',
374
+ recommendedFix: 'Use one versioning convention for public API routes or document a migration boundary for legacy routes.',
375
+ whyItMatters: 'Mixed versioning strategies create uncertainty about compatibility guarantees and contract ownership.',
376
+ howToFix: 'Standardize on path or controller-level versioning, or isolate legacy unversioned routes behind a clear boundary.',
377
+ documentationHint: 'controller base paths and API versioning ADRs',
378
+ },
379
+ API_CONTRACT_IMPLEMENTATION_DRIFT: {
380
+ id: exports.ValidationRuleIds.API_CONTRACT_IMPLEMENTATION_DRIFT,
381
+ title: 'Contract implementation drift',
382
+ category: 'api',
383
+ defaultSeverity: 'warning',
384
+ description: 'Compares discovered implementation endpoints with OpenAPI paths when a parseable contract is available.',
385
+ recommendedFix: 'Update the OpenAPI contract or implementation so method and path pairs describe the same public API surface.',
386
+ whyItMatters: 'Contract drift breaks API review, client generation, and integration expectations even when a contract file exists.',
387
+ howToFix: 'Add missing paths to the contract, remove stale contract paths, or align implementation route paths.',
388
+ documentationHint: 'contracts/openapi.yaml and controller endpoint routes',
389
+ },
390
+ DEP_CIRCULAR_MODULE_DEPENDENCY: {
391
+ id: exports.ValidationRuleIds.DEP_CIRCULAR_MODULE_DEPENDENCY,
392
+ title: 'Circular module dependency detected',
393
+ category: 'dependency',
394
+ defaultSeverity: 'error',
395
+ architectureScope: 'module',
396
+ applicability: { requiresArchitectureModules: true },
397
+ description: 'Checks for circular dependencies between modules under src/modules based on detected cross-module imports.',
398
+ recommendedFix: 'Break circular dependencies between modules by introducing a shared abstraction, moving common code, or reversing the dependency direction.',
399
+ },
400
+ DEP_FORBIDDEN_CROSS_MODULE_DEPENDENCY: {
401
+ id: exports.ValidationRuleIds.DEP_FORBIDDEN_CROSS_MODULE_DEPENDENCY,
402
+ title: 'Forbidden cross-module dependency detected',
403
+ category: 'dependency',
404
+ defaultSeverity: 'error',
405
+ architectureScope: 'module',
406
+ applicability: { requiresArchitectureModules: true },
407
+ description: 'Checks whether cross-module imports violate the allowedDependencies declared in .archpilot/dependency-rules.json.',
408
+ recommendedFix: 'Update .archpilot/dependency-rules.json or refactor the import so the module depends only on explicitly allowed modules.',
409
+ },
410
+ DEP_NON_PUBLIC_CROSS_MODULE_IMPORT: {
411
+ id: exports.ValidationRuleIds.DEP_NON_PUBLIC_CROSS_MODULE_IMPORT,
412
+ title: 'Non-public cross-module import detected',
413
+ category: 'dependency',
414
+ defaultSeverity: 'error',
415
+ architectureScope: 'module',
416
+ applicability: { requiresArchitectureModules: true },
417
+ description: 'Checks whether cross-module imports target another module through its public entrypoints rather than internal implementation files.',
418
+ recommendedFix: 'Import through the target module or package public entrypoint declared in its contract rather than reaching into internal implementation files.',
419
+ },
420
+ DEP_CROSS_MODULE_DEPENDENCY_DECLARED_IN_CONTRACT: {
421
+ id: exports.ValidationRuleIds.DEP_CROSS_MODULE_DEPENDENCY_DECLARED_IN_CONTRACT,
422
+ title: 'Cross-module dependency missing from module contract',
423
+ category: 'dependency',
424
+ defaultSeverity: 'error',
425
+ architectureScope: 'module',
426
+ applicability: { requiresArchitectureModules: true },
427
+ description: 'Checks whether each cross-module import is declared in the source module contract dependsOn list.',
428
+ recommendedFix: 'Add the imported module to source-module contract dependsOn or remove the cross-module dependency.',
429
+ explanation: 'Cross-module imports must be declared in module contracts to keep architectural dependencies explicit.',
430
+ whyItMatters: 'Undeclared dependencies introduce hidden coupling and make impact analysis and governance unreliable.',
431
+ howToFix: 'Update the source module contract dependsOn list to include the target module, or remove the import.',
432
+ exampleFix: `{
433
+ "module": "billing",
434
+ "dependsOn": ["inventory"]
435
+ }`,
436
+ documentationHint: '.archpilot/contracts/<module>.contract.json',
437
+ relatedAdrs: ['ADR-003'],
438
+ },
439
+ DEP_CROSS_MODULE_IMPORT_USES_PUBLIC_SURFACE_ONLY: {
440
+ id: exports.ValidationRuleIds.DEP_CROSS_MODULE_IMPORT_USES_PUBLIC_SURFACE_ONLY,
441
+ title: 'Cross-module import bypasses public module surface',
442
+ category: 'dependency',
443
+ defaultSeverity: 'error',
444
+ architectureScope: 'module',
445
+ applicability: { requiresArchitectureModules: true },
446
+ description: 'Checks whether cross-module imports resolve only through target module public entrypoints declared in module contracts.',
447
+ recommendedFix: 'Import only through target module public entrypoints declared in its module contract.',
448
+ explanation: 'Cross-module imports should use only public entrypoints exposed by the target module contract.',
449
+ whyItMatters: 'Importing internal files bypasses module boundaries and creates brittle coupling across teams and features.',
450
+ howToFix: 'Replace internal path imports with imports from public entrypoints declared by the target module.',
451
+ exampleFix: `import { findUser } from '../identity/public';`,
452
+ documentationHint: '.archpilot/contracts/<module>.contract.json publicEntrypoints',
453
+ relatedAdrs: ['ADR-003'],
454
+ },
455
+ DEP_DECLARED_MODULE_DEPENDENCY_NOT_USED: {
456
+ id: exports.ValidationRuleIds.DEP_DECLARED_MODULE_DEPENDENCY_NOT_USED,
457
+ title: 'Declared module dependency not used',
458
+ category: 'dependency',
459
+ defaultSeverity: 'warning',
460
+ architectureScope: 'module',
461
+ applicability: { requiresArchitectureModules: true },
462
+ description: 'Checks whether module contract dependsOn entries are actually used by cross-module imports from that module.',
463
+ recommendedFix: 'Remove unused modules from dependsOn or add a legitimate cross-module import that reflects the declared dependency.',
464
+ whyItMatters: 'The module contract declares an architectural dependency, but the current code does not use that dependency. That mismatch makes the contract less trustworthy for impact analysis, reviews, and dependency governance.',
465
+ howToFix: 'Choose the architecture decision explicitly: remove the unused dependsOn entry if the dependency is stale, add a real cross-module import through the target public surface if the dependency is intentional, or keep and document the future/temporary dependency with an approved exception.',
466
+ exampleFix: `// Option A: remove the stale dependency from the source module contract.
467
+ {
468
+ "module": "billing",
469
+ "dependsOn": []
470
+ }
471
+
472
+ // Option B: if the dependency is intentional, use the target module in code.
473
+ import { reserveInventory } from "../inventory";`,
474
+ documentationHint: '.archpilot/contracts/<module>.contract.json dependsOn and .archpilot/dependency-rules.json',
475
+ },
476
+ DEP_ISOLATED_MODULE_DETECTED: {
477
+ id: exports.ValidationRuleIds.DEP_ISOLATED_MODULE_DETECTED,
478
+ title: 'Potential orphan module detected',
479
+ category: 'dependency',
480
+ defaultSeverity: 'warning',
481
+ architectureScope: 'module',
482
+ applicability: { requiresArchitectureModules: true },
483
+ description: 'Checks whether a registered module has no peer-module dependencies and little evidence of intentional architectural use.',
484
+ recommendedFix: 'Add intentional-use evidence such as a contract, README, public entrypoint, package entrypoint, or remove the module if it is stale.',
485
+ },
486
+ DEP_TRANSITIVE_CIRCULAR_MODULE_DEPENDENCY: {
487
+ id: exports.ValidationRuleIds.DEP_TRANSITIVE_CIRCULAR_MODULE_DEPENDENCY,
488
+ title: 'Transitive circular module dependency detected',
489
+ category: 'dependency',
490
+ defaultSeverity: 'error',
491
+ architectureScope: 'module',
492
+ applicability: { requiresArchitectureModules: true },
493
+ description: 'Checks whether declared and actual module dependencies form transitive dependency cycles with three or more modules.',
494
+ recommendedFix: 'Break the dependency chain so modules only depend on lower-level modules or shared services.',
495
+ },
496
+ DEP_MODULE_HIGH_INBOUND_DEPENDENCY_COUNT: {
497
+ id: exports.ValidationRuleIds.DEP_MODULE_HIGH_INBOUND_DEPENDENCY_COUNT,
498
+ title: 'Module has high inbound dependency count',
499
+ category: 'dependency',
500
+ defaultSeverity: 'warning',
501
+ architectureScope: 'module',
502
+ applicability: { requiresArchitectureModules: true },
503
+ description: 'Checks whether a module has a high inbound dependency count, which may indicate an overloaded shared module or god module risk.',
504
+ recommendedFix: 'Review whether this module is becoming a shared bottleneck or god module. Consider splitting responsibilities or introducing clearer boundaries.',
505
+ },
506
+ DEP_LAYER_DIRECTION_VIOLATION: {
507
+ id: exports.ValidationRuleIds.DEP_LAYER_DIRECTION_VIOLATION,
508
+ title: 'Dependency violates architecture layer direction',
509
+ category: 'dependency',
510
+ defaultSeverity: 'error',
511
+ architectureScope: 'module',
512
+ applicability: { requiresArchitectureModules: true },
513
+ description: 'Checks whether module dependencies violate configured architecture layer direction rules.',
514
+ recommendedFix: "Generate .archpilot/layer-rules.json through ArchPilot initialization or add the file manually. Then refactor dependencies so modules depend only on modules in lower layers.",
515
+ },
516
+ DEP_CROSS_COMPONENT_POLICY_VIOLATION: {
517
+ id: exports.ValidationRuleIds.DEP_CROSS_COMPONENT_POLICY_VIOLATION,
518
+ title: 'Cross-component dependency policy violation',
519
+ category: 'dependency',
520
+ defaultSeverity: 'error',
521
+ architectureScope: 'component',
522
+ description: 'Checks whether cross-component imports violate allowedDependencies declared on source components in architecture.json.',
523
+ recommendedFix: 'Update the source component allowedDependencies list in .archpilot/architecture.json or refactor the import so the component depends only on explicitly allowed components.',
524
+ whyItMatters: 'Component dependency policies make workspace boundaries explicit so applications, services, and libraries do not drift into hidden coupling.',
525
+ howToFix: 'Add the target component id to sourceComponent.allowedDependencies when the dependency is intentional, or move the shared code/API behind an allowed component boundary.',
526
+ documentationHint: '.archpilot/architecture.json',
527
+ },
528
+ ADR_DIRECTORY_EXISTS: {
529
+ id: exports.ValidationRuleIds.ADR_DIRECTORY_EXISTS,
530
+ title: 'ADR directory missing',
531
+ category: 'documentation',
532
+ defaultSeverity: 'warning',
533
+ description: 'Checks whether docs/adrs/ exists so architecture decisions can be documented and enforced.',
534
+ recommendedFix: 'Create docs/adrs/ and add ADR markdown files such as adr-001-tenant-model.md.',
535
+ },
536
+ ADR_FILE_HAS_TITLE: {
537
+ id: exports.ValidationRuleIds.ADR_FILE_HAS_TITLE,
538
+ title: 'ADR file missing title',
539
+ category: 'documentation',
540
+ defaultSeverity: 'warning',
541
+ description: 'Checks whether each ADR markdown file includes a top-level H1 title near the top of the file.',
542
+ recommendedFix: 'Add a top-level H1 title to the ADR file, for example `# ADR-001: Tenant model`.',
543
+ },
544
+ ADR_STATUS_VALID: {
545
+ id: exports.ValidationRuleIds.ADR_STATUS_VALID,
546
+ title: 'ADR status missing or invalid',
547
+ category: 'documentation',
548
+ defaultSeverity: 'warning',
549
+ description: 'Checks whether each ADR file defines a supported status such as proposed, accepted, deprecated, or superseded.',
550
+ recommendedFix: 'Add `Status: Proposed`, `Status: Accepted`, `Status: Deprecated`, or `Status: Superseded` near the top of the ADR file.',
551
+ },
552
+ ADR_REQUIRED_COVERAGE_PRESENT: {
553
+ id: exports.ValidationRuleIds.ADR_REQUIRED_COVERAGE_PRESENT,
554
+ title: 'Required ADR missing for architecture context',
555
+ category: 'documentation',
556
+ defaultSeverity: 'warning',
557
+ description: 'Checks whether ADR coverage exists for key repository architecture topics such as tenant model, API style, or dependency governance.',
558
+ recommendedFix: 'Add ADRs covering key architecture context, for example tenant model, API style, and dependency governance decisions.',
559
+ },
560
+ SUPPRESSION_REASON_PRESENT: {
561
+ id: exports.ValidationRuleIds.SUPPRESSION_REASON_PRESENT,
562
+ title: 'Suppression reason missing',
563
+ category: 'documentation',
564
+ defaultSeverity: 'warning',
565
+ description: 'Checks whether each suppression entry explains why the exception is needed.',
566
+ recommendedFix: 'Add a concise reason to the suppression entry so the exception remains reviewable.',
567
+ },
568
+ SUPPRESSION_NOT_EXPIRED: {
569
+ id: exports.ValidationRuleIds.SUPPRESSION_NOT_EXPIRED,
570
+ title: 'Suppression expired',
571
+ category: 'documentation',
572
+ defaultSeverity: 'warning',
573
+ description: 'Checks whether suppression entries have passed their expiresOn date.',
574
+ recommendedFix: 'Remove expired suppressions or update expiresOn only if the exception is still justified.',
575
+ },
576
+ SUPPRESSION_MATCHES_ACTIVE_FINDING: {
577
+ id: exports.ValidationRuleIds.SUPPRESSION_MATCHES_ACTIVE_FINDING,
578
+ title: 'Suppression does not match any active finding',
579
+ category: 'documentation',
580
+ defaultSeverity: 'info',
581
+ description: 'Checks whether suppression entries currently match any active validation finding.',
582
+ recommendedFix: 'Remove suppressions that no longer match active findings.',
583
+ },
584
+ SUPPRESSION_NOT_DUPLICATED: {
585
+ id: exports.ValidationRuleIds.SUPPRESSION_NOT_DUPLICATED,
586
+ title: 'Duplicate suppression entry',
587
+ category: 'documentation',
588
+ defaultSeverity: 'warning',
589
+ description: 'Checks whether the same suppression rule and scope combination is configured more than once.',
590
+ recommendedFix: 'Keep a single suppression entry per rule and scope combination.',
591
+ },
592
+ SUPPRESSION_EXPIRY_FORMAT_VALID: {
593
+ id: exports.ValidationRuleIds.SUPPRESSION_EXPIRY_FORMAT_VALID,
594
+ title: 'Suppression expiry format invalid',
595
+ category: 'documentation',
596
+ defaultSeverity: 'warning',
597
+ description: 'Checks whether suppression expiresOn values use YYYY-MM-DD format.',
598
+ recommendedFix: 'Set expiresOn using YYYY-MM-DD, for example 2026-06-30.',
599
+ },
600
+ DB_TENANT_SAME_SCHEMA_TENANT_ID: {
601
+ id: exports.ValidationRuleIds.DB_TENANT_SAME_SCHEMA_TENANT_ID,
602
+ title: 'Tenant discriminator column exists for same-schema model',
603
+ category: 'database',
604
+ defaultSeverity: 'error',
605
+ description: 'Checks that db/sql/001_init.sql contains a tenant_id column when using the same_db_same_schema tenant model.',
606
+ recommendedFix: 'Add a non-null tenant_id column to tenant-scoped tables in db/sql/001_init.sql to safely separate tenant data.',
607
+ },
608
+ DB_SQL_BASELINE_EXISTS: {
609
+ id: exports.ValidationRuleIds.DB_SQL_BASELINE_EXISTS,
610
+ title: 'SQL baseline artifact exists',
611
+ category: 'database',
612
+ defaultSeverity: 'error',
613
+ description: 'Checks whether db/sql/001_init.sql exists as the baseline database artifact generated during initialization.',
614
+ recommendedFix: 'Restore or recreate db/sql/001_init.sql so the initialized database baseline remains available for validation and team workflows.',
615
+ },
616
+ DQR_MISSING_TENANT_FILTER_EVIDENCE: {
617
+ id: exports.ValidationRuleIds.DQR_MISSING_TENANT_FILTER_EVIDENCE,
618
+ title: 'Missing tenant filter evidence in multi-tenant SQL',
619
+ category: 'database',
620
+ defaultSeverity: 'warning',
621
+ description: 'Detects SELECT/UPDATE/DELETE statements on tenant-scoped tables without tenant_id filter evidence when tenant model is same_db_same_schema.',
622
+ recommendedFix: 'Add deterministic tenant_id filter conditions to tenant-scoped queries and verify multi-tenant query boundaries.',
623
+ whyItMatters: 'Missing tenant filters in same-schema models can cause cross-tenant data exposure and governance risk.',
624
+ howToFix: 'Apply explicit tenant_id conditions in query WHERE clauses (or equivalent query constraints) for tenant-scoped tables.',
625
+ documentationHint: 'db/sql and module SQL query files for tenant-scoped tables',
626
+ },
627
+ DQR_BROAD_SELECT_STAR_USAGE: {
628
+ id: exports.ValidationRuleIds.DQR_BROAD_SELECT_STAR_USAGE,
629
+ title: 'Broad SELECT * usage in query path',
630
+ category: 'database',
631
+ defaultSeverity: 'warning',
632
+ description: 'Detects broad SELECT * query patterns in SQL files where explicit column selection is usually safer and more stable.',
633
+ recommendedFix: 'Replace SELECT * with explicit column lists for architecture-critical query paths.',
634
+ whyItMatters: 'SELECT * increases coupling to schema changes and can inflate data access blast radius.',
635
+ howToFix: 'List required columns explicitly and keep query projections aligned with module contracts and API needs.',
636
+ documentationHint: 'db/sql and module SQL query files',
637
+ },
638
+ DQR_UNBOUNDED_QUERY_WITHOUT_PAGINATION: {
639
+ id: exports.ValidationRuleIds.DQR_UNBOUNDED_QUERY_WITHOUT_PAGINATION,
640
+ title: 'Unbounded query risk without limit/pagination evidence',
641
+ category: 'database',
642
+ defaultSeverity: 'warning',
643
+ description: 'Detects list-oriented SELECT queries in SQL files without LIMIT/OFFSET/FETCH evidence when no obvious point lookup is present.',
644
+ recommendedFix: 'Add deterministic pagination/limit controls for list queries and keep query size bounded by design.',
645
+ whyItMatters: 'Unbounded list queries can create scalability and operational risk as data grows.',
646
+ howToFix: 'Use LIMIT/OFFSET/FETCH or equivalent bounded query patterns and avoid unrestricted list reads in critical paths.',
647
+ documentationHint: 'db/sql and module SQL query files',
648
+ },
649
+ DQR_CROSS_MODULE_DIRECT_DB_ACCESS: {
650
+ id: exports.ValidationRuleIds.DQR_CROSS_MODULE_DIRECT_DB_ACCESS,
651
+ title: 'Cross-module direct database access bypasses module boundary',
652
+ category: 'database',
653
+ defaultSeverity: 'warning',
654
+ architectureScope: 'module',
655
+ applicability: { requiresArchitectureModules: true },
656
+ description: 'Detects high-confidence references from one module to another module database artifact path (for example /<module>/db/ or /<module>/sql/).',
657
+ recommendedFix: 'Route cross-module data access through module contracts/public surfaces instead of direct database artifact references.',
658
+ whyItMatters: 'Direct cross-module DB access can bypass architecture boundaries and create hidden coupling.',
659
+ howToFix: 'Expose required data through target module public contracts/APIs and remove direct path-level DB artifact coupling.',
660
+ documentationHint: 'module source files referencing other module db/sql paths',
661
+ },
662
+ DQR_N_PLUS_ONE_QUERY_EVIDENCE: {
663
+ id: exports.ValidationRuleIds.DQR_N_PLUS_ONE_QUERY_EVIDENCE,
664
+ title: 'N+1 query evidence in collection loop',
665
+ category: 'database',
666
+ defaultSeverity: 'warning',
667
+ description: 'Detects high-confidence repository/database reads inside collection loops without visible batching evidence.',
668
+ recommendedFix: 'Batch the lookup with an IN query/DataLoader-style loader or prefetch related records before iterating.',
669
+ whyItMatters: 'N+1 query paths can turn one request into many database round trips as collection size grows.',
670
+ howToFix: 'Move database reads out of the loop and use batched query constraints, joins, includes, or a request-scoped loader.',
671
+ documentationHint: 'service and repository methods with collection loops',
672
+ },
673
+ DQR_LARGE_COLLECTION_LOADING: {
674
+ id: exports.ValidationRuleIds.DQR_LARGE_COLLECTION_LOADING,
675
+ title: 'Large collection loaded then processed in memory',
676
+ category: 'database',
677
+ defaultSeverity: 'warning',
678
+ description: 'Detects obvious unbounded collection reads followed by in-memory filtering, mapping, or reducing.',
679
+ recommendedFix: 'Push filters/projections into the query and add pagination or explicit limits before processing collections.',
680
+ whyItMatters: 'Loading large collections into memory can create latency, memory pressure, and unstable request behavior.',
681
+ howToFix: 'Add take/limit/page/cursor controls and move filter or projection logic into the database query when practical.',
682
+ documentationHint: 'service and repository methods with list reads',
683
+ },
684
+ DQR_MISSING_PROJECTION_SELECT: {
685
+ id: exports.ValidationRuleIds.DQR_MISSING_PROJECTION_SELECT,
686
+ title: 'Missing projection/select for partial entity use',
687
+ category: 'database',
688
+ defaultSeverity: 'warning',
689
+ description: 'Detects list reads of full entities where only a small number of fields are used afterward and no projection is present.',
690
+ recommendedFix: 'Add select/projection/fields to fetch only the fields needed by this flow.',
691
+ whyItMatters: 'Missing projections increase data transfer, couple callers to full entity shape, and can expose unnecessary fields.',
692
+ howToFix: 'Use Prisma select, repository projections, DTO queries, or equivalent field selection in the read path.',
693
+ documentationHint: 'service and repository methods with partial entity use',
694
+ },
695
+ DQR_REPEATED_QUERY_PATTERN: {
696
+ id: exports.ValidationRuleIds.DQR_REPEATED_QUERY_PATTERN,
697
+ title: 'Repeated query pattern in one request flow',
698
+ category: 'database',
699
+ defaultSeverity: 'warning',
700
+ description: 'Detects repeated identical or near-identical query calls in a single method/request flow.',
701
+ recommendedFix: 'Reuse the first query result or consolidate repeated reads into one batched query.',
702
+ whyItMatters: 'Repeated query calls add avoidable database load and can hide request-flow coupling.',
703
+ howToFix: 'Cache the result in the method scope, batch related reads, or refactor duplicate query paths into one repository call.',
704
+ documentationHint: 'service and repository methods with repeated reads',
705
+ },
706
+ EVT_PUBLISHED_EVENT_HAS_NO_HANDLER: {
707
+ id: exports.ValidationRuleIds.EVT_PUBLISHED_EVENT_HAS_NO_HANDLER,
708
+ title: 'Published event has no handler',
709
+ category: 'architecture',
710
+ defaultSeverity: 'warning',
711
+ description: 'Detects statically visible events that are emitted/published but have no matching listener/handler in the workspace.',
712
+ recommendedFix: 'Add a handler/listener for the event or remove/rename the publish call if it is stale.',
713
+ whyItMatters: 'Unhandled events create dead architecture paths and make async workflows look reliable when no consumer exists.',
714
+ howToFix: 'Register an OnEvent/EventListener/EventPattern handler with the same event name or class.',
715
+ documentationHint: 'event publishers and handlers',
716
+ },
717
+ EVT_CIRCULAR_EVENT_CHAIN_RISK: {
718
+ id: exports.ValidationRuleIds.EVT_CIRCULAR_EVENT_CHAIN_RISK,
719
+ title: 'Circular event chain risk',
720
+ category: 'architecture',
721
+ defaultSeverity: 'warning',
722
+ description: 'Detects clear two-event static cycles where a handler for one event publishes another whose handler publishes the first.',
723
+ recommendedFix: 'Break the cycle with a coordinator, idempotent state transition, or explicit workflow boundary.',
724
+ whyItMatters: 'Circular event chains can produce duplicate processing, retries, or unstable async workflows.',
725
+ howToFix: 'Replace cyclic event reactions with one orchestrated workflow step or add explicit terminal conditions.',
726
+ documentationHint: 'event handler publish chains',
727
+ },
728
+ EVT_SYNCHRONOUS_DEPENDENCY_DISGUISED_AS_EVENT: {
729
+ id: exports.ValidationRuleIds.EVT_SYNCHRONOUS_DEPENDENCY_DISGUISED_AS_EVENT,
730
+ title: 'Synchronous dependency disguised as event',
731
+ category: 'architecture',
732
+ defaultSeverity: 'warning',
733
+ description: 'Detects event publish/emit calls that are awaited, assigned, or returned like request/response dependencies.',
734
+ recommendedFix: 'Use a direct service/API call for request-response behavior or make the event fire-and-forget with explicit follow-up handling.',
735
+ whyItMatters: 'Request-response behavior hidden behind events obscures coupling and weakens async architecture boundaries.',
736
+ howToFix: 'Choose either an explicit synchronous dependency or a true async handoff with durable event semantics.',
737
+ documentationHint: 'event bus publish/emit call sites',
738
+ },
739
+ EVT_EVENT_NAMING_INCONSISTENCY: {
740
+ id: exports.ValidationRuleIds.EVT_EVENT_NAMING_INCONSISTENCY,
741
+ title: 'Event naming inconsistency',
742
+ category: 'architecture',
743
+ defaultSeverity: 'warning',
744
+ description: 'Detects repeated mixed event naming styles for the same event surface.',
745
+ recommendedFix: 'Normalize event names to one convention such as user.created or UserCreatedEvent.',
746
+ whyItMatters: 'Mixed event names make async workflows harder to trace and increase handler mismatch risk.',
747
+ howToFix: 'Rename publishers and handlers to a single convention and keep aliases out of production event surfaces.',
748
+ documentationHint: 'event names/classes',
749
+ },
750
+ APP_CONTROLLER_BUSINESS_LOGIC: {
751
+ id: exports.ValidationRuleIds.APP_CONTROLLER_BUSINESS_LOGIC,
752
+ title: 'Controller contains business logic',
753
+ category: 'architecture',
754
+ defaultSeverity: 'warning',
755
+ description: 'Detects high-confidence controller files that contain persistence or business workflow logic instead of delegating to an application service.',
756
+ recommendedFix: 'Move orchestration and persistence access out of the controller and into an application service.',
757
+ whyItMatters: 'Controllers should adapt transport concerns to application use cases. Business logic in controllers makes APIs harder to test, reuse, and govern.',
758
+ howToFix: 'Keep request/response mapping in the controller, call a service method, and move repository/database access behind that service boundary.',
759
+ documentationHint: 'controller and service source files',
760
+ },
761
+ APP_REPOSITORY_BUSINESS_LOGIC: {
762
+ id: exports.ValidationRuleIds.APP_REPOSITORY_BUSINESS_LOGIC,
763
+ title: 'Repository contains business logic',
764
+ category: 'architecture',
765
+ defaultSeverity: 'warning',
766
+ description: 'Detects repository files that appear to contain workflow, integration, or application-service responsibilities.',
767
+ recommendedFix: 'Keep repositories focused on persistence and move workflow/integration behavior into services.',
768
+ whyItMatters: 'Repositories should isolate data access. Workflow or integration logic in repositories hides application behavior inside persistence code.',
769
+ howToFix: 'Move cross-service calls, event publishing, queues, mailers, and HTTP integrations into an application service that uses the repository.',
770
+ documentationHint: 'repository and service source files',
771
+ },
772
+ APP_SERVICE_BYPASSES_REPOSITORY: {
773
+ id: exports.ValidationRuleIds.APP_SERVICE_BYPASSES_REPOSITORY,
774
+ title: 'Service bypasses repository layer',
775
+ category: 'architecture',
776
+ defaultSeverity: 'warning',
777
+ description: 'Detects service files that directly access database primitives when a repository/persistence layer is present or strongly implied.',
778
+ recommendedFix: 'Route service persistence operations through repositories or explicit persistence adapters.',
779
+ whyItMatters: 'Direct database access from services can bypass persistence boundaries and spread schema coupling across application logic.',
780
+ howToFix: 'Move direct SQL/ORM client usage into a repository or persistence adapter and inject that abstraction into the service.',
781
+ documentationHint: 'service and repository source files',
782
+ },
783
+ APP_PACKAGE_RESPONSIBILITY_MISMATCH: {
784
+ id: exports.ValidationRuleIds.APP_PACKAGE_RESPONSIBILITY_MISMATCH,
785
+ title: 'Package responsibility mismatch',
786
+ category: 'architecture',
787
+ defaultSeverity: 'warning',
788
+ description: 'Detects files whose declared controller/service/repository responsibility conflicts with their package or folder role.',
789
+ recommendedFix: 'Move the file to the matching package or rename/refactor the class so package responsibility stays clear.',
790
+ whyItMatters: 'Package names are architecture signals. Mismatched roles make the codebase harder to navigate and weaken automated boundary checks.',
791
+ howToFix: 'Place controllers under controller packages, services under service packages, and repositories under repository or persistence packages.',
792
+ documentationHint: 'controller, service, and repository package layout',
793
+ },
794
+ APP_SERVICE_GOD_CLASS: {
795
+ id: exports.ValidationRuleIds.APP_SERVICE_GOD_CLASS,
796
+ title: 'Service has oversized responsibility',
797
+ category: 'architecture',
798
+ defaultSeverity: 'warning',
799
+ description: 'Detects very large or highly coupled service classes that likely combine multiple application responsibilities.',
800
+ recommendedFix: 'Split the service by use case or cohesive responsibility and move secondary concerns behind smaller collaborators.',
801
+ whyItMatters: 'Oversized services concentrate change risk and often become informal architecture boundaries that are difficult to govern.',
802
+ howToFix: 'Extract focused use-case services, keep dependencies cohesive, and leave shared infrastructure behind explicit adapters.',
803
+ documentationHint: 'service source files',
804
+ },
805
+ AUTH_MISSING_AUTHORIZATION_ENFORCEMENT: {
806
+ id: exports.ValidationRuleIds.AUTH_MISSING_AUTHORIZATION_ENFORCEMENT,
807
+ title: 'Missing authorization enforcement',
808
+ category: 'security',
809
+ defaultSeverity: 'warning',
810
+ description: 'Detects protected-looking endpoints that lack authorization evidence while nearby peer endpoints clearly use authorization.',
811
+ recommendedFix: 'Add the same guard, role, permission, or public-marker convention used by peer endpoints in the controller or resource.',
812
+ whyItMatters: 'Inconsistent authorization at endpoint boundaries can leave sensitive operations exposed while similar operations are protected.',
813
+ howToFix: 'Apply the appropriate guard/decorator or explicitly mark the endpoint public when that is intentional.',
814
+ documentationHint: 'controller/resource authorization decorators and security configuration',
815
+ },
816
+ AUTH_INCONSISTENT_AUTHORIZATION: {
817
+ id: exports.ValidationRuleIds.AUTH_INCONSISTENT_AUTHORIZATION,
818
+ title: 'Inconsistent authorization within resource',
819
+ category: 'security',
820
+ defaultSeverity: 'warning',
821
+ description: 'Detects endpoints in the same controller/resource with weaker authorization evidence than protected peers.',
822
+ recommendedFix: 'Align role or permission requirements across similar protected operations, or document and mark intentional public access.',
823
+ whyItMatters: 'Authorization drift within the same resource is difficult to notice in review and can create uneven access control.',
824
+ howToFix: 'Use consistent roles, permissions, guards, or explicit public annotations across related endpoint methods.',
825
+ documentationHint: 'controller/resource authorization decorators and security configuration',
826
+ },
827
+ AUTH_AUTHORIZATION_IN_WRONG_LAYER: {
828
+ id: exports.ValidationRuleIds.AUTH_AUTHORIZATION_IN_WRONG_LAYER,
829
+ title: 'Authorization logic placed in wrong layer',
830
+ category: 'security',
831
+ defaultSeverity: 'warning',
832
+ description: 'Detects role or permission authorization decisions inside repository or data-access classes.',
833
+ recommendedFix: 'Move role/permission authorization decisions to controller guards, middleware, policies, or application services.',
834
+ whyItMatters: 'Authorization buried in persistence code is harder to audit and can scatter access decisions across data paths.',
835
+ howToFix: 'Keep repositories focused on data access and enforce authorization through a consistent application/security boundary.',
836
+ documentationHint: 'repositories, services, guards, and security policies',
837
+ },
838
+ TEN_MISSING_TENANT_CONTEXT_PROPAGATION: {
839
+ id: exports.ValidationRuleIds.TEN_MISSING_TENANT_CONTEXT_PROPAGATION,
840
+ title: 'Missing tenant context propagation',
841
+ category: 'security',
842
+ defaultSeverity: 'warning',
843
+ description: 'Detects service methods that use tenant context but call repository operations without passing tenant or organization scope.',
844
+ recommendedFix: 'Pass tenant/organization scope through service and repository boundaries consistently.',
845
+ whyItMatters: 'Lost tenant context can cause lower layers to query or mutate data without the isolation scope required by the architecture.',
846
+ howToFix: 'Derive tenant context once at the boundary, pass it to services and repositories, and include it in query predicates.',
847
+ documentationHint: 'tenant context providers, services, and repository calls',
848
+ },
849
+ TEN_MISSING_TENANT_ENFORCEMENT: {
850
+ id: exports.ValidationRuleIds.TEN_MISSING_TENANT_ENFORCEMENT,
851
+ title: 'Missing tenant enforcement on data access',
852
+ category: 'security',
853
+ defaultSeverity: 'warning',
854
+ description: 'Detects ORM/query operations in tenant-scoped repositories without tenant or organization predicate evidence.',
855
+ recommendedFix: 'Add tenant/organization predicates to tenant-scoped repository queries and mutations.',
856
+ whyItMatters: 'Same-schema tenant isolation depends on every tenant-scoped data access path applying the tenant discriminator.',
857
+ howToFix: 'Include tenantId, organizationId, orgId, workspaceId, or equivalent scope in repository where clauses and query builders.',
858
+ documentationHint: 'tenant-scoped repository query methods',
859
+ },
860
+ TEN_CROSS_TENANT_ACCESS_RISK: {
861
+ id: exports.ValidationRuleIds.TEN_CROSS_TENANT_ACCESS_RISK,
862
+ title: 'Cross-tenant access risk',
863
+ category: 'security',
864
+ defaultSeverity: 'warning',
865
+ description: 'Detects destructive tenant-scoped data operations that appear to be scoped only by entity id.',
866
+ recommendedFix: 'Scope destructive tenant operations by tenant/organization as well as entity id, or isolate global/admin paths clearly.',
867
+ whyItMatters: 'Update/delete-by-id without tenant scope can mutate data across tenant boundaries when identifiers collide or leak.',
868
+ howToFix: 'Add tenant/org predicates to write operations or route explicit global/admin operations through guarded paths.',
869
+ documentationHint: 'tenant-scoped repository mutation methods',
870
+ },
871
+ TEN_TENANT_MODEL_UNDOCUMENTED: {
872
+ id: exports.ValidationRuleIds.TEN_TENANT_MODEL_UNDOCUMENTED,
873
+ title: 'Tenant isolation model undocumented or inconsistent',
874
+ category: 'security',
875
+ defaultSeverity: 'info',
876
+ description: 'Reports a single guidance finding when configured tenancy and scanned source tenant evidence appear inconsistent.',
877
+ recommendedFix: 'Document the tenant isolation model and ensure code paths show consistent tenant resolver or predicate evidence.',
878
+ whyItMatters: 'Tenant isolation is architecture-critical and should be visible in both configuration and implementation conventions.',
879
+ howToFix: 'Update architecture configuration/docs or add consistent tenant context and query-scope patterns in implementation code.',
880
+ documentationHint: '.archpilot/architecture.json and tenant context/query conventions',
881
+ },
882
+ TXN_TRANSACTION_BOUNDARY_TOO_BROAD: {
883
+ id: exports.ValidationRuleIds.TXN_TRANSACTION_BOUNDARY_TOO_BROAD,
884
+ title: 'Transaction boundary too broad',
885
+ category: 'architecture',
886
+ defaultSeverity: 'warning',
887
+ description: 'Detects high-confidence transaction scopes that coordinate many aggregate or repository collaborators.',
888
+ recommendedFix: 'Narrow the transaction to one cohesive aggregate operation or split orchestration from persistence changes.',
889
+ whyItMatters: 'Broad transaction scopes increase lock time, failure coupling, and ambiguity about which domain boundary owns the change.',
890
+ howToFix: 'Keep transaction ownership in an application service and limit the scope to the repositories needed for one cohesive use case.',
891
+ documentationHint: 'transactional service methods and repository collaborators',
892
+ },
893
+ TXN_TRANSACTION_STARTED_IN_CONTROLLER: {
894
+ id: exports.ValidationRuleIds.TXN_TRANSACTION_STARTED_IN_CONTROLLER,
895
+ title: 'Transaction started in controller/API layer',
896
+ category: 'architecture',
897
+ defaultSeverity: 'warning',
898
+ description: 'Detects controller or endpoint methods that appear to own transaction lifecycle or transactional annotations.',
899
+ recommendedFix: 'Move transaction boundaries from controllers into application service methods.',
900
+ whyItMatters: 'Controllers should adapt transport concerns; owning transactions there mixes API handling with application consistency rules.',
901
+ howToFix: 'Let the controller call a service use case and place @Transactional, transaction manager, or ORM transaction usage there.',
902
+ documentationHint: 'controllers and service transaction boundaries',
903
+ },
904
+ TXN_NESTED_TRANSACTION_RISK: {
905
+ id: exports.ValidationRuleIds.TXN_NESTED_TRANSACTION_RISK,
906
+ title: 'Nested transaction risk',
907
+ category: 'architecture',
908
+ defaultSeverity: 'warning',
909
+ description: 'Detects strong evidence that one transaction scope starts or calls another transaction scope.',
910
+ recommendedFix: 'Flatten nested transaction ownership or make propagation semantics explicit and intentional.',
911
+ whyItMatters: 'Nested transaction behavior is framework-specific and can produce surprising commits, rollbacks, or lock behavior.',
912
+ howToFix: 'Use one outer service transaction for the use case or document explicit propagation such as REQUIRES_NEW.',
913
+ documentationHint: 'transactional service methods and transaction propagation settings',
914
+ },
915
+ TXN_LONG_RUNNING_WORKFLOW_INSIDE_TRANSACTION: {
916
+ id: exports.ValidationRuleIds.TXN_LONG_RUNNING_WORKFLOW_INSIDE_TRANSACTION,
917
+ title: 'Long-running workflow inside transaction',
918
+ category: 'architecture',
919
+ defaultSeverity: 'warning',
920
+ description: 'Detects transaction scopes that include external I/O, queues, messaging, email, uploads, sleeps, or retries.',
921
+ recommendedFix: 'Move external I/O outside the database transaction and publish after commit when possible.',
922
+ whyItMatters: 'Long-running work inside a transaction can hold locks while waiting on systems the database cannot control.',
923
+ howToFix: 'Persist the state change in the transaction, then dispatch messages or call external systems after commit or through an outbox.',
924
+ documentationHint: 'transactional methods, messaging, email, queue, and HTTP integration calls',
925
+ },
926
+ DOM_ANEMIC_DOMAIN_MODEL: {
927
+ id: exports.ValidationRuleIds.DOM_ANEMIC_DOMAIN_MODEL,
928
+ title: 'Anemic domain model',
929
+ category: 'architecture',
930
+ defaultSeverity: 'warning',
931
+ description: 'Detects meaningful domain/entity models that are mostly state while business decisions appear in services.',
932
+ recommendedFix: 'Move cohesive domain decisions and invariants onto aggregate/domain methods where appropriate.',
933
+ whyItMatters: 'When domain models only hold data, business rules scatter into services and become harder to keep consistent.',
934
+ howToFix: 'Keep DTOs as data carriers, but give domain entities or aggregates methods for invariants, transitions, and decisions.',
935
+ documentationHint: 'domain entities, aggregates, and application services',
936
+ },
937
+ DOM_AGGREGATE_BOUNDARY_VIOLATION: {
938
+ id: exports.ValidationRuleIds.DOM_AGGREGATE_BOUNDARY_VIOLATION,
939
+ title: 'Aggregate boundary violation',
940
+ category: 'architecture',
941
+ defaultSeverity: 'warning',
942
+ description: 'Detects service methods that appear to reach directly into another aggregate or module repository.',
943
+ recommendedFix: 'Coordinate across aggregates through an application service, published contract, or explicit domain service.',
944
+ whyItMatters: 'Directly manipulating another aggregate bypasses ownership boundaries and can create hidden coupling between domains.',
945
+ howToFix: 'Route cross-aggregate work through a clear boundary, or move the use case to the module that owns the aggregate.',
946
+ documentationHint: 'module contracts, aggregate repositories, and domain services',
947
+ },
948
+ DOM_OVERSIZED_AGGREGATE_OR_SERVICE_BOUNDARY: {
949
+ id: exports.ValidationRuleIds.DOM_OVERSIZED_AGGREGATE_OR_SERVICE_BOUNDARY,
950
+ title: 'Oversized aggregate/service boundary',
951
+ category: 'architecture',
952
+ defaultSeverity: 'warning',
953
+ description: 'Detects services with enough aggregate collaborators or operations to suggest unclear domain ownership.',
954
+ recommendedFix: 'Split oversized domain/application services by aggregate ownership or cohesive use case.',
955
+ whyItMatters: 'Oversized domain boundaries hide multiple responsibilities behind one service and make change ownership unclear.',
956
+ howToFix: 'Extract smaller services around cohesive aggregate operations and move unrelated collaborators behind explicit boundaries.',
957
+ documentationHint: 'domain services, application services, and aggregate repositories',
958
+ },
959
+ DOM_DOMAIN_LOGIC_SCATTERED_ACROSS_LAYERS: {
960
+ id: exports.ValidationRuleIds.DOM_DOMAIN_LOGIC_SCATTERED_ACROSS_LAYERS,
961
+ title: 'Domain logic scattered across layers',
962
+ category: 'architecture',
963
+ defaultSeverity: 'warning',
964
+ description: 'Detects business decision evidence in controllers or repositories instead of a domain/application boundary.',
965
+ recommendedFix: 'Move business decisions into application services, domain services, or aggregate methods.',
966
+ whyItMatters: 'Business rules in transport or persistence layers are harder to reuse, test, and govern consistently.',
967
+ howToFix: 'Keep controllers focused on request mapping and repositories focused on persistence; place rules in the domain/application layer.',
968
+ documentationHint: 'controllers, repositories, services, and domain model classes',
969
+ },
970
+ DOM_CROSS_AGGREGATE_MODIFICATION_RISK: {
971
+ id: exports.ValidationRuleIds.DOM_CROSS_AGGREGATE_MODIFICATION_RISK,
972
+ title: 'Cross-aggregate modification risk',
973
+ category: 'architecture',
974
+ defaultSeverity: 'warning',
975
+ description: 'Detects transaction scopes that mutate multiple aggregate or repository roots.',
976
+ recommendedFix: 'Keep one transaction focused on one aggregate boundary, or make cross-aggregate orchestration explicit.',
977
+ whyItMatters: 'Mutating unrelated aggregates in one transaction can blur ownership and make failure handling difficult.',
978
+ howToFix: 'Separate unrelated aggregate updates, use domain events/outbox patterns, or document an intentional aggregate-spanning use case.',
979
+ documentationHint: 'transactional service methods and aggregate repositories',
980
+ },
981
+ DOC_TENANT_DIFFERENT_SCHEMA_ADR: {
982
+ id: exports.ValidationRuleIds.DOC_TENANT_DIFFERENT_SCHEMA_ADR,
983
+ title: 'Tenant model ADR exists for different-schema model',
984
+ category: 'documentation',
985
+ defaultSeverity: 'warning',
986
+ description: 'Checks that an ADR describing the tenant model decision exists when using the same_db_different_schema model.',
987
+ recommendedFix: 'Create docs/adrs/adr-001-tenant-model.md documenting why different schemas per tenant were chosen and their implications.',
988
+ relatedAdrs: ['ADR-001'],
989
+ },
990
+ DOC_RBAC_MENTIONED_IN_OVERVIEW: {
991
+ id: exports.ValidationRuleIds.DOC_RBAC_MENTIONED_IN_OVERVIEW,
992
+ title: 'RBAC or role-based access control mentioned in overview',
993
+ category: 'documentation',
994
+ defaultSeverity: 'warning',
995
+ description: 'Checks that the architecture overview mentions RBAC or role-based access control when RBAC is enabled.',
996
+ recommendedFix: 'Update docs/architecture/overview.md to describe how RBAC is implemented and enforced across the system.',
997
+ },
998
+ SAFE_MISSING_ADR_COVERAGE: {
999
+ id: exports.ValidationRuleIds.SAFE_MISSING_ADR_COVERAGE,
1000
+ title: 'Missing ADR coverage for risky architecture change',
1001
+ category: 'architecture',
1002
+ defaultSeverity: 'warning',
1003
+ description: 'Derived governance signal indicating ADR coverage is required but not satisfied for a risky architecture change context.',
1004
+ recommendedFix: 'Create or update the required ADR and ensure ADR requirement checks are satisfied in the current run.',
1005
+ whyItMatters: 'Risky architecture changes should be documented with ADR coverage to keep decisions and tradeoffs auditable.',
1006
+ howToFix: 'Use the suggested ADR command from review output, then re-run `archpilot validate`.',
1007
+ },
1008
+ SAFE_MISSING_ROLLBACK_PLAN: {
1009
+ id: exports.ValidationRuleIds.SAFE_MISSING_ROLLBACK_PLAN,
1010
+ title: 'Missing rollback plan for risky architecture change',
1011
+ category: 'architecture',
1012
+ defaultSeverity: 'warning',
1013
+ description: 'Derived governance signal indicating rollback evidence is missing in a risky architecture change context.',
1014
+ recommendedFix: 'Create .archpilot/rollout/rollback-plan.md with rollback trigger, steps, owner, and validation checks.',
1015
+ whyItMatters: 'Rollback planning reduces governance and operational risk when architecture changes need controlled reversal.',
1016
+ howToFix: 'Run `archpilot safety init` or create the rollback plan artifact manually, then re-run validation.',
1017
+ },
1018
+ SAFE_MISSING_FEATURE_FLAG_EVIDENCE: {
1019
+ id: exports.ValidationRuleIds.SAFE_MISSING_FEATURE_FLAG_EVIDENCE,
1020
+ title: 'Missing feature-flag or staged rollout evidence for risky architecture change',
1021
+ category: 'architecture',
1022
+ defaultSeverity: 'warning',
1023
+ description: 'Derived governance signal indicating feature flag or staged rollout evidence is missing or invalid in a risky architecture change context.',
1024
+ recommendedFix: 'Create .archpilot/rollout/feature-flags.json with enabled=true or non-empty flags[] entries.',
1025
+ whyItMatters: 'Staged rollout controls help reduce blast radius while introducing higher-risk architecture changes.',
1026
+ howToFix: 'Run `archpilot safety init` or fix the rollout config file, then re-run validation.',
1027
+ },
1028
+ };
1029
+ function getValidationRuleMetadataById(id) {
1030
+ for (const key of Object.keys(exports.ValidationRuleCatalog)) {
1031
+ const meta = exports.ValidationRuleCatalog[key];
1032
+ if (meta.id === id) {
1033
+ return meta;
1034
+ }
1035
+ }
1036
+ // Fallback metadata for unknown rule IDs to keep reporting robust.
1037
+ return {
1038
+ id,
1039
+ title: 'Unknown rule',
1040
+ category: 'documentation',
1041
+ defaultSeverity: 'info',
1042
+ description: 'This rule is not registered in the catalog. It may be new or incorrectly configured.',
1043
+ recommendedFix: 'Review the project structure and rule implementation to understand and address this rule.',
1044
+ };
1045
+ }
1046
+ function getAllValidationRuleMetadata() {
1047
+ return Object.keys(exports.ValidationRuleCatalog)
1048
+ .map((key) => exports.ValidationRuleCatalog[key])
1049
+ .sort((a, b) => a.id.localeCompare(b.id));
1050
+ }
1051
+ function getConfiguredArchitectureModuleIds(contract) {
1052
+ const moduleIds = new Set(Object.keys(contract.modules ?? {}));
1053
+ const components = contract.components;
1054
+ if (components && typeof components === 'object') {
1055
+ const entries = Array.isArray(components)
1056
+ ? components.map((component, index) => [component.id ?? `component-${index + 1}`, component])
1057
+ : Object.entries(components);
1058
+ for (const [componentId, component] of entries) {
1059
+ if (!component || typeof component !== 'object' || Array.isArray(component)) {
1060
+ continue;
1061
+ }
1062
+ for (const moduleId of Object.keys(component.modules ?? {})) {
1063
+ moduleIds.add(moduleId.includes('/') ? moduleId : `${componentId}/${moduleId}`);
1064
+ }
1065
+ }
1066
+ }
1067
+ return [...moduleIds].sort((left, right) => left.localeCompare(right));
1068
+ }
1069
+ function hasConfiguredArchitectureModules(contract) {
1070
+ return getConfiguredArchitectureModuleIds(contract).length > 0;
1071
+ }
1072
+ function buildNotApplicableRuleResults(contract, validationConfig) {
1073
+ const hasModules = hasConfiguredArchitectureModules(contract);
1074
+ return getAllValidationRuleMetadata()
1075
+ .filter((meta) => meta.applicability?.requiresArchitectureModules === true)
1076
+ .filter((meta) => !isRuleDisabled(meta.id, validationConfig))
1077
+ .filter(() => !hasModules)
1078
+ .map((meta) => ({
1079
+ id: meta.id,
1080
+ severity: meta.defaultSeverity,
1081
+ passed: true,
1082
+ applicability: 'not-applicable',
1083
+ applicabilityReason: 'No architecture modules are configured at the project root or under components.',
1084
+ message: `${meta.title} is not applicable because this architecture has no configured modules.`,
1085
+ }));
1086
+ }
1087
+ function applyRuleApplicability(results, contract, validationConfig, hasArchitectureModulesOverride) {
1088
+ if (hasArchitectureModulesOverride ?? hasConfiguredArchitectureModules(contract)) {
1089
+ return results.map((result) => ({
1090
+ ...result,
1091
+ applicability: result.applicability ?? 'applicable',
1092
+ }));
1093
+ }
1094
+ const moduleRequiredRuleIds = new Set(getAllValidationRuleMetadata()
1095
+ .filter((meta) => meta.applicability?.requiresArchitectureModules === true)
1096
+ .map((meta) => meta.id));
1097
+ return [
1098
+ ...results
1099
+ .filter((result) => !moduleRequiredRuleIds.has(result.id))
1100
+ .map((result) => ({
1101
+ ...result,
1102
+ applicability: result.applicability ?? 'applicable',
1103
+ })),
1104
+ ...buildNotApplicableRuleResults(contract, validationConfig),
1105
+ ];
1106
+ }
1107
+ function formatValidationRuleLabel(id) {
1108
+ const meta = getValidationRuleMetadataById(id);
1109
+ return `[${meta.id}] ${meta.title}`;
1110
+ }
1111
+ function isEnforcementRuleCategory(category) {
1112
+ return (category === 'architecture' ||
1113
+ category === 'api' ||
1114
+ category === 'dependency' ||
1115
+ category === 'database');
1116
+ }
1117
+ function getValidationSeverityPolicyNote() {
1118
+ return 'ArchPilot uses enforcement-oriented severities by default: core architecture, API, dependency, and database safety violations are treated as errors; documentation gaps are warnings; advisory compatibility notices are informational.';
1119
+ }
1120
+ function getSeveritySortOrder(severity) {
1121
+ switch (severity) {
1122
+ case 'error':
1123
+ return 1;
1124
+ case 'warning':
1125
+ return 2;
1126
+ case 'info':
1127
+ return 3;
1128
+ default:
1129
+ return 99;
1130
+ }
1131
+ }
1132
+ function getValidationOverallStatus(summary) {
1133
+ if (summary.errors > 0) {
1134
+ return 'fail';
1135
+ }
1136
+ if (summary.warnings > 0) {
1137
+ return 'warn';
1138
+ }
1139
+ return 'pass';
1140
+ }
1141
+ function isHeuristicArchitectureSmellRuleId(ruleId) {
1142
+ return /^AP-(?:APP|DOM|TXN)-/u.test(ruleId);
1143
+ }
1144
+ function buildArchitectureSmellSummaryFromResults(results) {
1145
+ const failedSmells = results.filter((result) => result.applicability !== 'not-applicable' &&
1146
+ !result.passed &&
1147
+ isHeuristicArchitectureSmellRuleId(result.id));
1148
+ const familyEntries = ['AP-APP', 'AP-DOM', 'AP-TXN']
1149
+ .map((family) => {
1150
+ const familyResults = failedSmells.filter((result) => result.id.startsWith(family));
1151
+ const ruleCounts = [...familyResults.reduce((acc, result) => {
1152
+ acc.set(result.id, (acc.get(result.id) ?? 0) + 1);
1153
+ return acc;
1154
+ }, new Map()).entries()]
1155
+ .sort(([left], [right]) => left.localeCompare(right))
1156
+ .map(([ruleId, count]) => ({ ruleId, count }));
1157
+ return {
1158
+ family,
1159
+ count: familyResults.length,
1160
+ rules: ruleCounts,
1161
+ };
1162
+ })
1163
+ .filter((entry) => entry.count > 0);
1164
+ return {
1165
+ count: failedSmells.length,
1166
+ ruleFamilies: familyEntries,
1167
+ explanation: 'AP-APP/AP-DOM/AP-TXN findings are heuristic architecture review signals; they are not deterministic compile, security, or database failures.',
1168
+ };
1169
+ }
1170
+ function buildFindingCountContext(results) {
1171
+ const failedResults = results.filter((result) => result.applicability !== 'not-applicable' && !result.passed);
1172
+ const normalizedKeys = new Set();
1173
+ for (const result of failedResults) {
1174
+ normalizedKeys.add(classifyValidationResult(result).deductionKey);
1175
+ }
1176
+ return {
1177
+ rawFindingCount: failedResults.length,
1178
+ normalizedFindingCount: normalizedKeys.size,
1179
+ duplicateReductionCount: Math.max(0, failedResults.length - normalizedKeys.size),
1180
+ };
1181
+ }
1182
+ function buildScoreExplanationSummary(results, score) {
1183
+ const problemSummary = (0, architectureHealthScore_1.summarizeArchitectureScoreProblems)({
1184
+ results: results.map((result) => {
1185
+ const classification = classifyValidationResult(result);
1186
+ return {
1187
+ ruleId: result.id,
1188
+ category: getValidationRuleMetadataById(result.id).category,
1189
+ severity: result.severity,
1190
+ passed: result.passed,
1191
+ applicability: result.applicability,
1192
+ impact: classification.scoreImpact,
1193
+ deductionKey: classification.deductionKey,
1194
+ };
1195
+ }),
1196
+ });
1197
+ const categoryEntries = Object.entries(score.categoryScores)
1198
+ .sort((left, right) => left[1] - right[1] || left[0].localeCompare(right[0]))
1199
+ .slice(0, 3)
1200
+ .map(([category, categoryScore]) => ({ category, score: categoryScore }));
1201
+ const applicableCategoryScores = score.applicableCategories
1202
+ .map((category) => ({
1203
+ category,
1204
+ score: score.categoryScores[category] ?? 100,
1205
+ weight: architectureHealthScore_1.architectureHealthCategoryWeights[category] ?? 0,
1206
+ }))
1207
+ .sort((left, right) => left.category.localeCompare(right.category));
1208
+ const applicableResults = results.filter((result) => result.applicability !== 'not-applicable');
1209
+ const heuristicWarnings = applicableResults.filter((result) => !result.passed &&
1210
+ result.severity === 'warning' &&
1211
+ isHeuristicArchitectureSmellRuleId(result.id)).length;
1212
+ const dependencyWarnings = applicableResults.filter((result) => !result.passed && result.severity === 'warning' && result.id.startsWith('AP-DEP-')).length;
1213
+ const databaseWarnings = applicableResults.filter((result) => !result.passed && result.severity === 'warning' && getValidationRuleMetadataById(result.id).category === 'database').length;
1214
+ const documentationWarnings = applicableResults.filter((result) => !result.passed && result.severity === 'warning' && getValidationRuleMetadataById(result.id).category === 'documentation').length;
1215
+ const blockingDependencyErrors = problemSummary.clusters.filter((cluster) => cluster.severity === 'error' &&
1216
+ cluster.ruleIds.some((ruleId) => ruleId === 'AP-DEP-001' || ruleId === 'AP-DEP-008')).length;
1217
+ const whyScoreChanged = score.errors > 0
1218
+ ? 'Error-level findings carry the highest score impact, but repeated root-cause clusters are normalized before scoring.'
1219
+ : heuristicWarnings > 0 && dependencyWarnings > 0
1220
+ ? 'The score reflects normalized warning clusters: capped architecture-review signals plus remaining dependency governance warnings.'
1221
+ : heuristicWarnings > 0
1222
+ ? 'The score reflects capped cumulative impact from heuristic architecture-smell warning clusters.'
1223
+ : dependencyWarnings > 0
1224
+ ? 'The score reflects dependency governance warning clusters after repeated-surface normalization.'
1225
+ : score.warnings > 0
1226
+ ? 'The score reflects warning-level findings with volume normalization and diminishing returns for repeated problem families.'
1227
+ : 'No active findings reduced the score.';
1228
+ const largestScoreContributors = [];
1229
+ if (blockingDependencyErrors > 0) {
1230
+ largestScoreContributors.push(`Circular dependency / blocking dependency problems remain high-impact, but related findings are grouped into ${blockingDependencyErrors} blocking cluster${blockingDependencyErrors === 1 ? '' : 's'}.`);
1231
+ }
1232
+ if (databaseWarnings > 0) {
1233
+ largestScoreContributors.push(`Database/query risk volume is a major contributor (${databaseWarnings} warning${databaseWarnings === 1 ? '' : 's'} across ${problemSummary.clusters.filter((cluster) => cluster.category === 'database').length} score cluster${problemSummary.clusters.filter((cluster) => cluster.category === 'database').length === 1 ? '' : 's'}; database score ${score.categoryScores.database ?? 100}/100).`);
1234
+ }
1235
+ if (documentationWarnings > 0) {
1236
+ largestScoreContributors.push(`Documentation gaps contributed ${documentationWarnings} warning${documentationWarnings === 1 ? '' : 's'} to the documentation category.`);
1237
+ }
1238
+ if (heuristicWarnings > 0) {
1239
+ largestScoreContributors.push(`Heuristic AP-APP/AP-DOM/AP-TXN findings are capped and did not dominate the score (${heuristicWarnings} warning${heuristicWarnings === 1 ? '' : 's'} across ${problemSummary.heuristicClusterCount} heuristic cluster${problemSummary.heuristicClusterCount === 1 ? '' : 's'}).`);
1240
+ }
1241
+ if (score.warnings > 1) {
1242
+ largestScoreContributors.push(`Fixing one warning may not visibly move the rounded score; grouped fixes that clear repeated problem clusters (${problemSummary.qualityClusterCount} quality cluster${problemSummary.qualityClusterCount === 1 ? '' : 's'}) usually move it more.`);
1243
+ }
1244
+ return {
1245
+ errors: score.errors,
1246
+ warnings: score.warnings,
1247
+ heuristicWarnings,
1248
+ weightedBaseScore: score.weightedBaseScore,
1249
+ applicableCategoryScores,
1250
+ highestImpactCategories: categoryEntries,
1251
+ whyScoreChanged,
1252
+ largestScoreContributors,
1253
+ };
1254
+ }
1255
+ function toTimelineRiskLevelFromValidationSummary(summary) {
1256
+ if (summary.errors > 0 || summary.qualityFindings > 0) {
1257
+ return 'HIGH';
1258
+ }
1259
+ if (summary.warnings > 0 || summary.setupGaps > 0) {
1260
+ return 'MEDIUM';
1261
+ }
1262
+ return 'LOW';
1263
+ }
1264
+ function toTimelineEnforcementOutcomeFromValidationSummary(summary) {
1265
+ if (summary.errors > 0) {
1266
+ return 'FAIL';
1267
+ }
1268
+ if (summary.warnings > 0) {
1269
+ return 'WARN';
1270
+ }
1271
+ return 'PASS';
1272
+ }
1273
+ function buildSetupGapDeductionKeyFromMessage(message) {
1274
+ const normalized = message.toLowerCase();
1275
+ if (normalized.includes('.archpilot/dependency-rules.json is missing')) {
1276
+ return 'setup:missing-dependency-rules';
1277
+ }
1278
+ if (normalized.includes('.archpilot/layer-rules.json is missing')) {
1279
+ return 'setup:missing-layer-rules';
1280
+ }
1281
+ if (normalized.includes('contract for') && normalized.includes('is missing')) {
1282
+ const matches = [...normalized.matchAll(/contract for '([^']+)' is missing/gu)];
1283
+ if (matches.length > 0) {
1284
+ return `setup:missing-contract:${matches
1285
+ .map((match) => match[1])
1286
+ .sort((left, right) => left.localeCompare(right))
1287
+ .join('+')}`;
1288
+ }
1289
+ }
1290
+ if (normalized.includes('contract for') && normalized.includes('is invalid')) {
1291
+ const matches = [...normalized.matchAll(/contract for '([^']+)' is invalid/gu)];
1292
+ if (matches.length > 0) {
1293
+ return `setup:invalid-contract:${matches
1294
+ .map((match) => match[1])
1295
+ .sort((left, right) => left.localeCompare(right))
1296
+ .join('+')}`;
1297
+ }
1298
+ }
1299
+ return undefined;
1300
+ }
1301
+ function buildDependencyDeductionKey(result) {
1302
+ if (result.findingType !== 'module-dependency') {
1303
+ return undefined;
1304
+ }
1305
+ if (typeof result.sourceFile !== 'string' ||
1306
+ typeof result.sourceLine !== 'number' ||
1307
+ typeof result.module !== 'string' ||
1308
+ typeof result.target !== 'string') {
1309
+ return undefined;
1310
+ }
1311
+ return [
1312
+ 'quality',
1313
+ result.id,
1314
+ result.module,
1315
+ result.target,
1316
+ result.sourceFile.replace(/\\/g, '/'),
1317
+ String(result.sourceLine),
1318
+ result.importSpecifier ?? '',
1319
+ ].join('|');
1320
+ }
1321
+ function classifyValidationResult(result) {
1322
+ if (result.applicability === 'not-applicable') {
1323
+ return {
1324
+ kind: 'guidance',
1325
+ scoreImpact: 'guidance',
1326
+ deductionKey: `not-applicable:${result.id}`,
1327
+ };
1328
+ }
1329
+ if (result.passed) {
1330
+ return {
1331
+ kind: 'guidance',
1332
+ scoreImpact: 'guidance',
1333
+ deductionKey: `passed:${result.id}`,
1334
+ };
1335
+ }
1336
+ const normalizedMessage = result.message.toLowerCase();
1337
+ const setupLike = normalizedMessage.includes('validation skipped') ||
1338
+ normalizedMessage.includes('is missing. add it to enable') ||
1339
+ normalizedMessage.includes('run \'archpilot: initialize architecture\' to generate') ||
1340
+ normalizedMessage.includes('contract for') &&
1341
+ (normalizedMessage.includes('is missing') || normalizedMessage.includes('is invalid')) ||
1342
+ (result.id === exports.ValidationRuleIds.ARCH_MODULE_CONTRACT_FILE_EXISTS &&
1343
+ normalizedMessage.includes('is missing'));
1344
+ if (setupLike) {
1345
+ return {
1346
+ kind: 'setup-gap',
1347
+ scoreImpact: 'setup-gap',
1348
+ deductionKey: buildSetupGapDeductionKeyFromMessage(result.message) ??
1349
+ `setup:${result.id}:${normalizedMessage}`,
1350
+ };
1351
+ }
1352
+ if (result.severity === 'info') {
1353
+ return {
1354
+ kind: 'guidance',
1355
+ scoreImpact: 'guidance',
1356
+ deductionKey: `guidance:${result.id}:${normalizedMessage}`,
1357
+ };
1358
+ }
1359
+ return {
1360
+ kind: 'violation',
1361
+ scoreImpact: 'quality',
1362
+ deductionKey: result.deductionKey ??
1363
+ buildDependencyDeductionKey(result) ??
1364
+ `quality:${result.id}:${normalizedMessage}`,
1365
+ };
1366
+ }
1367
+ function getRuleIdSet() {
1368
+ return new Set(getAllValidationRuleMetadata().map((meta) => meta.id));
1369
+ }
1370
+ function isKnownValidationRuleId(value) {
1371
+ return getRuleIdSet().has(value);
1372
+ }
1373
+ function normalizeValidationConfig(config) {
1374
+ const knownRuleIds = getRuleIdSet();
1375
+ const rawDisabledRules = Array.isArray(config.disabledRules) ? config.disabledRules : [];
1376
+ const rawSeverityOverrides = config.severityOverrides &&
1377
+ typeof config.severityOverrides === 'object' &&
1378
+ !Array.isArray(config.severityOverrides)
1379
+ ? config.severityOverrides
1380
+ : {};
1381
+ const disabledRules = [...rawDisabledRules]
1382
+ .filter((id) => {
1383
+ if (typeof id !== 'string' || !knownRuleIds.has(id)) {
1384
+ return false;
1385
+ }
1386
+ return true;
1387
+ })
1388
+ .sort((a, b) => a.localeCompare(b));
1389
+ const severityOverrideEntries = Object.entries(rawSeverityOverrides)
1390
+ .filter(([id, severity]) => {
1391
+ if (!knownRuleIds.has(id)) {
1392
+ return false;
1393
+ }
1394
+ return severity === 'error' || severity === 'warning' || severity === 'info';
1395
+ })
1396
+ .sort(([left], [right]) => left.localeCompare(right));
1397
+ const normalizedOverrides = severityOverrideEntries.reduce((acc, [id, severity]) => {
1398
+ acc[id] = severity;
1399
+ return acc;
1400
+ }, {});
1401
+ const ignoredEntries = [];
1402
+ for (const rawId of rawDisabledRules) {
1403
+ if (typeof rawId !== 'string' || !isKnownValidationRuleId(rawId)) {
1404
+ ignoredEntries.push({
1405
+ kind: 'disabledRule',
1406
+ id: String(rawId),
1407
+ reason: 'Unknown rule ID. Entry was ignored.',
1408
+ });
1409
+ }
1410
+ }
1411
+ for (const [rawId, rawSeverity] of Object.entries(rawSeverityOverrides)) {
1412
+ if (!isKnownValidationRuleId(rawId)) {
1413
+ ignoredEntries.push({
1414
+ kind: 'severityOverride',
1415
+ id: rawId,
1416
+ reason: 'Unknown rule ID. Override was ignored.',
1417
+ });
1418
+ continue;
1419
+ }
1420
+ if (rawSeverity !== 'error' && rawSeverity !== 'warning' && rawSeverity !== 'info') {
1421
+ ignoredEntries.push({
1422
+ kind: 'severityOverride',
1423
+ id: rawId,
1424
+ reason: 'Invalid severity. Override was ignored.',
1425
+ });
1426
+ }
1427
+ }
1428
+ return {
1429
+ config: {
1430
+ ...(disabledRules.length > 0 ? { disabledRules } : {}),
1431
+ ...(Object.keys(normalizedOverrides).length > 0
1432
+ ? { severityOverrides: normalizedOverrides }
1433
+ : {}),
1434
+ },
1435
+ ignoredEntries: ignoredEntries.sort((a, b) => {
1436
+ const kindCompare = a.kind.localeCompare(b.kind);
1437
+ if (kindCompare !== 0) {
1438
+ return kindCompare;
1439
+ }
1440
+ return a.id.localeCompare(b.id);
1441
+ }),
1442
+ };
1443
+ }
1444
+ function buildValidationStatusSummary(results, disabledCount = 0, suppressedCount = 0, exceptionsCount = 0, ignoredSuppressionCount = 0) {
1445
+ const score = (0, architectureHealthScore_1.calculateArchitectureHealthScore)({
1446
+ results: results.map((result) => {
1447
+ const classification = classifyValidationResult(result);
1448
+ return {
1449
+ ruleId: result.id,
1450
+ category: getValidationRuleMetadataById(result.id).category,
1451
+ severity: result.severity,
1452
+ passed: result.passed,
1453
+ applicability: result.applicability,
1454
+ ...(result.sourceComponentId ? { componentId: result.sourceComponentId } : {}),
1455
+ ...(result.module ? { moduleId: result.module } : {}),
1456
+ impact: classification.scoreImpact,
1457
+ deductionKey: classification.deductionKey,
1458
+ };
1459
+ }),
1460
+ categories: architectureHealthScore_1.defaultArchitectureHealthCategories,
1461
+ });
1462
+ const totalRulesEvaluated = score.totalChecks;
1463
+ const info = score.info;
1464
+ const failed = score.errors + score.warnings + score.info;
1465
+ const passed = Math.max(0, score.passed - exceptionsCount);
1466
+ const findingCountContext = buildFindingCountContext(results);
1467
+ const architectureSmellSummary = buildArchitectureSmellSummaryFromResults(results);
1468
+ const scoreExplanation = buildScoreExplanationSummary(results, score);
1469
+ const base = {
1470
+ totalRulesEvaluated,
1471
+ notApplicable: score.notApplicable,
1472
+ disabled: disabledCount,
1473
+ passed,
1474
+ failed,
1475
+ ...findingCountContext,
1476
+ heuristicArchitectureSmellCount: architectureSmellSummary.count,
1477
+ architectureSmellSummary,
1478
+ scoreExplanation,
1479
+ suppressed: suppressedCount,
1480
+ exceptions: exceptionsCount,
1481
+ ignoredSuppressions: ignoredSuppressionCount,
1482
+ errors: score.errors,
1483
+ warnings: score.warnings,
1484
+ info,
1485
+ healthScore: score.overallScore,
1486
+ categoryScores: score.categoryScores,
1487
+ ...(score.componentScores ? { componentScores: score.componentScores } : {}),
1488
+ ...(score.moduleScores ? { moduleScores: score.moduleScores } : {}),
1489
+ readinessScore: score.readinessScore,
1490
+ readinessCategoryScores: score.readinessCategoryScores,
1491
+ setupGaps: score.setupGaps,
1492
+ guidanceFindings: score.guidanceFindings,
1493
+ qualityFindings: score.qualityFindings,
1494
+ };
1495
+ const status = getValidationOverallStatus(base);
1496
+ return {
1497
+ ...base,
1498
+ status,
1499
+ hasErrors: score.errors > 0,
1500
+ hasWarnings: score.warnings > 0,
1501
+ };
1502
+ }
1503
+ function mergeValidationConfig(base, override, explicitlyEnabledRuleIds = []) {
1504
+ const disabledSet = new Set(base.disabledRules ?? []);
1505
+ for (const ruleId of explicitlyEnabledRuleIds) {
1506
+ disabledSet.delete(ruleId);
1507
+ }
1508
+ for (const ruleId of override.disabledRules ?? []) {
1509
+ disabledSet.add(ruleId);
1510
+ }
1511
+ const mergedOverrides = {
1512
+ ...(base.severityOverrides ?? {}),
1513
+ ...(override.severityOverrides ?? {}),
1514
+ };
1515
+ return {
1516
+ ...(disabledSet.size > 0
1517
+ ? { disabledRules: [...disabledSet].sort((a, b) => a.localeCompare(b)) }
1518
+ : {}),
1519
+ ...(Object.keys(mergedOverrides).length > 0
1520
+ ? { severityOverrides: mergedOverrides }
1521
+ : {}),
1522
+ };
1523
+ }
1524
+ function parseRulesConfigToValidationConfig(config) {
1525
+ const rawRules = config.rules && typeof config.rules === 'object' && !Array.isArray(config.rules)
1526
+ ? config.rules
1527
+ : {};
1528
+ const disabledRules = [];
1529
+ const explicitlyEnabledRuleIds = [];
1530
+ const severityOverrides = {};
1531
+ for (const [ruleId, value] of Object.entries(rawRules)) {
1532
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1533
+ continue;
1534
+ }
1535
+ if (value.enabled === false) {
1536
+ disabledRules.push(ruleId);
1537
+ }
1538
+ else if (value.enabled === true) {
1539
+ explicitlyEnabledRuleIds.push(ruleId);
1540
+ }
1541
+ if (value.severity !== undefined) {
1542
+ severityOverrides[ruleId] = value.severity;
1543
+ }
1544
+ }
1545
+ return {
1546
+ explicitlyEnabledRuleIds,
1547
+ config: {
1548
+ ...(disabledRules.length > 0
1549
+ ? { disabledRules: disabledRules }
1550
+ : {}),
1551
+ ...(Object.keys(severityOverrides).length > 0
1552
+ ? {
1553
+ severityOverrides: severityOverrides,
1554
+ }
1555
+ : {}),
1556
+ },
1557
+ };
1558
+ }
1559
+ async function readLegacyValidationConfig(workspaceRoot) {
1560
+ const configPath = await resolveValidationConfigPath(workspaceRoot);
1561
+ if (!configPath) {
1562
+ return {};
1563
+ }
1564
+ const contents = await readTextFileIfExists(configPath);
1565
+ if (!contents) {
1566
+ return {};
1567
+ }
1568
+ try {
1569
+ const parsed = JSON.parse(stripJsonLineComments(contents));
1570
+ const normalized = normalizeValidationConfig(parsed);
1571
+ return normalized.config;
1572
+ }
1573
+ catch {
1574
+ return {};
1575
+ }
1576
+ }
1577
+ async function readValidationConfig(workspaceRoot) {
1578
+ const legacyConfig = await readLegacyValidationConfig(workspaceRoot);
1579
+ const rulesConfigPath = path.join(workspaceRoot, '.archpilot', 'rules.json');
1580
+ const rulesConfigContents = await readTextFileIfExists(rulesConfigPath);
1581
+ if (!rulesConfigContents) {
1582
+ return { config: legacyConfig, configResults: [] };
1583
+ }
1584
+ try {
1585
+ const parsed = JSON.parse(rulesConfigContents);
1586
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1587
+ return {
1588
+ config: legacyConfig,
1589
+ configResults: [
1590
+ {
1591
+ id: exports.ValidationRuleIds.ARCH_RULE_CONFIGURATION_VALID,
1592
+ severity: 'error',
1593
+ passed: false,
1594
+ message: 'ArchPilot rule configuration is invalid: .archpilot/rules.json must be a JSON object.',
1595
+ },
1596
+ ],
1597
+ };
1598
+ }
1599
+ const rulesConfig = parseRulesConfigToValidationConfig(parsed);
1600
+ const merged = mergeValidationConfig(legacyConfig, rulesConfig.config, rulesConfig.explicitlyEnabledRuleIds);
1601
+ return { config: merged, configResults: [] };
1602
+ }
1603
+ catch {
1604
+ return {
1605
+ config: legacyConfig,
1606
+ configResults: [
1607
+ {
1608
+ id: exports.ValidationRuleIds.ARCH_RULE_CONFIGURATION_VALID,
1609
+ severity: 'error',
1610
+ passed: false,
1611
+ message: 'ArchPilot rule configuration is invalid JSON. Please fix .archpilot/rules.json.',
1612
+ },
1613
+ ],
1614
+ };
1615
+ }
1616
+ }
1617
+ function canOverrideSeverity(ruleId, from, to) {
1618
+ void ruleId;
1619
+ void from;
1620
+ void to;
1621
+ return true;
1622
+ }
1623
+ function resolveEffectiveSeverity(ruleId, config, baseSeverity) {
1624
+ const meta = getValidationRuleMetadataById(ruleId);
1625
+ const effectiveBaseSeverity = baseSeverity ?? meta.defaultSeverity;
1626
+ const requested = config.severityOverrides?.[ruleId];
1627
+ if (!requested) {
1628
+ return effectiveBaseSeverity;
1629
+ }
1630
+ return canOverrideSeverity(ruleId, effectiveBaseSeverity, requested)
1631
+ ? requested
1632
+ : effectiveBaseSeverity;
1633
+ }
1634
+ function buildEffectiveValidationConfig(config) {
1635
+ const normalized = normalizeValidationConfig(config);
1636
+ const disabledRuleIds = [...(normalized.config.disabledRules ?? [])].sort((a, b) => a.localeCompare(b));
1637
+ const ignoredEntries = [...normalized.ignoredEntries];
1638
+ const appliedSeverityOverrides = [];
1639
+ for (const [id, requestedSeverity] of Object.entries(normalized.config.severityOverrides ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
1640
+ const ruleId = id;
1641
+ const from = getValidationRuleMetadataById(ruleId).defaultSeverity;
1642
+ if (requestedSeverity !== from) {
1643
+ appliedSeverityOverrides.push({
1644
+ id: ruleId,
1645
+ from,
1646
+ to: requestedSeverity,
1647
+ });
1648
+ }
1649
+ }
1650
+ const effectiveOverrides = appliedSeverityOverrides.reduce((acc, override) => {
1651
+ acc[override.id] = override.to;
1652
+ return acc;
1653
+ }, {});
1654
+ return {
1655
+ config: {
1656
+ ...(disabledRuleIds.length > 0 ? { disabledRules: disabledRuleIds } : {}),
1657
+ ...(Object.keys(effectiveOverrides).length > 0
1658
+ ? { severityOverrides: effectiveOverrides }
1659
+ : {}),
1660
+ },
1661
+ disabledRuleIds,
1662
+ disabledRuleIdSet: new Set(disabledRuleIds),
1663
+ appliedSeverityOverrides,
1664
+ ignoredEntries: ignoredEntries.sort((a, b) => {
1665
+ const kindCompare = a.kind.localeCompare(b.kind);
1666
+ if (kindCompare !== 0) {
1667
+ return kindCompare;
1668
+ }
1669
+ return a.id.localeCompare(b.id);
1670
+ }),
1671
+ };
1672
+ }
1673
+ function isRuleDisabled(ruleId, config) {
1674
+ return config.disabledRuleIdSet.has(ruleId);
1675
+ }
1676
+ function applyEffectiveSeverities(results, config) {
1677
+ return results.map((result) => ({
1678
+ ...result,
1679
+ severity: resolveEffectiveSeverity(result.id, config, result.severity),
1680
+ }));
1681
+ }
1682
+ function buildDisabledRuleDetails(disabledRuleIds) {
1683
+ return disabledRuleIds.map((id) => {
1684
+ const meta = getValidationRuleMetadataById(id);
1685
+ return {
1686
+ id,
1687
+ title: meta.title,
1688
+ category: meta.category,
1689
+ };
1690
+ });
1691
+ }
1692
+ function getEffectiveValidationConfigSummary(config) {
1693
+ const effective = buildEffectiveValidationConfig(config);
1694
+ const disabledCount = effective.disabledRuleIds.length;
1695
+ const overrideCount = effective.appliedSeverityOverrides.length;
1696
+ const ignoredCount = effective.ignoredEntries.length;
1697
+ return `disabled=${disabledCount}, severityOverrides=${overrideCount}, ignoredConfigEntries=${ignoredCount}`;
1698
+ }
1699
+ function sortResultsForExport(results) {
1700
+ return [...results].sort((a, b) => {
1701
+ const bySeverity = getSeveritySortOrder(a.severity) - getSeveritySortOrder(b.severity);
1702
+ if (bySeverity !== 0) {
1703
+ return bySeverity;
1704
+ }
1705
+ return a.id.localeCompare(b.id);
1706
+ });
1707
+ }
1708
+ function sortSuppressedResults(results) {
1709
+ return [...results].sort((left, right) => {
1710
+ const fileCompare = left.filePath.localeCompare(right.filePath);
1711
+ if (fileCompare !== 0) {
1712
+ return fileCompare;
1713
+ }
1714
+ const lineCompare = (left.line ?? 0) - (right.line ?? 0);
1715
+ if (lineCompare !== 0) {
1716
+ return lineCompare;
1717
+ }
1718
+ return left.id.localeCompare(right.id);
1719
+ });
1720
+ }
1721
+ function sortSuppressionEntries(entries) {
1722
+ return [...entries].sort((left, right) => {
1723
+ const ruleCompare = left.entry.ruleId.localeCompare(right.entry.ruleId);
1724
+ if (ruleCompare !== 0) {
1725
+ return ruleCompare;
1726
+ }
1727
+ const scopeCompare = (left.entry.scope ?? '').localeCompare(right.entry.scope ?? '');
1728
+ if (scopeCompare !== 0) {
1729
+ return scopeCompare;
1730
+ }
1731
+ const reasonCompare = (left.entry.reason ?? '').localeCompare(right.entry.reason ?? '');
1732
+ if (reasonCompare !== 0) {
1733
+ return reasonCompare;
1734
+ }
1735
+ const expiryCompare = (left.entry.expiresOn ?? '').localeCompare(right.entry.expiresOn ?? '');
1736
+ if (expiryCompare !== 0) {
1737
+ return expiryCompare;
1738
+ }
1739
+ return left.entry.index - right.entry.index;
1740
+ });
1741
+ }
1742
+ function buildDriftSnapshot(generatedAtUtc, status) {
1743
+ return {
1744
+ schemaVersion: '1.0',
1745
+ generatedAtUtc,
1746
+ score: status.summary.healthScore,
1747
+ errors: status.summary.errors,
1748
+ warnings: status.summary.warnings,
1749
+ info: status.summary.info,
1750
+ passed: status.summary.passed,
1751
+ violations: status.results
1752
+ .filter((result) => !result.passed)
1753
+ .map((result) => ({
1754
+ ruleId: result.id,
1755
+ severity: result.severity,
1756
+ ...(result.sourceFile ? { file: result.sourceFile } : {}),
1757
+ ...(result.message ? { message: result.message } : {}),
1758
+ }))
1759
+ .sort((left, right) => {
1760
+ const ruleCompare = left.ruleId.localeCompare(right.ruleId);
1761
+ if (ruleCompare !== 0) {
1762
+ return ruleCompare;
1763
+ }
1764
+ const fileCompare = (left.file ?? '').localeCompare(right.file ?? '');
1765
+ if (fileCompare !== 0) {
1766
+ return fileCompare;
1767
+ }
1768
+ const severityCompare = left.severity.localeCompare(right.severity);
1769
+ if (severityCompare !== 0) {
1770
+ return severityCompare;
1771
+ }
1772
+ return (left.message ?? '').localeCompare(right.message ?? '');
1773
+ }),
1774
+ };
1775
+ }
1776
+ function sortAdrSummary(summary) {
1777
+ return {
1778
+ total: summary.total,
1779
+ proposed: summary.proposed,
1780
+ accepted: summary.accepted,
1781
+ deprecated: summary.deprecated,
1782
+ superseded: summary.superseded,
1783
+ };
1784
+ }
1785
+ function buildValidationStatusExport(config, results, validationConfig, suppressedResults, approvedExceptions, ignoredSuppressions, suppressionEntriesFound, exceptionEntriesFound, driftSummary, dependencyGraphSummary, suppressionSummary, suppressionEntries, policyStatus, adrSummary) {
1786
+ const sorted = sortResultsForExport(results);
1787
+ const sortedSuppressedResults = sortSuppressedResults(suppressedResults);
1788
+ const summary = buildValidationStatusSummary(sorted, validationConfig.disabledRuleIds.length, sortedSuppressedResults.length, approvedExceptions.length, ignoredSuppressions.length);
1789
+ const disabledRules = buildDisabledRuleDetails(validationConfig.disabledRuleIds);
1790
+ const exportResults = sorted.map((r) => {
1791
+ const meta = getValidationRuleMetadataById(r.id);
1792
+ const classification = classifyValidationResult(r);
1793
+ return {
1794
+ id: r.id,
1795
+ title: meta.title,
1796
+ category: meta.category,
1797
+ severity: r.severity,
1798
+ passed: r.passed,
1799
+ ...(r.applicability ? { applicability: r.applicability } : {}),
1800
+ ...(r.applicabilityReason ? { applicabilityReason: r.applicabilityReason } : {}),
1801
+ message: r.message,
1802
+ kind: classification.kind,
1803
+ ...(r.findingType ? { findingType: r.findingType } : {}),
1804
+ ...(r.module ? { module: r.module } : {}),
1805
+ ...(r.target ? { target: r.target } : {}),
1806
+ ...(r.filePath ? { filePath: r.filePath } : {}),
1807
+ ...(r.table ? { table: r.table } : {}),
1808
+ ...(r.api ? { api: r.api } : {}),
1809
+ ...(r.quickFix ? { quickFix: r.quickFix } : {}),
1810
+ ...(r.status ? { status: r.status } : {}),
1811
+ ...(r.exceptionReason ? { exceptionReason: r.exceptionReason } : {}),
1812
+ ...(r.exceptionApprovedBy
1813
+ ? { exceptionApprovedBy: r.exceptionApprovedBy }
1814
+ : {}),
1815
+ ...(r.exceptionExpiresAt ? { exceptionExpiresAt: r.exceptionExpiresAt } : {}),
1816
+ ...(r.exceptionModule ? { exceptionModule: r.exceptionModule } : {}),
1817
+ ...(r.exceptionTarget ? { exceptionTarget: r.exceptionTarget } : {}),
1818
+ ...(r.sourceFile ? { sourceFile: r.sourceFile } : {}),
1819
+ ...(r.sourceLine !== undefined ? { sourceLine: r.sourceLine } : {}),
1820
+ ...(r.sourceColumn !== undefined ? { sourceColumn: r.sourceColumn } : {}),
1821
+ ...(r.dependencyType ? { dependencyType: r.dependencyType } : {}),
1822
+ ...(r.sourceComponentId ? { sourceComponentId: r.sourceComponentId } : {}),
1823
+ ...(r.sourceComponentName ? { sourceComponentName: r.sourceComponentName } : {}),
1824
+ ...(r.targetComponentId !== undefined ? { targetComponentId: r.targetComponentId } : {}),
1825
+ ...(r.targetComponentName !== undefined ? { targetComponentName: r.targetComponentName } : {}),
1826
+ ...(r.resolvedTargetPath !== undefined ? { resolvedTargetPath: r.resolvedTargetPath } : {}),
1827
+ ...(r.isExternal !== undefined ? { isExternal: r.isExternal } : {}),
1828
+ };
1829
+ });
1830
+ return {
1831
+ tool: 'ArchPilot',
1832
+ version: 1,
1833
+ architectureStyle: config.architectureStyle,
1834
+ implementationProfile: config.implementationProfile,
1835
+ suppressionsFile: (0, validationSuppressions_1.getSuppressionsFilePathLabel)(),
1836
+ suppressionEntriesFound,
1837
+ exceptionsFile: '.archpilot/exceptions.json',
1838
+ exceptionEntriesFound,
1839
+ drift: driftSummary,
1840
+ dependencyGraphSummary,
1841
+ config: validationConfig.config,
1842
+ summary,
1843
+ disabledRules,
1844
+ appliedSeverityOverrides: validationConfig.appliedSeverityOverrides,
1845
+ ignoredConfigEntries: validationConfig.ignoredEntries,
1846
+ suppressedResults: sortedSuppressedResults,
1847
+ approvedExceptions: [...approvedExceptions].sort((left, right) => {
1848
+ const ruleCompare = left.ruleId.localeCompare(right.ruleId);
1849
+ if (ruleCompare !== 0) {
1850
+ return ruleCompare;
1851
+ }
1852
+ const moduleCompare = (left.module ?? '').localeCompare(right.module ?? '');
1853
+ if (moduleCompare !== 0) {
1854
+ return moduleCompare;
1855
+ }
1856
+ const targetCompare = (left.target ?? '').localeCompare(right.target ?? '');
1857
+ if (targetCompare !== 0) {
1858
+ return targetCompare;
1859
+ }
1860
+ const filePathCompare = (left.filePath ?? '').localeCompare(right.filePath ?? '');
1861
+ if (filePathCompare !== 0) {
1862
+ return filePathCompare;
1863
+ }
1864
+ const tableCompare = (left.table ?? '').localeCompare(right.table ?? '');
1865
+ if (tableCompare !== 0) {
1866
+ return tableCompare;
1867
+ }
1868
+ const apiCompare = (left.api ?? '').localeCompare(right.api ?? '');
1869
+ if (apiCompare !== 0) {
1870
+ return apiCompare;
1871
+ }
1872
+ return left.reason.localeCompare(right.reason);
1873
+ }),
1874
+ ignoredSuppressions,
1875
+ results: exportResults,
1876
+ ...(suppressionSummary ? { suppressionSummary } : {}),
1877
+ ...(suppressionEntries ? { suppressionEntries: sortSuppressionEntries(suppressionEntries) } : {}),
1878
+ ...(policyStatus ? { policy: policyStatus } : {}),
1879
+ ...(adrSummary ? { adrSummary: sortAdrSummary(adrSummary) } : {}),
1880
+ };
1881
+ }
1882
+ function normalizeRelativePathForSuppression(filePath) {
1883
+ let normalized = filePath.replace(/\\/g, '/').trim();
1884
+ while (normalized.startsWith('./')) {
1885
+ normalized = normalized.slice(2);
1886
+ }
1887
+ return normalized;
1888
+ }
1889
+ function resolveFindingPathForSuppression(result) {
1890
+ if (typeof result.sourceFile === 'string' && result.sourceFile.length > 0) {
1891
+ return normalizeRelativePathForSuppression(result.sourceFile);
1892
+ }
1893
+ const resolvedTargetPath = (0, findingTargetResolution_1.resolveNavigationTargetPath)({
1894
+ ruleId: result.id,
1895
+ message: result.message,
1896
+ });
1897
+ if (!resolvedTargetPath) {
1898
+ return undefined;
1899
+ }
1900
+ return normalizeRelativePathForSuppression(resolvedTargetPath);
1901
+ }
1902
+ function applyValidationSuppressions(results, suppressions) {
1903
+ const activeResults = [];
1904
+ const suppressedResults = [];
1905
+ for (const result of results) {
1906
+ if (result.passed) {
1907
+ activeResults.push(result);
1908
+ continue;
1909
+ }
1910
+ const findingPath = resolveFindingPathForSuppression(result);
1911
+ const suppression = (0, validationSuppressions_1.findMatchingSuppression)(result.id, findingPath, suppressions);
1912
+ if (!suppression) {
1913
+ activeResults.push(result);
1914
+ continue;
1915
+ }
1916
+ const meta = getValidationRuleMetadataById(result.id);
1917
+ suppressedResults.push({
1918
+ id: result.id,
1919
+ title: meta.title,
1920
+ category: meta.category,
1921
+ severity: result.severity,
1922
+ filePath: findingPath ?? (0, validationSuppressions_1.getSuppressionScopeLabel)(suppression),
1923
+ ...(result.sourceLine !== undefined ? { line: result.sourceLine } : {}),
1924
+ suppressionPath: (0, validationSuppressions_1.getSuppressionScopeLabel)(suppression),
1925
+ ...(suppression.reason ? { suppressionReason: suppression.reason } : {}),
1926
+ message: result.message,
1927
+ });
1928
+ }
1929
+ return { activeResults, suppressedResults };
1930
+ }
1931
+ function applyValidationExceptions(results, exceptions) {
1932
+ const activeResults = [];
1933
+ const approvedExceptions = [];
1934
+ for (const result of results) {
1935
+ const matchedException = (0, exceptionLoader_1.shouldIgnoreViolationDueToException)(result, exceptions);
1936
+ if (!matchedException) {
1937
+ activeResults.push(result);
1938
+ continue;
1939
+ }
1940
+ activeResults.push({
1941
+ ...result,
1942
+ passed: true,
1943
+ status: 'exception',
1944
+ exceptionReason: matchedException.reason,
1945
+ exceptionApprovedBy: matchedException.approvedBy,
1946
+ ...(matchedException.expiresAt
1947
+ ? { exceptionExpiresAt: matchedException.expiresAt }
1948
+ : {}),
1949
+ ...(matchedException.module ? { exceptionModule: matchedException.module } : {}),
1950
+ ...(matchedException.target ? { exceptionTarget: matchedException.target } : {}),
1951
+ });
1952
+ approvedExceptions.push({
1953
+ ruleId: matchedException.ruleId,
1954
+ ...(matchedException.module ? { module: matchedException.module } : {}),
1955
+ ...(matchedException.target ? { target: matchedException.target } : {}),
1956
+ ...(matchedException.filePath ? { filePath: matchedException.filePath } : {}),
1957
+ ...(matchedException.table ? { table: matchedException.table } : {}),
1958
+ ...(matchedException.api ? { api: matchedException.api } : {}),
1959
+ reason: matchedException.reason,
1960
+ approvedBy: matchedException.approvedBy,
1961
+ createdAt: matchedException.createdAt,
1962
+ ...(matchedException.expiresAt ? { expiresAt: matchedException.expiresAt } : {}),
1963
+ });
1964
+ }
1965
+ const dedupedApprovedExceptions = [...approvedExceptions]
1966
+ .sort((left, right) => {
1967
+ const ruleCompare = left.ruleId.localeCompare(right.ruleId);
1968
+ if (ruleCompare !== 0) {
1969
+ return ruleCompare;
1970
+ }
1971
+ const moduleCompare = (left.module ?? '').localeCompare(right.module ?? '');
1972
+ if (moduleCompare !== 0) {
1973
+ return moduleCompare;
1974
+ }
1975
+ const targetCompare = (left.target ?? '').localeCompare(right.target ?? '');
1976
+ if (targetCompare !== 0) {
1977
+ return targetCompare;
1978
+ }
1979
+ const filePathCompare = (left.filePath ?? '').localeCompare(right.filePath ?? '');
1980
+ if (filePathCompare !== 0) {
1981
+ return filePathCompare;
1982
+ }
1983
+ const tableCompare = (left.table ?? '').localeCompare(right.table ?? '');
1984
+ if (tableCompare !== 0) {
1985
+ return tableCompare;
1986
+ }
1987
+ const apiCompare = (left.api ?? '').localeCompare(right.api ?? '');
1988
+ if (apiCompare !== 0) {
1989
+ return apiCompare;
1990
+ }
1991
+ return left.reason.localeCompare(right.reason);
1992
+ })
1993
+ .filter((entry, index, values) => {
1994
+ if (index === 0) {
1995
+ return true;
1996
+ }
1997
+ const previous = values[index - 1];
1998
+ return !(entry.ruleId === previous.ruleId &&
1999
+ (entry.module ?? '') === (previous.module ?? '') &&
2000
+ (entry.target ?? '') === (previous.target ?? '') &&
2001
+ (entry.filePath ?? '') === (previous.filePath ?? '') &&
2002
+ (entry.table ?? '') === (previous.table ?? '') &&
2003
+ (entry.api ?? '') === (previous.api ?? '') &&
2004
+ entry.reason === previous.reason);
2005
+ });
2006
+ return {
2007
+ activeResults,
2008
+ approvedExceptions: dedupedApprovedExceptions,
2009
+ };
2010
+ }
2011
+ async function pathExists(targetPath) {
2012
+ try {
2013
+ await node_fs_1.promises.access(targetPath);
2014
+ return true;
2015
+ }
2016
+ catch {
2017
+ return false;
2018
+ }
2019
+ }
2020
+ async function readTextFileIfExists(filePath) {
2021
+ try {
2022
+ const buf = await node_fs_1.promises.readFile(filePath, { encoding: 'utf8' });
2023
+ return buf.toString();
2024
+ }
2025
+ catch {
2026
+ return undefined;
2027
+ }
2028
+ }
2029
+ async function readDependencyRulesRawConfig(workspaceRoot) {
2030
+ const dependencyRulesPath = path.join(workspaceRoot, '.archpilot', 'dependency-rules.json');
2031
+ const contents = await readTextFileIfExists(dependencyRulesPath);
2032
+ if (!contents) {
2033
+ return undefined;
2034
+ }
2035
+ try {
2036
+ const parsed = JSON.parse(contents);
2037
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2038
+ return undefined;
2039
+ }
2040
+ return parsed;
2041
+ }
2042
+ catch {
2043
+ return undefined;
2044
+ }
2045
+ }
2046
+ async function resolveValidationConfigPath(workspaceRoot) {
2047
+ const configDir = path.join(workspaceRoot, '.archpilot');
2048
+ const jsoncPath = path.join(configDir, 'validation-config.jsonc');
2049
+ if (await pathExists(jsoncPath)) {
2050
+ return jsoncPath;
2051
+ }
2052
+ // Legacy fallback for older workspaces that still use strict .json.
2053
+ const jsonPath = path.join(configDir, 'validation-config.json');
2054
+ if (await pathExists(jsonPath)) {
2055
+ return jsonPath;
2056
+ }
2057
+ return undefined;
2058
+ }
2059
+ function stripJsonLineComments(contents) {
2060
+ let result = '';
2061
+ let inString = false;
2062
+ let isEscaped = false;
2063
+ for (let index = 0; index < contents.length; index += 1) {
2064
+ const current = contents[index];
2065
+ const next = contents[index + 1];
2066
+ if (inString) {
2067
+ result += current;
2068
+ if (isEscaped) {
2069
+ isEscaped = false;
2070
+ continue;
2071
+ }
2072
+ if (current === '\\') {
2073
+ isEscaped = true;
2074
+ continue;
2075
+ }
2076
+ if (current === '"') {
2077
+ inString = false;
2078
+ }
2079
+ continue;
2080
+ }
2081
+ if (current === '"') {
2082
+ inString = true;
2083
+ result += current;
2084
+ continue;
2085
+ }
2086
+ if (current === '/' && next === '/') {
2087
+ while (index < contents.length && contents[index] !== '\n') {
2088
+ index += 1;
2089
+ }
2090
+ if (index < contents.length) {
2091
+ result += contents[index];
2092
+ }
2093
+ continue;
2094
+ }
2095
+ result += current;
2096
+ }
2097
+ return result;
2098
+ }
2099
+ function hasConfiguredApi(config) {
2100
+ return config.apiStyle !== 'none';
2101
+ }
2102
+ function hasConfiguredDatabase(config) {
2103
+ return config.database !== 'none';
2104
+ }
2105
+ function isFlatModulesRoot(modulesRoot) {
2106
+ return !modulesRoot.endsWith('/modules') && !modulesRoot.endsWith('/domain');
2107
+ }
2108
+ async function getConfiguredModulesRoot(workspaceRoot, contract) {
2109
+ const fallbackModulesRoot = (0, moduleDiscovery_1.resolveModulesRootFromContract)(contract);
2110
+ return (0, moduleDiscovery_1.resolveConfiguredModulesRoot)(workspaceRoot, fallbackModulesRoot);
2111
+ }
2112
+ function normalizeRelativePath(value) {
2113
+ return value.replace(/\\/g, '/').replace(/^\/+/, '');
2114
+ }
2115
+ function resolveOverviewRelativePath(contract) {
2116
+ if (typeof contract.artifacts?.architectureOverview === 'string' &&
2117
+ contract.artifacts.architectureOverview.trim().length > 0) {
2118
+ return normalizeRelativePath(contract.artifacts.architectureOverview);
2119
+ }
2120
+ return `${(0, moduleDiscovery_1.resolveDocsRootFromContract)(contract)}/architecture/overview.md`;
2121
+ }
2122
+ function resolveAdrDirectoryRelativePath(contract) {
2123
+ if (typeof contract.artifacts?.adrDirectory === 'string' &&
2124
+ contract.artifacts.adrDirectory.trim().length > 0) {
2125
+ return normalizeRelativePath(contract.artifacts.adrDirectory);
2126
+ }
2127
+ return `${(0, moduleDiscovery_1.resolveDocsRootFromContract)(contract)}/adrs`;
2128
+ }
2129
+ function resolveTenantModelAdrRelativePath(contract) {
2130
+ return `${resolveAdrDirectoryRelativePath(contract)}/adr-001-tenant-model.md`;
2131
+ }
2132
+ function resolveOpenApiRelativePath(contract) {
2133
+ if (typeof contract.artifacts?.openApiSpec === 'string' &&
2134
+ contract.artifacts.openApiSpec.trim().length > 0) {
2135
+ return normalizeRelativePath(contract.artifacts.openApiSpec);
2136
+ }
2137
+ return `${(0, moduleDiscovery_1.resolveApiRootFromContract)(contract)}/openapi.yaml`;
2138
+ }
2139
+ function resolveSqlBaselineRelativePath(contract) {
2140
+ if (typeof contract.artifacts?.sqlBaseline === 'string' &&
2141
+ contract.artifacts.sqlBaseline.trim().length > 0) {
2142
+ return normalizeRelativePath(contract.artifacts.sqlBaseline);
2143
+ }
2144
+ return `${(0, moduleDiscovery_1.resolveDatabaseRootFromContract)(contract)}/001_init.sql`;
2145
+ }
2146
+ async function readArchitectureConfig(workspaceRoot) {
2147
+ try {
2148
+ const contract = await (0, architectureInit_1.loadArchitectureContract)(workspaceRoot);
2149
+ if (!contract) {
2150
+ return undefined;
2151
+ }
2152
+ const config = {
2153
+ version: contract.version,
2154
+ projectName: contract.projectName,
2155
+ repoTopology: contract.repoTopology,
2156
+ projectKinds: contract.projectKinds,
2157
+ stacks: contract.stacks,
2158
+ architectureStyle: contract.architectureStyle,
2159
+ database: contract.database,
2160
+ tenantModel: contract.tenantModel,
2161
+ rbac: contract.rbac,
2162
+ apiStyle: contract.apiStyle,
2163
+ authStyle: contract.authStyle,
2164
+ implementationProfile: contract.implementationProfile,
2165
+ createdAtUtc: contract.createdAtUtc,
2166
+ };
2167
+ return config;
2168
+ }
2169
+ catch {
2170
+ return undefined;
2171
+ }
2172
+ }
2173
+ async function validateApiStyle(workspaceRoot, contract, config, validationConfig) {
2174
+ const results = [];
2175
+ if (!hasConfiguredApi(config)) {
2176
+ return results;
2177
+ }
2178
+ const openApiRequired = !isRuleDisabled(exports.ValidationRuleIds.API_OPENAPI_EXISTS, validationConfig);
2179
+ const graphqlRequired = config.apiStyle === 'graphql' &&
2180
+ !isRuleDisabled(exports.ValidationRuleIds.API_GRAPHQL_PATH_PRESENT, validationConfig);
2181
+ if (!openApiRequired && !graphqlRequired) {
2182
+ return results;
2183
+ }
2184
+ const openApiRelativePath = resolveOpenApiRelativePath(contract);
2185
+ const openApiPath = path.join(workspaceRoot, ...openApiRelativePath.split('/'));
2186
+ const exists = await pathExists(openApiPath);
2187
+ if (openApiRequired) {
2188
+ results.push({
2189
+ id: exports.ValidationRuleIds.API_OPENAPI_EXISTS,
2190
+ severity: 'error',
2191
+ passed: exists,
2192
+ message: exists
2193
+ ? `${openApiRelativePath} exists.`
2194
+ : `${openApiRelativePath} is missing but required by apiStyle.`,
2195
+ sourceFile: openApiRelativePath,
2196
+ filePath: openApiRelativePath,
2197
+ findingType: 'file-scope',
2198
+ api: 'openapi',
2199
+ });
2200
+ }
2201
+ if (graphqlRequired) {
2202
+ const text = exists ? await readTextFileIfExists(openApiPath) : undefined;
2203
+ const hasGraphqlPath = !!text && text.includes('/graphql');
2204
+ results.push({
2205
+ id: exports.ValidationRuleIds.API_GRAPHQL_PATH_PRESENT,
2206
+ severity: 'error',
2207
+ passed: hasGraphqlPath,
2208
+ message: hasGraphqlPath
2209
+ ? 'OpenAPI contract mentions /graphql for GraphQL API style.'
2210
+ : 'OpenAPI contract does not mention /graphql even though apiStyle is graphql.',
2211
+ sourceFile: openApiRelativePath,
2212
+ filePath: openApiRelativePath,
2213
+ findingType: 'api-scope',
2214
+ api: '/graphql',
2215
+ });
2216
+ }
2217
+ return results;
2218
+ }
2219
+ async function validateTenantModel(workspaceRoot, contract, config, validationConfig) {
2220
+ const results = [];
2221
+ if (!hasConfiguredDatabase(config)) {
2222
+ return results;
2223
+ }
2224
+ if (config.tenantModel === 'same_db_same_schema' &&
2225
+ !isRuleDisabled(exports.ValidationRuleIds.DB_TENANT_SAME_SCHEMA_TENANT_ID, validationConfig)) {
2226
+ const sqlRelativePath = resolveSqlBaselineRelativePath(contract);
2227
+ const sqlPath = path.join(workspaceRoot, ...sqlRelativePath.split('/'));
2228
+ const sql = await readTextFileIfExists(sqlPath);
2229
+ const hasTenantId = !!sql && sql.includes('tenant_id');
2230
+ results.push({
2231
+ id: exports.ValidationRuleIds.DB_TENANT_SAME_SCHEMA_TENANT_ID,
2232
+ severity: 'error',
2233
+ passed: hasTenantId,
2234
+ message: hasTenantId
2235
+ ? 'Multi-tenant same-schema SQL contains tenant_id discriminator.'
2236
+ : `${sqlRelativePath} should contain tenant_id for same_db_same_schema tenant model.`,
2237
+ sourceFile: sqlRelativePath,
2238
+ filePath: sqlRelativePath,
2239
+ findingType: 'table-scope',
2240
+ table: 'tenant_id',
2241
+ });
2242
+ }
2243
+ if (config.tenantModel === 'same_db_different_schema' &&
2244
+ !isRuleDisabled(exports.ValidationRuleIds.DOC_TENANT_DIFFERENT_SCHEMA_ADR, validationConfig)) {
2245
+ const tenantModelAdrRelativePath = resolveTenantModelAdrRelativePath(contract);
2246
+ const adrPath = path.join(workspaceRoot, ...tenantModelAdrRelativePath.split('/'));
2247
+ const exists = await pathExists(adrPath);
2248
+ results.push({
2249
+ id: exports.ValidationRuleIds.DOC_TENANT_DIFFERENT_SCHEMA_ADR,
2250
+ severity: 'warning',
2251
+ passed: exists,
2252
+ message: exists
2253
+ ? 'ADR for tenant model exists for different-schema model.'
2254
+ : `${tenantModelAdrRelativePath} should exist for same_db_different_schema tenant model.`,
2255
+ });
2256
+ }
2257
+ return results;
2258
+ }
2259
+ async function validateRbac(workspaceRoot, contract, config, validationConfig) {
2260
+ const results = [];
2261
+ if (!config.rbac ||
2262
+ isRuleDisabled(exports.ValidationRuleIds.DOC_RBAC_MENTIONED_IN_OVERVIEW, validationConfig)) {
2263
+ return results;
2264
+ }
2265
+ const overviewRelativePath = resolveOverviewRelativePath(contract);
2266
+ const overviewPath = path.join(workspaceRoot, ...overviewRelativePath.split('/'));
2267
+ const overview = await readTextFileIfExists(overviewPath);
2268
+ const overviewLower = overview?.toLowerCase() ?? '';
2269
+ const mentionsRbac = overviewLower.includes('rbac') || overviewLower.includes('role-based access control');
2270
+ results.push({
2271
+ id: exports.ValidationRuleIds.DOC_RBAC_MENTIONED_IN_OVERVIEW,
2272
+ severity: 'warning',
2273
+ passed: mentionsRbac,
2274
+ message: mentionsRbac
2275
+ ? 'Architecture overview mentions RBAC or role-based access control.'
2276
+ : `${overviewRelativePath} should mention RBAC or role-based access control when rbac is true.`,
2277
+ sourceFile: overviewRelativePath,
2278
+ filePath: overviewRelativePath,
2279
+ findingType: 'file-scope',
2280
+ });
2281
+ return results;
2282
+ }
2283
+ async function validateArchitectureStyle(workspaceRoot, contract, validationConfig) {
2284
+ const results = [];
2285
+ const { architectureStyle, implementationProfile } = contract;
2286
+ if (architectureStyle !== 'modular_monolith' ||
2287
+ isRuleDisabled(exports.ValidationRuleIds.ARCH_MODULAR_STRUCTURE, validationConfig)) {
2288
+ return results;
2289
+ }
2290
+ const isLibraryOnlyProject = contract.projectKinds.includes('library') &&
2291
+ !contract.projectKinds.some((kind) => kind === 'backend' || kind === 'frontend');
2292
+ if (isLibraryOnlyProject) {
2293
+ return results;
2294
+ }
2295
+ const modulesRootRelativePath = await getConfiguredModulesRoot(workspaceRoot, contract);
2296
+ const moduleDirectories = await (0, moduleDiscovery_1.listModuleDirectoryNames)(workspaceRoot, modulesRootRelativePath);
2297
+ const minimumModuleCount = isFlatModulesRoot(modulesRootRelativePath) ? 2 : 1;
2298
+ const hasValidModuleRoot = moduleDirectories.length >= minimumModuleCount;
2299
+ results.push({
2300
+ id: exports.ValidationRuleIds.ARCH_MODULAR_STRUCTURE,
2301
+ severity: 'error',
2302
+ passed: hasValidModuleRoot,
2303
+ message: hasValidModuleRoot
2304
+ ? `Configured module root '${modulesRootRelativePath}' contains ${moduleDirectories.length} top-level module/capability directories.`
2305
+ : `Modular monolith structure missing: configured module root '${modulesRootRelativePath}' must exist and contain at least ${minimumModuleCount} top-level module/capability director${minimumModuleCount === 1 ? 'y' : 'ies'}.`,
2306
+ });
2307
+ if (implementationProfile === 'typescript-backend' &&
2308
+ !isRuleDisabled(exports.ValidationRuleIds.ARCH_OVERVIEW_ARTIFACT_EXISTS, validationConfig)) {
2309
+ const overviewRelativePath = resolveOverviewRelativePath(contract);
2310
+ const overviewPath = path.join(workspaceRoot, ...overviewRelativePath.split('/'));
2311
+ const overviewExists = await pathExists(overviewPath);
2312
+ results.push({
2313
+ id: exports.ValidationRuleIds.ARCH_OVERVIEW_ARTIFACT_EXISTS,
2314
+ severity: 'error',
2315
+ passed: overviewExists,
2316
+ message: overviewExists
2317
+ ? `${overviewRelativePath} exists.`
2318
+ : `${overviewRelativePath} is missing from the initialized architecture baseline.`,
2319
+ });
2320
+ }
2321
+ if (implementationProfile === 'typescript-backend' &&
2322
+ !isRuleDisabled(exports.ValidationRuleIds.ARCH_MODULE_BASELINE_COMPLETE, validationConfig)) {
2323
+ const normalizeEntrypoint = (entry) => normalizeRelativePath(entry.trim());
2324
+ const hasDeclaredPublicEntrypoint = async (moduleName, entry) => {
2325
+ const entryPath = path.join(workspaceRoot, ...normalizeEntrypoint(entry).split('/'));
2326
+ return pathExists(entryPath);
2327
+ };
2328
+ const readModuleContractPublicEntrypoints = async (moduleName) => {
2329
+ const registryEntry = contract.modules[moduleName];
2330
+ const contractRelativePath = typeof registryEntry?.contract === 'string'
2331
+ ? normalizeRelativePath(registryEntry.contract)
2332
+ : normalizeRelativePath((0, architectureInit_1.buildDefaultModuleContractPath)(moduleName, contract));
2333
+ const contractAbsolutePath = path.join(workspaceRoot, ...contractRelativePath.split('/'));
2334
+ const rawContract = await readTextFileIfExists(contractAbsolutePath);
2335
+ if (!rawContract) {
2336
+ return [];
2337
+ }
2338
+ try {
2339
+ const parsed = JSON.parse(rawContract);
2340
+ if (!Array.isArray(parsed.publicEntrypoints)) {
2341
+ return [];
2342
+ }
2343
+ return parsed.publicEntrypoints
2344
+ .filter((value) => typeof value === 'string')
2345
+ .map((entry) => normalizeEntrypoint(entry));
2346
+ }
2347
+ catch {
2348
+ return [];
2349
+ }
2350
+ };
2351
+ const modulesRootRelativePath = await getConfiguredModulesRoot(workspaceRoot, contract);
2352
+ const modulesRoot = path.join(workspaceRoot, ...modulesRootRelativePath.split('/'));
2353
+ if (await pathExists(modulesRoot)) {
2354
+ const hierarchicalModules = await (0, moduleDiscovery_1.discoverHierarchicalModuleRoots)(workspaceRoot, modulesRootRelativePath);
2355
+ const moduleDirectories = hierarchicalModules.length > 0
2356
+ ? hierarchicalModules.map((entry) => ({
2357
+ moduleName: entry.moduleId,
2358
+ sourcePath: entry.sourcePath,
2359
+ }))
2360
+ : (await node_fs_1.promises.readdir(modulesRoot, { withFileTypes: true }))
2361
+ .filter((entry) => entry.isDirectory())
2362
+ .map((entry) => ({
2363
+ moduleName: entry.name,
2364
+ sourcePath: `${modulesRootRelativePath}/${entry.name}`,
2365
+ }))
2366
+ .sort((left, right) => left.moduleName.localeCompare(right.moduleName));
2367
+ const dependencySummary = await (0, dependencyValidation_1.buildDependencyGraphSummary)(workspaceRoot, contract, {
2368
+ modulesRootRelativePath,
2369
+ });
2370
+ const modulesRequiringPublicEntrypointFromInboundDeclaredDependencies = new Set(dependencySummary.modules
2371
+ .filter((moduleSummary) => moduleSummary.inboundDeclaredFrom.length > 0)
2372
+ .map((moduleSummary) => moduleSummary.module));
2373
+ const incompleteModules = [];
2374
+ for (const moduleDirectory of moduleDirectories) {
2375
+ const moduleName = moduleDirectory.moduleName;
2376
+ const moduleDir = path.join(workspaceRoot, ...moduleDirectory.sourcePath.split('/'));
2377
+ const readmeExists = await pathExists(path.join(moduleDir, 'README.md'));
2378
+ const indexExists = await pathExists(path.join(moduleDir, 'index.ts'));
2379
+ const registryPublicEntrypoints = (contract.modules[moduleName]?.publicEntrypoints ?? [])
2380
+ .filter((entry) => typeof entry === 'string')
2381
+ .map((entry) => normalizeEntrypoint(entry));
2382
+ const contractPublicEntrypoints = await readModuleContractPublicEntrypoints(moduleName);
2383
+ const declaredPublicEntrypoints = [
2384
+ ...new Set([...registryPublicEntrypoints, ...contractPublicEntrypoints]),
2385
+ ];
2386
+ const requiresPublicEntrypoint = declaredPublicEntrypoints.length > 0 ||
2387
+ modulesRequiringPublicEntrypointFromInboundDeclaredDependencies.has(moduleName);
2388
+ const missingParts = [];
2389
+ if (!readmeExists) {
2390
+ missingParts.push('README.md');
2391
+ }
2392
+ if (requiresPublicEntrypoint) {
2393
+ if (!indexExists) {
2394
+ missingParts.push('index.ts');
2395
+ }
2396
+ if (declaredPublicEntrypoints.length > 0) {
2397
+ const declaredEntrypointExists = (await Promise.all(declaredPublicEntrypoints.map((entry) => hasDeclaredPublicEntrypoint(moduleName, entry)))).some((exists) => exists);
2398
+ if (!declaredEntrypointExists) {
2399
+ missingParts.push('public entrypoint');
2400
+ }
2401
+ }
2402
+ }
2403
+ if (missingParts.length > 0) {
2404
+ incompleteModules.push(`${moduleName} (${missingParts.join(', ')})`);
2405
+ }
2406
+ }
2407
+ results.push({
2408
+ id: exports.ValidationRuleIds.ARCH_MODULE_BASELINE_COMPLETE,
2409
+ severity: 'warning',
2410
+ passed: incompleteModules.length === 0,
2411
+ message: incompleteModules.length === 0
2412
+ ? `All immediate modules under ${modulesRootRelativePath} contain README.md, and modules requiring a public entrypoint provide one.`
2413
+ : `Module baseline drift detected in ${modulesRootRelativePath}: ${incompleteModules.join('; ')}.`,
2414
+ });
2415
+ }
2416
+ }
2417
+ return results;
2418
+ }
2419
+ async function validateDatabaseArtifacts(workspaceRoot, contract, config, validationConfig) {
2420
+ const results = [];
2421
+ if (!hasConfiguredDatabase(config) ||
2422
+ config.implementationProfile !== 'typescript-backend' ||
2423
+ isRuleDisabled(exports.ValidationRuleIds.DB_SQL_BASELINE_EXISTS, validationConfig)) {
2424
+ return results;
2425
+ }
2426
+ const sqlBaselineRelativePath = resolveSqlBaselineRelativePath(contract);
2427
+ const sqlBaselinePath = path.join(workspaceRoot, ...sqlBaselineRelativePath.split('/'));
2428
+ const exists = await pathExists(sqlBaselinePath);
2429
+ results.push({
2430
+ id: exports.ValidationRuleIds.DB_SQL_BASELINE_EXISTS,
2431
+ severity: 'error',
2432
+ passed: exists,
2433
+ message: exists
2434
+ ? `${sqlBaselineRelativePath} exists.`
2435
+ : `${sqlBaselineRelativePath} is missing from the initialized database baseline.`,
2436
+ });
2437
+ return results;
2438
+ }
2439
+ function buildDependencyFindingStableKey(finding) {
2440
+ if (!finding.result.id ||
2441
+ !finding.module ||
2442
+ !finding.target ||
2443
+ !finding.filePath ||
2444
+ finding.line === undefined ||
2445
+ !finding.importSpecifier) {
2446
+ return undefined;
2447
+ }
2448
+ return [
2449
+ finding.result.id,
2450
+ finding.module,
2451
+ finding.target,
2452
+ finding.filePath.replace(/\\/g, '/'),
2453
+ String(finding.line),
2454
+ finding.importSpecifier,
2455
+ ].join('|');
2456
+ }
2457
+ function dedupeDependencyFindings(findings) {
2458
+ const seen = new Set();
2459
+ const deduped = [];
2460
+ for (const finding of findings) {
2461
+ const stableKey = buildDependencyFindingStableKey(finding);
2462
+ if (stableKey && seen.has(stableKey)) {
2463
+ continue;
2464
+ }
2465
+ if (stableKey) {
2466
+ seen.add(stableKey);
2467
+ }
2468
+ deduped.push(finding);
2469
+ }
2470
+ return deduped;
2471
+ }
2472
+ async function validateModuleDependencies(workspaceRoot, contract, validationConfig, onDependencyConfigError, dependencyRulesConfigOverride, precomputedImports, hasArchitectureModulesOverride) {
2473
+ const modulesRootRelativePath = await getConfiguredModulesRoot(workspaceRoot, contract);
2474
+ const hasArchitectureModules = hasArchitectureModulesOverride ?? hasConfiguredArchitectureModules(contract);
2475
+ const dependencyRuleChecks = {
2476
+ modulesRootRelativePath,
2477
+ checkCircularDependencies: hasArchitectureModules && !isRuleDisabled(exports.ValidationRuleIds.DEP_CIRCULAR_MODULE_DEPENDENCY, validationConfig),
2478
+ checkForbiddenDependencies: hasArchitectureModules && !isRuleDisabled(exports.ValidationRuleIds.DEP_FORBIDDEN_CROSS_MODULE_DEPENDENCY, validationConfig),
2479
+ checkNonPublicImports: hasArchitectureModules && !isRuleDisabled(exports.ValidationRuleIds.DEP_NON_PUBLIC_CROSS_MODULE_IMPORT, validationConfig),
2480
+ checkComponentDependencyPolicies: !isRuleDisabled(exports.ValidationRuleIds.DEP_CROSS_COMPONENT_POLICY_VIOLATION, validationConfig),
2481
+ };
2482
+ const contractDependencyRuleChecks = {
2483
+ checkDeclaredDependencies: hasArchitectureModules && !isRuleDisabled(exports.ValidationRuleIds.DEP_CROSS_MODULE_DEPENDENCY_DECLARED_IN_CONTRACT, validationConfig),
2484
+ checkPublicSurfaceImports: hasArchitectureModules && !isRuleDisabled(exports.ValidationRuleIds.DEP_CROSS_MODULE_IMPORT_USES_PUBLIC_SURFACE_ONLY, validationConfig),
2485
+ checkDeclaredDependenciesUsedByCode: hasArchitectureModules && !isRuleDisabled(exports.ValidationRuleIds.DEP_DECLARED_MODULE_DEPENDENCY_NOT_USED, validationConfig),
2486
+ };
2487
+ try {
2488
+ const [legacyFindings, contractFindings] = await Promise.all([
2489
+ (0, dependencyValidation_1.validateDependencyBoundaries)(workspaceRoot, {
2490
+ ...dependencyRuleChecks,
2491
+ ...(dependencyRulesConfigOverride
2492
+ ? { dependencyRulesConfigOverride }
2493
+ : {}),
2494
+ ...(precomputedImports ? { precomputedImports } : {}),
2495
+ }),
2496
+ (0, dependencyValidation_1.validateDependencyContractBoundaries)(workspaceRoot, contract, {
2497
+ ...contractDependencyRuleChecks,
2498
+ modulesRootRelativePath,
2499
+ suppressPublicSurfaceFailuresForNonPublicImports: dependencyRuleChecks.checkNonPublicImports,
2500
+ ...(precomputedImports ? { precomputedImports } : {}),
2501
+ }),
2502
+ ]);
2503
+ const findings = dedupeDependencyFindings([...legacyFindings, ...contractFindings]);
2504
+ const results = [];
2505
+ for (const finding of findings) {
2506
+ const resultWithSource = {
2507
+ ...finding.result,
2508
+ ...(finding.module ? { module: finding.module } : {}),
2509
+ ...(finding.target ? { target: finding.target } : {}),
2510
+ ...(finding.table ? { table: finding.table } : {}),
2511
+ ...(finding.api ? { api: finding.api } : {}),
2512
+ ...(finding.findingType ? { findingType: finding.findingType } : {}),
2513
+ ...(finding.filePath ? { sourceFile: finding.filePath, filePath: finding.filePath } : {}),
2514
+ ...(finding.line !== undefined ? { sourceLine: finding.line } : {}),
2515
+ ...(finding.importSpecifier ? { importSpecifier: finding.importSpecifier } : {}),
2516
+ ...(finding.dependencyType ? { dependencyType: finding.dependencyType } : {}),
2517
+ ...(finding.sourceComponentId ? { sourceComponentId: finding.sourceComponentId } : {}),
2518
+ ...(finding.sourceComponentName ? { sourceComponentName: finding.sourceComponentName } : {}),
2519
+ ...(finding.targetComponentId !== undefined
2520
+ ? { targetComponentId: finding.targetComponentId }
2521
+ : {}),
2522
+ ...(finding.targetComponentName !== undefined
2523
+ ? { targetComponentName: finding.targetComponentName }
2524
+ : {}),
2525
+ ...(finding.resolvedTargetPath !== undefined
2526
+ ? { resolvedTargetPath: finding.resolvedTargetPath }
2527
+ : {}),
2528
+ ...(finding.isExternal !== undefined ? { isExternal: finding.isExternal } : {}),
2529
+ };
2530
+ results.push(resultWithSource);
2531
+ }
2532
+ return results;
2533
+ }
2534
+ catch (error) {
2535
+ if (error instanceof dependencyValidation_1.DependencyRulesConfigError) {
2536
+ onDependencyConfigError?.(error.message);
2537
+ return [];
2538
+ }
2539
+ throw error;
2540
+ }
2541
+ }
2542
+ async function validateDependencyGraphIsolation(workspaceRoot, contract, dependencyGraphSummary, validationConfig) {
2543
+ if (isRuleDisabled(exports.ValidationRuleIds.DEP_ISOLATED_MODULE_DETECTED, validationConfig)) {
2544
+ return [];
2545
+ }
2546
+ const isolatedResults = [];
2547
+ const sourceFileExtensions = new Set([
2548
+ '.ts',
2549
+ '.tsx',
2550
+ '.js',
2551
+ '.jsx',
2552
+ '.mjs',
2553
+ '.cjs',
2554
+ '.mts',
2555
+ '.cts',
2556
+ '.py',
2557
+ '.go',
2558
+ '.java',
2559
+ '.kt',
2560
+ '.kts',
2561
+ '.cs',
2562
+ '.php',
2563
+ '.rb',
2564
+ '.vue',
2565
+ '.svelte',
2566
+ ]);
2567
+ const isTestLikeModule = (moduleName) => {
2568
+ const normalized = moduleName.toLowerCase();
2569
+ return (normalized === 'test' ||
2570
+ normalized === 'tests' ||
2571
+ normalized.endsWith('-test') ||
2572
+ normalized.endsWith('-tests'));
2573
+ };
2574
+ const collectModuleSourceEvidence = async (directoryPath) => {
2575
+ let entries;
2576
+ try {
2577
+ entries = await node_fs_1.promises.readdir(directoryPath, { withFileTypes: true });
2578
+ }
2579
+ catch {
2580
+ return { sourceFileCount: 0, hasReadme: false, hasEntrypointFile: false };
2581
+ }
2582
+ let sourceFileCount = 0;
2583
+ let hasReadme = false;
2584
+ let hasEntrypointFile = false;
2585
+ for (const entry of entries) {
2586
+ const fullPath = path.join(directoryPath, entry.name);
2587
+ if (entry.isDirectory()) {
2588
+ if ([
2589
+ 'node_modules',
2590
+ '.git',
2591
+ '.archpilot',
2592
+ '.next',
2593
+ 'dist',
2594
+ 'out',
2595
+ 'build',
2596
+ 'coverage',
2597
+ 'fixtures',
2598
+ '__fixtures__',
2599
+ ].includes(entry.name)) {
2600
+ continue;
2601
+ }
2602
+ const nested = await collectModuleSourceEvidence(fullPath);
2603
+ sourceFileCount += nested.sourceFileCount;
2604
+ hasReadme ||= nested.hasReadme;
2605
+ hasEntrypointFile ||= nested.hasEntrypointFile;
2606
+ continue;
2607
+ }
2608
+ if (!entry.isFile()) {
2609
+ continue;
2610
+ }
2611
+ const lowerName = entry.name.toLowerCase();
2612
+ if (lowerName === 'readme.md' || lowerName === 'readme.mdx') {
2613
+ hasReadme = true;
2614
+ }
2615
+ if (/^(index|main|app|extension|cli)\.[cm]?[jt]sx?$/iu.test(entry.name)) {
2616
+ hasEntrypointFile = true;
2617
+ }
2618
+ if (sourceFileExtensions.has(path.extname(entry.name).toLowerCase())) {
2619
+ sourceFileCount += 1;
2620
+ }
2621
+ }
2622
+ return { sourceFileCount, hasReadme, hasEntrypointFile };
2623
+ };
2624
+ const readPackageJsonIfExists = async (moduleDirectoryPath) => {
2625
+ const content = await readTextFileIfExists(path.join(moduleDirectoryPath, 'package.json'));
2626
+ if (!content) {
2627
+ return undefined;
2628
+ }
2629
+ try {
2630
+ const parsed = JSON.parse(content);
2631
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
2632
+ ? parsed
2633
+ : undefined;
2634
+ }
2635
+ catch {
2636
+ return undefined;
2637
+ }
2638
+ };
2639
+ const hasPackageEntrypoint = async (moduleDirectoryPath) => {
2640
+ const packageJson = await readPackageJsonIfExists(moduleDirectoryPath);
2641
+ if (!packageJson) {
2642
+ return false;
2643
+ }
2644
+ return ['main', 'module', 'browser', 'types', 'bin', 'exports'].some((field) => packageJson[field] !== undefined);
2645
+ };
2646
+ const hasKnownBoundaryRole = (moduleName) => {
2647
+ const normalized = moduleName.toLowerCase();
2648
+ const compact = normalized.replace(/[-_/]/gu, '');
2649
+ const segments = normalized.split(/[/-]/u);
2650
+ return ['adr', 'ci', 'compliance', 'config', 'governance', 'policy', 'validation', 'smartinit'].some((role) => compact.includes(role) || segments.includes(role));
2651
+ };
2652
+ const hasConfiguredPublicEntrypoint = (moduleName) => {
2653
+ const publicEntrypoints = contract.modules[moduleName]?.publicEntrypoints;
2654
+ return (Array.isArray(publicEntrypoints) &&
2655
+ publicEntrypoints.some((entry) => typeof entry === 'string' && entry.trim().length > 0));
2656
+ };
2657
+ const hasIntentionalIndependentEvidence = async (moduleSummary, moduleDirectoryPath) => {
2658
+ const sourceEvidence = await collectModuleSourceEvidence(moduleDirectoryPath);
2659
+ return (moduleSummary.hasContract ||
2660
+ hasConfiguredPublicEntrypoint(moduleSummary.module) ||
2661
+ sourceEvidence.hasReadme ||
2662
+ sourceEvidence.hasEntrypointFile ||
2663
+ (await hasPackageEntrypoint(moduleDirectoryPath)) ||
2664
+ moduleSummary.childModules.length > 0 ||
2665
+ (moduleSummary.parentModule !== undefined &&
2666
+ moduleSummary.childModules.length === 0 &&
2667
+ sourceEvidence.sourceFileCount > 0) ||
2668
+ hasKnownBoundaryRole(moduleSummary.module) ||
2669
+ sourceEvidence.sourceFileCount >= 2);
2670
+ };
2671
+ for (const moduleSummary of dependencyGraphSummary.modules) {
2672
+ if (isTestLikeModule(moduleSummary.module)) {
2673
+ continue;
2674
+ }
2675
+ const registryEntry = contract.modules[moduleSummary.module];
2676
+ if (!registryEntry?.path) {
2677
+ continue;
2678
+ }
2679
+ const moduleDirectoryPath = path.join(workspaceRoot, ...registryEntry.path.split('/'));
2680
+ const moduleDirectoryExists = await pathExists(moduleDirectoryPath);
2681
+ if (!moduleDirectoryExists) {
2682
+ continue;
2683
+ }
2684
+ const isIsolated = moduleSummary.declaredDependencies.length === 0 &&
2685
+ moduleSummary.actualDependencies.length === 0 &&
2686
+ moduleSummary.inboundDeclaredFrom.length === 0 &&
2687
+ moduleSummary.inboundActualFrom.length === 0;
2688
+ if (!isIsolated) {
2689
+ continue;
2690
+ }
2691
+ if (await hasIntentionalIndependentEvidence(moduleSummary, moduleDirectoryPath)) {
2692
+ continue;
2693
+ }
2694
+ isolatedResults.push({
2695
+ id: exports.ValidationRuleIds.DEP_ISOLATED_MODULE_DETECTED,
2696
+ severity: 'warning',
2697
+ passed: false,
2698
+ message: `Module '${moduleSummary.module}' is a potential orphan: it has no declared or actual inbound/outbound module dependencies and little evidence of intentional architectural use.`,
2699
+ findingType: 'module-dependency',
2700
+ module: moduleSummary.module,
2701
+ });
2702
+ }
2703
+ if (isolatedResults.length > 0) {
2704
+ return isolatedResults;
2705
+ }
2706
+ return [
2707
+ {
2708
+ id: exports.ValidationRuleIds.DEP_ISOLATED_MODULE_DETECTED,
2709
+ severity: 'warning',
2710
+ passed: true,
2711
+ message: 'No potential orphan modules detected among registered modules with existing registry paths.',
2712
+ },
2713
+ ];
2714
+ }
2715
+ function canonicalizeTransitiveCycle(cycle) {
2716
+ const uniqueNodes = cycle.slice(0, -1);
2717
+ const rotations = uniqueNodes.map((_, index) => {
2718
+ const rotated = [...uniqueNodes.slice(index), ...uniqueNodes.slice(0, index)];
2719
+ return `${rotated.join('->')}->${rotated[0]}`;
2720
+ });
2721
+ return rotations.sort((left, right) => left.localeCompare(right))[0];
2722
+ }
2723
+ function detectTransitiveCyclesFromDependencySummary(dependencyGraphSummary) {
2724
+ const moduleIds = dependencyGraphSummary.modules.map((entry) => entry.module);
2725
+ const moduleIdSet = new Set(moduleIds);
2726
+ const adjacency = new Map();
2727
+ for (const moduleSummary of dependencyGraphSummary.modules) {
2728
+ const neighbors = [
2729
+ ...moduleSummary.declaredDependencies,
2730
+ ...moduleSummary.actualDependencies,
2731
+ ]
2732
+ .filter((dependency) => moduleIdSet.has(dependency) && dependency !== moduleSummary.module)
2733
+ .filter((dependency, index, values) => values.indexOf(dependency) === index)
2734
+ .sort((left, right) => left.localeCompare(right));
2735
+ adjacency.set(moduleSummary.module, neighbors);
2736
+ }
2737
+ const cycles = new Set();
2738
+ const visited = new Set();
2739
+ const stack = [];
2740
+ const visit = (moduleId) => {
2741
+ visited.add(moduleId);
2742
+ stack.push(moduleId);
2743
+ for (const neighbor of adjacency.get(moduleId) ?? []) {
2744
+ const stackIndex = stack.indexOf(neighbor);
2745
+ if (stackIndex !== -1) {
2746
+ const cyclePath = [...stack.slice(stackIndex), neighbor];
2747
+ const cycleNodeCount = cyclePath.length - 1;
2748
+ if (cycleNodeCount >= 3) {
2749
+ cycles.add(canonicalizeTransitiveCycle(cyclePath));
2750
+ }
2751
+ continue;
2752
+ }
2753
+ if (!visited.has(neighbor)) {
2754
+ visit(neighbor);
2755
+ }
2756
+ }
2757
+ stack.pop();
2758
+ };
2759
+ for (const moduleId of [...moduleIds].sort((left, right) => left.localeCompare(right))) {
2760
+ if (!visited.has(moduleId)) {
2761
+ visit(moduleId);
2762
+ }
2763
+ }
2764
+ return [...cycles].sort((left, right) => left.localeCompare(right));
2765
+ }
2766
+ function validateTransitiveModuleDependencyCycles(dependencyGraphSummary, validationConfig) {
2767
+ if (isRuleDisabled(exports.ValidationRuleIds.DEP_TRANSITIVE_CIRCULAR_MODULE_DEPENDENCY, validationConfig)) {
2768
+ return [];
2769
+ }
2770
+ const cycles = detectTransitiveCyclesFromDependencySummary(dependencyGraphSummary);
2771
+ if (cycles.length === 0) {
2772
+ return [
2773
+ {
2774
+ id: exports.ValidationRuleIds.DEP_TRANSITIVE_CIRCULAR_MODULE_DEPENDENCY,
2775
+ severity: 'error',
2776
+ passed: true,
2777
+ message: 'No transitive circular module dependencies detected across declared and actual dependency edges.',
2778
+ },
2779
+ ];
2780
+ }
2781
+ return cycles.map((cycle) => ({
2782
+ id: exports.ValidationRuleIds.DEP_TRANSITIVE_CIRCULAR_MODULE_DEPENDENCY,
2783
+ severity: 'error',
2784
+ passed: false,
2785
+ message: `Transitive circular dependency detected: ${cycle.replaceAll('->', ' -> ')}.`,
2786
+ }));
2787
+ }
2788
+ function resolveHighInboundDependencyThreshold(contract) {
2789
+ const configured = contract.governance.validation?.highInboundDependencyWarningThreshold;
2790
+ if (typeof configured !== 'number' || !Number.isInteger(configured) || configured < 1) {
2791
+ return 3;
2792
+ }
2793
+ return configured;
2794
+ }
2795
+ function validateHighInboundDependencyCount(contract, dependencyGraphSummary, validationConfig) {
2796
+ if (isRuleDisabled(exports.ValidationRuleIds.DEP_MODULE_HIGH_INBOUND_DEPENDENCY_COUNT, validationConfig)) {
2797
+ return [];
2798
+ }
2799
+ const threshold = resolveHighInboundDependencyThreshold(contract);
2800
+ const violations = dependencyGraphSummary.modules
2801
+ .map((moduleSummary) => {
2802
+ const declaredInboundCount = moduleSummary.inboundDeclaredFrom.length;
2803
+ const actualInboundCount = moduleSummary.inboundActualFrom.length;
2804
+ const effectiveInboundCount = Math.max(declaredInboundCount, actualInboundCount);
2805
+ return {
2806
+ moduleSummary,
2807
+ declaredInboundCount,
2808
+ actualInboundCount,
2809
+ effectiveInboundCount,
2810
+ };
2811
+ })
2812
+ .filter((entry) => entry.effectiveInboundCount >= threshold)
2813
+ .sort((left, right) => left.moduleSummary.module.localeCompare(right.moduleSummary.module));
2814
+ if (violations.length === 0) {
2815
+ return [
2816
+ {
2817
+ id: exports.ValidationRuleIds.DEP_MODULE_HIGH_INBOUND_DEPENDENCY_COUNT,
2818
+ severity: 'warning',
2819
+ passed: true,
2820
+ message: `No modules exceeded the inbound dependency threshold (${threshold}).`,
2821
+ },
2822
+ ];
2823
+ }
2824
+ return violations.map((entry) => ({
2825
+ id: exports.ValidationRuleIds.DEP_MODULE_HIGH_INBOUND_DEPENDENCY_COUNT,
2826
+ severity: 'warning',
2827
+ passed: false,
2828
+ message: `Module '${entry.moduleSummary.module}' has high inbound dependency count (${entry.effectiveInboundCount}). Declared inbound: ${entry.declaredInboundCount}. Actual inbound: ${entry.actualInboundCount}.`,
2829
+ findingType: 'module-dependency',
2830
+ module: entry.moduleSummary.module,
2831
+ }));
2832
+ }
2833
+ async function readLayerRulesConfig(workspaceRoot) {
2834
+ const layerRulesPath = path.join(workspaceRoot, '.archpilot', 'layer-rules.json');
2835
+ const contents = await readTextFileIfExists(layerRulesPath);
2836
+ if (!contents) {
2837
+ return undefined;
2838
+ }
2839
+ let parsed;
2840
+ try {
2841
+ parsed = JSON.parse(contents);
2842
+ }
2843
+ catch {
2844
+ return {
2845
+ layers: [],
2846
+ moduleLayers: {},
2847
+ };
2848
+ }
2849
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2850
+ return {
2851
+ layers: [],
2852
+ moduleLayers: {},
2853
+ };
2854
+ }
2855
+ const typedParsed = parsed;
2856
+ const layersRaw = typedParsed.layers;
2857
+ const moduleLayersRaw = typedParsed.moduleLayers;
2858
+ const layers = Array.isArray(layersRaw)
2859
+ ? layersRaw.filter((value) => typeof value === 'string')
2860
+ : [];
2861
+ const moduleLayers = moduleLayersRaw && typeof moduleLayersRaw === 'object' && !Array.isArray(moduleLayersRaw)
2862
+ ? Object.entries(moduleLayersRaw)
2863
+ .filter((entry) => typeof entry[1] === 'string')
2864
+ .sort(([left], [right]) => left.localeCompare(right))
2865
+ .reduce((accumulator, [moduleName, layer]) => {
2866
+ accumulator[moduleName] = layer;
2867
+ return accumulator;
2868
+ }, {})
2869
+ : {};
2870
+ return {
2871
+ layers,
2872
+ moduleLayers,
2873
+ };
2874
+ }
2875
+ function buildLayerDirectionAdjacency(dependencyGraphSummary) {
2876
+ return new Map(dependencyGraphSummary.modules
2877
+ .map((moduleSummary) => {
2878
+ const combinedDependencies = [
2879
+ ...moduleSummary.declaredDependencies,
2880
+ ...moduleSummary.actualDependencies,
2881
+ ]
2882
+ .filter((dependency, index, values) => values.indexOf(dependency) === index)
2883
+ .sort((left, right) => left.localeCompare(right));
2884
+ return [moduleSummary.module, combinedDependencies];
2885
+ })
2886
+ .sort(([left], [right]) => left.localeCompare(right)));
2887
+ }
2888
+ async function validateDependencyLayerDirection(workspaceRoot, dependencyGraphSummary, validationConfig) {
2889
+ if (isRuleDisabled(exports.ValidationRuleIds.DEP_LAYER_DIRECTION_VIOLATION, validationConfig)) {
2890
+ return [];
2891
+ }
2892
+ const layerRules = await readLayerRulesConfig(workspaceRoot);
2893
+ if (!layerRules) {
2894
+ return [
2895
+ {
2896
+ id: exports.ValidationRuleIds.DEP_LAYER_DIRECTION_VIOLATION,
2897
+ severity: 'info',
2898
+ passed: false,
2899
+ message: "Layer direction validation skipped: .archpilot/layer-rules.json is missing. Run 'ArchPilot: Initialize Architecture' to generate default layer rules.",
2900
+ },
2901
+ ];
2902
+ }
2903
+ if (layerRules.layers.length === 0) {
2904
+ return [
2905
+ {
2906
+ id: exports.ValidationRuleIds.DEP_LAYER_DIRECTION_VIOLATION,
2907
+ severity: 'error',
2908
+ passed: false,
2909
+ message: 'Layer direction validation configuration is invalid: .archpilot/layer-rules.json must define a non-empty layers array.',
2910
+ },
2911
+ ];
2912
+ }
2913
+ const layerOrder = new Map(layerRules.layers.map((layer, index) => [layer, index]));
2914
+ const adjacency = buildLayerDirectionAdjacency(dependencyGraphSummary);
2915
+ const violations = [];
2916
+ for (const [sourceModule, targets] of adjacency.entries()) {
2917
+ const sourceLayer = layerRules.moduleLayers[sourceModule];
2918
+ if (!sourceLayer || !layerOrder.has(sourceLayer)) {
2919
+ continue;
2920
+ }
2921
+ for (const targetModule of targets) {
2922
+ const targetLayer = layerRules.moduleLayers[targetModule];
2923
+ if (!targetLayer || !layerOrder.has(targetLayer)) {
2924
+ continue;
2925
+ }
2926
+ const sourceLayerIndex = layerOrder.get(sourceLayer) ?? 0;
2927
+ const targetLayerIndex = layerOrder.get(targetLayer) ?? 0;
2928
+ if (targetLayerIndex >= sourceLayerIndex) {
2929
+ continue;
2930
+ }
2931
+ violations.push({
2932
+ id: exports.ValidationRuleIds.DEP_LAYER_DIRECTION_VIOLATION,
2933
+ severity: 'error',
2934
+ passed: false,
2935
+ message: `Module '${sourceModule}' (${sourceLayer} layer) depends on '${targetModule}' (${targetLayer} layer), which violates architecture layering.`,
2936
+ findingType: 'module-dependency',
2937
+ module: sourceModule,
2938
+ target: targetModule,
2939
+ });
2940
+ }
2941
+ }
2942
+ if (violations.length === 0) {
2943
+ return [
2944
+ {
2945
+ id: exports.ValidationRuleIds.DEP_LAYER_DIRECTION_VIOLATION,
2946
+ severity: 'error',
2947
+ passed: true,
2948
+ message: 'No architecture layer direction dependency violations detected.',
2949
+ },
2950
+ ];
2951
+ }
2952
+ return violations.sort((left, right) => left.message.localeCompare(right.message));
2953
+ }
2954
+ async function validateModuleContractRegistryIntegrity(workspaceRoot, contract, validationConfig) {
2955
+ const moduleContractRuleChecks = {
2956
+ checkRegistryPathExists: !isRuleDisabled(exports.ValidationRuleIds.ARCH_MODULE_REGISTRY_PATH_EXISTS, validationConfig),
2957
+ checkContractFileExists: !isRuleDisabled(exports.ValidationRuleIds.ARCH_MODULE_CONTRACT_FILE_EXISTS, validationConfig),
2958
+ checkContractIdentityMatchesRegistry: !isRuleDisabled(exports.ValidationRuleIds.ARCH_MODULE_CONTRACT_IDENTITY_MATCHES_REGISTRY, validationConfig),
2959
+ checkPublicEntrypointsAligned: !isRuleDisabled(exports.ValidationRuleIds.ARCH_MODULE_PUBLIC_ENTRYPOINTS_ALIGNED, validationConfig),
2960
+ };
2961
+ const findings = await (0, moduleContractValidation_1.validateModuleContractIntegrity)(workspaceRoot, contract, moduleContractRuleChecks);
2962
+ const results = [];
2963
+ for (const finding of findings) {
2964
+ if (!finding.filePath) {
2965
+ results.push(finding.result);
2966
+ continue;
2967
+ }
2968
+ const resultWithSource = {
2969
+ ...finding.result,
2970
+ sourceFile: finding.filePath,
2971
+ filePath: finding.filePath,
2972
+ ...(finding.line !== undefined ? { sourceLine: finding.line } : {}),
2973
+ };
2974
+ results.push(resultWithSource);
2975
+ }
2976
+ return results;
2977
+ }
2978
+ async function validateImplementationProfile(config, validationConfig) {
2979
+ const results = [];
2980
+ const normalizedProfile = config.implementationProfile.trim().toLowerCase();
2981
+ const supportedProfileMatchers = [
2982
+ /^typescript-backend$/u,
2983
+ /^java-spring$/u,
2984
+ /^spring-backend$/u,
2985
+ /^backend-spring(?:-.+)?$/u,
2986
+ /^backend-nestjs(?:-.+)?$/u,
2987
+ /^python-fastapi$/u,
2988
+ /^backend-fastapi(?:-.+)?$/u,
2989
+ /^dotnet-webapi$/u,
2990
+ /^php-laravel$/u,
2991
+ /^backend-laravel(?:-.+)?$/u,
2992
+ /^go-backend$/u,
2993
+ ];
2994
+ const isSupportedProfile = supportedProfileMatchers.some((matcher) => matcher.test(normalizedProfile));
2995
+ if (!isSupportedProfile &&
2996
+ !isRuleDisabled(exports.ValidationRuleIds.ARCH_PROFILE_NOT_FULLY_SUPPORTED, validationConfig)) {
2997
+ results.push({
2998
+ id: exports.ValidationRuleIds.ARCH_PROFILE_NOT_FULLY_SUPPORTED,
2999
+ severity: 'info',
3000
+ passed: true,
3001
+ message: `Implementation profile '${config.implementationProfile}' has limited dedicated analyzer support in this release.`,
3002
+ });
3003
+ }
3004
+ return results;
3005
+ }
3006
+ const architectureReviewSignalRulePrefixes = ['AP-APP-', 'AP-DOM-', 'AP-TXN-'];
3007
+ function isArchitectureReviewSignalRule(ruleId) {
3008
+ return architectureReviewSignalRulePrefixes.some((prefix) => ruleId.startsWith(prefix));
3009
+ }
3010
+ function formatFindingMessageSections(ruleId, message, indent = ' ') {
3011
+ const nPlusOneMatch = /^(.+?without clear batching evidence\.)\s+Affected methods:\s+(.+?)\.\s+Repository\/model families:\s+(.+?)\.\s+Loop query evidence:\s+(.+?)\.\s+Risk instances:\s+(\d+)\.?$/u.exec(message);
3012
+ if (ruleId === exports.ValidationRuleIds.DQR_N_PLUS_ONE_QUERY_EVIDENCE && nPlusOneMatch) {
3013
+ const [, summary, methods, repositories, evidence, instances] = nPlusOneMatch;
3014
+ return [
3015
+ `${indent}- Details: ${summary}`,
3016
+ `${indent}- Affected methods:`,
3017
+ ...methods.split(/,\s*/u).filter(Boolean).map((value) => `${indent} - ${value}`),
3018
+ `${indent}- Repository/model families:`,
3019
+ ...repositories
3020
+ .split(/,\s*/u)
3021
+ .filter(Boolean)
3022
+ .map((value) => `${indent} - ${value}`),
3023
+ `${indent}- Risk instances: ${instances}`,
3024
+ `${indent}- Loop query evidence:`,
3025
+ ...evidence.split(/,\s*/u).filter(Boolean).map((value) => `${indent} - ${value}`),
3026
+ ];
3027
+ }
3028
+ const repeatedQueryMatch = /^Repeated query pattern:\s+method '(.+?)' performs the same '(.+?)' query (\d+) times in one flow\.$/u.exec(message);
3029
+ if (ruleId === exports.ValidationRuleIds.DQR_REPEATED_QUERY_PATTERN && repeatedQueryMatch) {
3030
+ const [, methodName, queryLabel, duplicateReads] = repeatedQueryMatch;
3031
+ return [
3032
+ `${indent}- Details: Repeated query detected in one request flow.`,
3033
+ `${indent}- Repeated query:`,
3034
+ `${indent} - Method: ${methodName}`,
3035
+ `${indent} - Query: ${queryLabel}`,
3036
+ `${indent} - Duplicate reads: ${duplicateReads}`,
3037
+ ];
3038
+ }
3039
+ const oversizedMatch = /^(.*?)\s+\((loc=\d+.*)\)\.?$/u.exec(message);
3040
+ if (oversizedMatch && (ruleId === exports.ValidationRuleIds.APP_SERVICE_GOD_CLASS || ruleId === exports.ValidationRuleIds.DOM_OVERSIZED_AGGREGATE_OR_SERVICE_BOUNDARY)) {
3041
+ const [, summary, metrics] = oversizedMatch;
3042
+ const metricMap = new Map(metrics.split(/,\s*/u).map((entry) => {
3043
+ const [key, value] = entry.split('=', 2);
3044
+ return [key?.trim() ?? '', value?.trim() ?? ''];
3045
+ }));
3046
+ const metricLines = [
3047
+ ['Lines of code', formatMetricNumber(metricMap.get('loc'))],
3048
+ ['Methods', metricMap.get('methods')],
3049
+ ['Collaborators', metricMap.get('collaborators')],
3050
+ ['Repository collaborators', metricMap.get('repositories')],
3051
+ ['Persistence calls', metricMap.get('persistence')],
3052
+ [
3053
+ 'Database signals',
3054
+ metricMap.get('db')?.split('|').filter(Boolean).join(', '),
3055
+ ],
3056
+ [
3057
+ 'External systems',
3058
+ metricMap.get('external')?.split('|').filter(Boolean).join(', '),
3059
+ ],
3060
+ ].filter(([, value]) => value && value.length > 0);
3061
+ return [
3062
+ `${indent}- Details: ${summary}`,
3063
+ `${indent}- Metrics:`,
3064
+ ...metricLines.map(([label, value]) => `${indent} - ${label}: ${value}`),
3065
+ ];
3066
+ }
3067
+ return [`${indent}- Details: ${message}`];
3068
+ }
3069
+ function formatMetricNumber(value) {
3070
+ if (!value || !/^\d+$/u.test(value)) {
3071
+ return value;
3072
+ }
3073
+ return Number(value).toLocaleString('en-US');
3074
+ }
3075
+ function collectValidationGroupDetails(entries) {
3076
+ const files = [...new Set(entries
3077
+ .map((entry) => entry.sourceFile)
3078
+ .filter((value) => typeof value === 'string' && value.length > 0))].sort((left, right) => left.localeCompare(right));
3079
+ const tables = [...new Set(entries
3080
+ .map((entry) => entry.table)
3081
+ .filter((value) => typeof value === 'string' && value.length > 0))].sort((left, right) => left.localeCompare(right));
3082
+ const apis = [...new Set(entries
3083
+ .map((entry) => entry.api)
3084
+ .filter((value) => typeof value === 'string' && value.length > 0))].sort((left, right) => left.localeCompare(right));
3085
+ const modules = [...new Set(entries
3086
+ .map((entry) => entry.module)
3087
+ .filter((value) => typeof value === 'string' && value.length > 0))].sort((left, right) => left.localeCompare(right));
3088
+ const targets = [...new Set(entries
3089
+ .map((entry) => entry.target)
3090
+ .filter((value) => typeof value === 'string' && value.length > 0))].sort((left, right) => left.localeCompare(right));
3091
+ return { files, tables, apis, modules, targets };
3092
+ }
3093
+ function formatValidationMessageGroup(ruleId, entries, indent = ' ') {
3094
+ const first = entries[0];
3095
+ if (!first) {
3096
+ return [];
3097
+ }
3098
+ const lines = formatFindingMessageSections(ruleId, first.message, indent);
3099
+ if (entries.length > 1) {
3100
+ lines.push(`${indent}- Occurrences: ${entries.length}`);
3101
+ }
3102
+ const details = collectValidationGroupDetails(entries);
3103
+ if (details.files.length > 0) {
3104
+ lines.push(`${indent}- Affected files:`);
3105
+ for (const file of details.files) {
3106
+ lines.push(`${indent} - ${file}`);
3107
+ }
3108
+ }
3109
+ if (details.tables.length > 0) {
3110
+ lines.push(`${indent}- Affected tables:`);
3111
+ for (const table of details.tables) {
3112
+ lines.push(`${indent} - ${table}`);
3113
+ }
3114
+ }
3115
+ if (details.apis.length > 0) {
3116
+ lines.push(`${indent}- Affected APIs:`);
3117
+ for (const api of details.apis) {
3118
+ lines.push(`${indent} - ${api}`);
3119
+ }
3120
+ }
3121
+ if (details.modules.length > 0) {
3122
+ lines.push(`${indent}- Affected modules:`);
3123
+ for (const moduleName of details.modules) {
3124
+ lines.push(`${indent} - ${moduleName}`);
3125
+ }
3126
+ }
3127
+ if (details.targets.length > 0) {
3128
+ lines.push(`${indent}- Affected targets:`);
3129
+ for (const targetName of details.targets) {
3130
+ lines.push(`${indent} - ${targetName}`);
3131
+ }
3132
+ }
3133
+ return lines;
3134
+ }
3135
+ function groupValidationResultsByRule(items) {
3136
+ const groups = new Map();
3137
+ for (const item of items) {
3138
+ const group = groups.get(item.id);
3139
+ if (group) {
3140
+ group.push(item);
3141
+ }
3142
+ else {
3143
+ groups.set(item.id, [item]);
3144
+ }
3145
+ }
3146
+ return [...groups.values()];
3147
+ }
3148
+ function buildValidationReportMarkdown(config, results, validationConfig, suppressedResults, approvedExceptions, ignoredSuppressions, suppressionEntriesFound, exceptionEntriesFound, driftSummary, dependencyGraphSummary, suppressionSummary, timestampIso, discoveredAdrs, healthTrend, healthHistoryRecent) {
3149
+ const buildDefaultWhyItMatters = (category) => {
3150
+ switch (category) {
3151
+ case 'architecture':
3152
+ return 'Architecture structure rules keep module boundaries explicit and reduce drift over time.';
3153
+ case 'dependency':
3154
+ return 'Dependency rules prevent hidden coupling and keep architecture impact predictable.';
3155
+ case 'api':
3156
+ return 'API rules keep service contracts explicit for consumers and reviewers.';
3157
+ case 'database':
3158
+ return 'Database rules protect consistency, tenant boundaries, and long-term maintainability.';
3159
+ case 'documentation':
3160
+ return 'Documentation rules preserve architectural decisions and operational context.';
3161
+ case 'security':
3162
+ return 'Security rules help keep architecture controls explicit and enforceable.';
3163
+ default:
3164
+ return 'This rule helps keep implementation aligned with declared architecture.';
3165
+ }
3166
+ };
3167
+ const buildDefaultDocumentationHint = (category) => {
3168
+ switch (category) {
3169
+ case 'architecture':
3170
+ return '.archpilot/architecture.json and docs/architecture/overview.md';
3171
+ case 'dependency':
3172
+ return '.archpilot/dependency-rules.json and module contracts';
3173
+ case 'api':
3174
+ return 'contracts/openapi.yaml';
3175
+ case 'database':
3176
+ return 'db/sql/001_init.sql';
3177
+ case 'documentation':
3178
+ return 'docs/architecture/overview.md and docs/adrs/';
3179
+ case 'security':
3180
+ return 'docs/architecture/overview.md';
3181
+ default:
3182
+ return 'docs/architecture/overview.md';
3183
+ }
3184
+ };
3185
+ const passed = results.filter((r) => r.passed);
3186
+ const errors = results.filter((r) => !r.passed && r.severity === 'error');
3187
+ const architectureReviewSignals = results.filter((r) => !r.passed && r.severity === 'warning' && isArchitectureReviewSignalRule(r.id));
3188
+ const warnings = results.filter((r) => !r.passed && r.severity === 'warning' && !isArchitectureReviewSignalRule(r.id));
3189
+ const infos = results.filter((r) => !r.passed && r.severity === 'info');
3190
+ const disabledRules = buildDisabledRuleDetails(validationConfig.disabledRuleIds);
3191
+ const sortedSuppressedResults = sortSuppressedResults(suppressedResults);
3192
+ const summary = buildValidationStatusSummary(results, disabledRules.length, sortedSuppressedResults.length, approvedExceptions.length, ignoredSuppressions.length);
3193
+ const summaryLines = [
3194
+ `- Project: ${config.projectName}`,
3195
+ `- Timestamp (UTC): ${timestampIso}`,
3196
+ `- Architecture health score: ${summary.healthScore} / 100`,
3197
+ `- Setup readiness score: ${summary.readinessScore} / 100`,
3198
+ `- ${(0, architectureHealthHistoryReporter_1.formatArchitectureHealthTrendLine)(healthTrend)}`,
3199
+ `- Total checks: ${summary.totalRulesEvaluated}`,
3200
+ `- Disabled rules: ${summary.disabled}`,
3201
+ `- Suppressions file: ${(0, validationSuppressions_1.getSuppressionsFilePathLabel)()}`,
3202
+ `- Suppression entries found: ${suppressionEntriesFound}`,
3203
+ `- Suppressed results: ${summary.suppressed}`,
3204
+ `- Exceptions file: .archpilot/exceptions.json`,
3205
+ `- Exception entries found: ${exceptionEntriesFound}`,
3206
+ `- Approved exceptions applied: ${summary.exceptions}`,
3207
+ `- Ignored suppression entries: ${summary.ignoredSuppressions}`,
3208
+ ...(suppressionSummary
3209
+ ? [
3210
+ `- Active suppressions: ${suppressionSummary.activeSuppressions}`,
3211
+ `- Expired suppressions: ${suppressionSummary.expiredSuppressions}`,
3212
+ `- Unused suppressions: ${suppressionSummary.unusedSuppressions}`,
3213
+ `- Findings suppressed in this run: ${suppressionSummary.findingsSuppressed}`,
3214
+ ]
3215
+ : []),
3216
+ `- Errors: ${summary.errors}`,
3217
+ `- Warnings: ${summary.warnings}`,
3218
+ `- Info: ${summary.info}`,
3219
+ `- Architecture findings: ${summary.qualityFindings}`,
3220
+ `- Setup gaps: ${summary.setupGaps}`,
3221
+ `- Guidance findings: ${summary.guidanceFindings}`,
3222
+ `- Passed: ${summary.passed}`,
3223
+ `- Policy: ${getValidationSeverityPolicyNote()}`,
3224
+ ];
3225
+ const categoryLabel = {
3226
+ architecture: 'Architecture',
3227
+ dependency: 'Dependency',
3228
+ api: 'API',
3229
+ database: 'Database',
3230
+ documentation: 'Documentation',
3231
+ security: 'Security',
3232
+ };
3233
+ const healthCategoryLines = Object.entries(summary.categoryScores)
3234
+ .sort(([left], [right]) => left.localeCompare(right))
3235
+ .map(([category, score]) => `- ${categoryLabel[category] ?? category}: ${score} / 100`);
3236
+ const normalizedFindingLines = [
3237
+ `- Raw findings: ${summary.rawFindingCount}`,
3238
+ `- Normalized findings: ${summary.normalizedFindingCount}`,
3239
+ `- Duplicate reduction: ${summary.duplicateReductionCount}`,
3240
+ ];
3241
+ const architectureSignalSummaryLines = [
3242
+ `- Heuristic architecture smell findings: ${summary.architectureSmellSummary.count}`,
3243
+ `- Explanation: ${summary.architectureSmellSummary.explanation}`,
3244
+ ...(summary.architectureSmellSummary.ruleFamilies.length === 0
3245
+ ? ['- Families: (none)']
3246
+ : summary.architectureSmellSummary.ruleFamilies.map((family) => `- ${family.family}: ${family.count} (${family.rules
3247
+ .map((rule) => `${rule.ruleId}=${rule.count}`)
3248
+ .join(', ')})`)),
3249
+ ];
3250
+ const scoreExplanationLines = [
3251
+ `- Errors: ${summary.scoreExplanation.errors}`,
3252
+ `- Warnings: ${summary.scoreExplanation.warnings}`,
3253
+ `- Heuristic warnings: ${summary.scoreExplanation.heuristicWarnings}`,
3254
+ `- Weighted base health: ${summary.scoreExplanation.weightedBaseScore} / 100`,
3255
+ `- Applicable category weights: ${summary.scoreExplanation.applicableCategoryScores
3256
+ .map((entry) => `${categoryLabel[entry.category] ?? entry.category}=${entry.score} x ${Math.round(entry.weight * 100)}%`)
3257
+ .join(', ') || '(none)'}`,
3258
+ `- Highest-impact categories: ${summary.scoreExplanation.highestImpactCategories
3259
+ .map((entry) => `${categoryLabel[entry.category] ?? entry.category}=${entry.score}`)
3260
+ .join(', ') || '(none)'}`,
3261
+ `- Why score changed: ${summary.scoreExplanation.whyScoreChanged}`,
3262
+ ];
3263
+ const healthHistoryLines = healthHistoryRecent.length === 0
3264
+ ? ['- (none)']
3265
+ : [...healthHistoryRecent]
3266
+ .sort((left, right) => left.generatedAtUtc.localeCompare(right.generatedAtUtc))
3267
+ .map((snapshot) => `- ${snapshot.generatedAtUtc} | ${snapshot.overallScore}`);
3268
+ const readinessCategoryLines = Object.entries(summary.readinessCategoryScores)
3269
+ .sort(([left], [right]) => left.localeCompare(right))
3270
+ .map(([category, score]) => `- ${categoryLabel[category] ?? category}: ${score} / 100`);
3271
+ const formatResultLines = (items) => {
3272
+ if (items.length === 0) {
3273
+ return ['- (none)'];
3274
+ }
3275
+ const lines = [];
3276
+ for (const group of groupValidationResultsByRule(items)) {
3277
+ const first = group[0];
3278
+ if (!first) {
3279
+ continue;
3280
+ }
3281
+ const meta = getValidationRuleMetadataById(first.id);
3282
+ const classification = classifyValidationResult(first);
3283
+ const relatedAdrs = [...(meta.relatedAdrs ?? [])]
3284
+ .map((adrId) => adrId.trim().toUpperCase())
3285
+ .filter((adrId) => adrId.length > 0)
3286
+ .sort((left, right) => left.localeCompare(right))
3287
+ .map((adrId) => {
3288
+ const found = discoveredAdrs.find((discoveredAdr) => discoveredAdr.id.toUpperCase() === adrId);
3289
+ return {
3290
+ id: adrId,
3291
+ title: found?.title ?? '(not found)',
3292
+ filePath: found?.filePath,
3293
+ found: !!found,
3294
+ };
3295
+ });
3296
+ lines.push(`- [${meta.id}] ${meta.title}${group.length > 1 ? ` (${group.length} findings)` : ''}`);
3297
+ lines.push(` - Category: ${categoryLabel[meta.category] ?? meta.category}`);
3298
+ if (first.passed) {
3299
+ lines.push(' - Status: Passed');
3300
+ }
3301
+ else {
3302
+ lines.push(` - Severity: ${first.severity}`);
3303
+ lines.push(` - Classification: ${isArchitectureReviewSignalRule(first.id)
3304
+ ? 'Architecture Review Signal'
3305
+ : classification.kind}`);
3306
+ }
3307
+ const groupedMessages = new Map();
3308
+ for (const item of group) {
3309
+ groupedMessages.set(item.message, (groupedMessages.get(item.message) ?? 0) + 1);
3310
+ }
3311
+ if (groupedMessages.size === 1) {
3312
+ lines.push(...formatValidationMessageGroup(first.id, group));
3313
+ }
3314
+ else {
3315
+ lines.push(' - Findings:');
3316
+ for (const detailMessage of [...groupedMessages.keys()].sort((left, right) => left.localeCompare(right))) {
3317
+ lines.push(...formatValidationMessageGroup(first.id, group.filter((item) => item.message === detailMessage), ' '));
3318
+ }
3319
+ }
3320
+ if (!first.passed) {
3321
+ lines.push(` - Recommended fix: ${meta.recommendedFix}`);
3322
+ lines.push(` - Why this matters: ${meta.whyItMatters ?? buildDefaultWhyItMatters(meta.category)}`);
3323
+ lines.push(` - How to fix: ${meta.howToFix ?? meta.recommendedFix}`);
3324
+ lines.push(` - Example: ${meta.exampleFix ?? 'Update the affected architecture artifact or implementation path so the rule expectation is met.'}`);
3325
+ lines.push(` - Documentation hint: ${meta.documentationHint ?? buildDefaultDocumentationHint(meta.category)}`);
3326
+ if (relatedAdrs.length > 0) {
3327
+ lines.push(' - Related ADRs:');
3328
+ for (const adr of relatedAdrs) {
3329
+ lines.push(` - ${adr.id} ${adr.title}${adr.filePath ? ` (${adr.filePath})` : ''}${adr.found ? '' : ' (not found)'}`);
3330
+ }
3331
+ }
3332
+ if (group.some((item) => item.quickFix?.available)) {
3333
+ const quickFix = group.find((item) => item.quickFix?.available)?.quickFix;
3334
+ lines.push(' - Quick Fix:');
3335
+ lines.push(' - Available: Yes');
3336
+ if (quickFix) {
3337
+ lines.push(` - Type: ${quickFix.type}`);
3338
+ lines.push(` - Description: ${quickFix.description}`);
3339
+ }
3340
+ }
3341
+ }
3342
+ }
3343
+ return lines;
3344
+ };
3345
+ const passedLines = formatResultLines(passed);
3346
+ const errorLines = formatResultLines(errors);
3347
+ const architectureReviewSignalLines = formatResultLines(architectureReviewSignals);
3348
+ const warningLines = formatResultLines(warnings);
3349
+ const infoLines = formatResultLines(infos);
3350
+ const disabledRuleLines = disabledRules.length === 0
3351
+ ? ['- (none)']
3352
+ : disabledRules.map((rule) => `- ${rule.id} | ${rule.title}`);
3353
+ const appliedOverrideLines = validationConfig.appliedSeverityOverrides.length === 0
3354
+ ? ['- (none)']
3355
+ : validationConfig.appliedSeverityOverrides.map((override) => `- ${override.id}: ${override.from} -> ${override.to}`);
3356
+ const ignoredConfigLines = validationConfig.ignoredEntries.length === 0
3357
+ ? ['- (none)']
3358
+ : validationConfig.ignoredEntries.map((entry) => {
3359
+ const requestedSeverity = entry.requestedSeverity
3360
+ ? ` | requested=${entry.requestedSeverity}`
3361
+ : '';
3362
+ return `- ${entry.kind} | ${entry.id}${requestedSeverity} | ${entry.reason}`;
3363
+ });
3364
+ const suppressedResultLines = sortedSuppressedResults.length === 0
3365
+ ? ['- (none)']
3366
+ : sortedSuppressedResults.map((result) => {
3367
+ const lineSuffix = result.line !== undefined ? `:${result.line}` : '';
3368
+ const reasonLine = result.suppressionReason
3369
+ ? `\n - Reason: ${result.suppressionReason}`
3370
+ : '';
3371
+ return `- [${result.id}] suppressed for ${result.filePath}${lineSuffix}\n - Matched suppression path: ${result.suppressionPath}${reasonLine}`;
3372
+ });
3373
+ const approvedExceptionLines = approvedExceptions.length === 0
3374
+ ? ['- (none)']
3375
+ : [...approvedExceptions]
3376
+ .sort((left, right) => {
3377
+ const ruleCompare = left.ruleId.localeCompare(right.ruleId);
3378
+ if (ruleCompare !== 0) {
3379
+ return ruleCompare;
3380
+ }
3381
+ const moduleCompare = (left.module ?? '').localeCompare(right.module ?? '');
3382
+ if (moduleCompare !== 0) {
3383
+ return moduleCompare;
3384
+ }
3385
+ const targetCompare = (left.target ?? '').localeCompare(right.target ?? '');
3386
+ if (targetCompare !== 0) {
3387
+ return targetCompare;
3388
+ }
3389
+ const filePathCompare = (left.filePath ?? '').localeCompare(right.filePath ?? '');
3390
+ if (filePathCompare !== 0) {
3391
+ return filePathCompare;
3392
+ }
3393
+ const tableCompare = (left.table ?? '').localeCompare(right.table ?? '');
3394
+ if (tableCompare !== 0) {
3395
+ return tableCompare;
3396
+ }
3397
+ const apiCompare = (left.api ?? '').localeCompare(right.api ?? '');
3398
+ if (apiCompare !== 0) {
3399
+ return apiCompare;
3400
+ }
3401
+ return left.reason.localeCompare(right.reason);
3402
+ })
3403
+ .map((entry) => {
3404
+ return [
3405
+ `- Rule ID: ${entry.ruleId}`,
3406
+ ` - Module: ${entry.module ?? '(any)'}`,
3407
+ ` - Target: ${entry.target ?? '(any)'}`,
3408
+ ` - File Path: ${entry.filePath ?? '(any)'}`,
3409
+ ` - Table: ${entry.table ?? '(any)'}`,
3410
+ ` - API: ${entry.api ?? '(any)'}`,
3411
+ ` - Reason: ${entry.reason}`,
3412
+ ` - Approved By: ${entry.approvedBy}`,
3413
+ ` - Expires At: ${entry.expiresAt ?? '(none)'}`,
3414
+ ].join('\n');
3415
+ });
3416
+ const ignoredSuppressionLines = ignoredSuppressions.length === 0
3417
+ ? ['- (none)']
3418
+ : ignoredSuppressions.map((entry) => {
3419
+ const ruleId = entry.ruleId ?? '(missing ruleId)';
3420
+ const entryPath = entry.path ?? '(missing path)';
3421
+ return `- #${entry.index} | ${ruleId} | ${entryPath} | ${entry.reason ?? 'ignored'}`;
3422
+ });
3423
+ const formatDependencyList = (values) => values.length === 0 ? 'none' : values.join(', ');
3424
+ const dependencyGraphLines = dependencyGraphSummary.modules.length === 0
3425
+ ? ['- (none)']
3426
+ : dependencyGraphSummary.modules.flatMap((entry) => [
3427
+ `- ${entry.module}`,
3428
+ ` - Contract: ${entry.hasContract ? 'present' : 'missing or invalid'}`,
3429
+ ` - Declared: ${formatDependencyList(entry.declaredDependencies)}`,
3430
+ ` - Actual: ${formatDependencyList(entry.actualDependencies)}`,
3431
+ ` - Unused declared: ${formatDependencyList(entry.unusedDeclaredDependencies)}`,
3432
+ ` - Undeclared actual: ${formatDependencyList(entry.undeclaredActualDependencies)}`,
3433
+ ` - Inbound declared from: ${formatDependencyList(entry.inboundDeclaredFrom)}`,
3434
+ ` - Inbound actual from: ${formatDependencyList(entry.inboundActualFrom)}`,
3435
+ ]);
3436
+ const suggestedNextActions = errors.length === 0 && warnings.length === 0
3437
+ ? [
3438
+ '- Keep configuration and documentation in sync as the system evolves.',
3439
+ '- Add additional checks as new architecture decisions are made.',
3440
+ ]
3441
+ : errors.length > 0
3442
+ ? [
3443
+ '- Resolve all error-level architecture, API, and database violations before considering the system aligned with the selected architecture.',
3444
+ '- After errors are resolved, address warning-level documentation and advisory findings.',
3445
+ '- Consider adding automated tests or CI checks that enforce these rules.',
3446
+ ]
3447
+ : [
3448
+ '- Review each warning and decide whether the configuration or implementation should change.',
3449
+ '- Update documentation (ADRs, overview, contracts) to match the current system.',
3450
+ '- Consider adding automated tests or CI checks that enforce these rules.',
3451
+ ];
3452
+ if (summary.setupGaps > 0) {
3453
+ suggestedNextActions.unshift('- Run `archpilot bootstrap --with-contracts` to close setup gaps quickly.');
3454
+ }
3455
+ const catalogLines = getAllValidationRuleMetadata().map((meta) => `- ${meta.id} | ${meta.title} | ${meta.category} | ${meta.defaultSeverity}`);
3456
+ return `# ArchPilot Architecture Validation Report
3457
+
3458
+ ## Architecture Health Score
3459
+ - Overall Score: ${summary.healthScore} / 100
3460
+ - ${(0, architectureHealthHistoryReporter_1.formatArchitectureHealthTrendLine)(healthTrend)}
3461
+ - Total Checks: ${summary.totalRulesEvaluated}
3462
+ - Errors: ${summary.errors}
3463
+ - Warnings: ${summary.warnings}
3464
+ - Passed: ${summary.passed}
3465
+
3466
+ ## Score Explanation
3467
+ ${scoreExplanationLines.join('\n')}
3468
+
3469
+ ## Largest Score Contributors
3470
+ ${summary.scoreExplanation.largestScoreContributors.length === 0
3471
+ ? '- (none)'
3472
+ : summary.scoreExplanation.largestScoreContributors.map((entry) => `- ${entry}`).join('\n')}
3473
+
3474
+ ## Architecture Drift
3475
+ ${driftSummary.message}
3476
+
3477
+ ## Category Breakdown
3478
+ ${healthCategoryLines.join('\n')}
3479
+
3480
+ ## Setup Readiness Category Breakdown
3481
+ ${readinessCategoryLines.join('\n')}
3482
+
3483
+ ## Recent Health Snapshots
3484
+ ${healthHistoryLines.join('\n')}
3485
+
3486
+ ## Summary
3487
+ ${summaryLines.join('\n')}
3488
+
3489
+ ## Normalized Finding Context
3490
+ ${normalizedFindingLines.join('\n')}
3491
+
3492
+ ## Architecture Review Signals
3493
+ ${architectureSignalSummaryLines.join('\n')}
3494
+ ${architectureReviewSignalLines.join('\n')}
3495
+
3496
+ ## Errors
3497
+ ${errorLines.join('\n')}
3498
+
3499
+ ## Warnings
3500
+ ${warningLines.join('\n')}
3501
+
3502
+ ## Informational Checks
3503
+ ${infoLines.join('\n')}
3504
+
3505
+ ## Passed Checks
3506
+ ${passedLines.join('\n')}
3507
+
3508
+ ## Module Dependency Summary
3509
+ ${dependencyGraphLines.join('\n')}
3510
+
3511
+ ## Disabled Rules
3512
+ ${disabledRuleLines.join('\n')}
3513
+
3514
+ ## Severity Overrides Applied
3515
+ ${appliedOverrideLines.join('\n')}
3516
+
3517
+ ## Ignored Config Entries
3518
+ ${ignoredConfigLines.join('\n')}
3519
+
3520
+ ## Suppressed Results
3521
+ ${suppressedResultLines.join('\n')}
3522
+
3523
+ ## Approved Exceptions
3524
+ ${approvedExceptionLines.join('\n')}
3525
+
3526
+ ## Ignored Suppression Entries
3527
+ ${ignoredSuppressionLines.join('\n')}
3528
+
3529
+ ## Suggested Next Actions
3530
+ ${suggestedNextActions.map((line) => `${line}`).join('\n')}
3531
+
3532
+ ## Rule Catalog Reference
3533
+ ${catalogLines.join('\n')}
3534
+ `;
3535
+ }
3536
+ async function writeValidationReport(workspaceRoot, config, results, validationConfig, suppressedResults, approvedExceptions, ignoredSuppressions, suppressionEntriesFound, exceptionEntriesFound, driftSummary, dependencyGraphSummary, suppressionSummary, suppressionEntries, adrSummary, policyStatus, options) {
3537
+ const timestampIso = new Date().toISOString();
3538
+ const discoveredAdrs = await (0, validationProfiling_1.profileValidationPhase)('report generation ADR discovery', () => (0, discoverAdrs_1.discoverAdrs)(workspaceRoot));
3539
+ const reportDir = path.join(workspaceRoot, '.archpilot');
3540
+ const reportPath = path.join(reportDir, 'validation-report.md');
3541
+ const statusPath = path.join(reportDir, 'validation-status.json');
3542
+ const statusExport = await (0, validationProfiling_1.profileValidationPhase)('scoring', () => Promise.resolve(buildValidationStatusExport(config, results, validationConfig, suppressedResults, approvedExceptions, ignoredSuppressions, suppressionEntriesFound, exceptionEntriesFound, driftSummary, dependencyGraphSummary, suppressionSummary, suppressionEntries, policyStatus, adrSummary)), (result) => [
3543
+ { name: 'results', value: result.results.length },
3544
+ { name: 'healthScore', value: result.summary.healthScore },
3545
+ ]);
3546
+ statusExport.xArchPilot = await (0, validationProfiling_1.profileValidationPhase)('managed metadata generation', () => (0, managedArtifacts_1.buildManagedMetadata)('validation-status', timestampIso));
3547
+ let healthTrend = {
3548
+ currentScore: statusExport.summary.healthScore,
3549
+ trendDirection: 'no-history',
3550
+ };
3551
+ let driftComparison = {
3552
+ hasPreviousSnapshot: false,
3553
+ newViolations: [],
3554
+ resolvedViolations: [],
3555
+ };
3556
+ let healthHistoryRecent = [];
3557
+ const previousDriftSnapshot = await (0, validationProfiling_1.profileValidationPhase)('history/artifact read', () => (0, driftSnapshotLoader_1.loadLatestDriftSnapshot)(workspaceRoot));
3558
+ const currentDriftSnapshot = buildDriftSnapshot(timestampIso, statusExport);
3559
+ driftComparison = (0, driftComparator_1.compareDriftSnapshots)(previousDriftSnapshot, currentDriftSnapshot);
3560
+ healthTrend = (0, architectureHealthHistory_1.buildArchitectureHealthTrendFromSnapshotScores)(statusExport.summary.healthScore, driftComparison.previousScore);
3561
+ const snapshot = (0, architectureHealthHistory_1.createArchitectureHealthSnapshot)({
3562
+ generatedAtUtc: timestampIso,
3563
+ overallScore: statusExport.summary.healthScore,
3564
+ categoryScores: statusExport.summary.categoryScores,
3565
+ totalChecks: statusExport.summary.totalRulesEvaluated,
3566
+ errors: statusExport.summary.errors,
3567
+ warnings: statusExport.summary.warnings,
3568
+ info: statusExport.summary.info,
3569
+ passed: statusExport.summary.passed,
3570
+ });
3571
+ try {
3572
+ const appended = await (0, validationProfiling_1.profileValidationPhase)('badge/history/artifact writing health history', () => (0, architectureHealthHistory_1.appendArchitectureHealthSnapshot)(workspaceRoot, snapshot, {
3573
+ maxSnapshots: 100,
3574
+ }));
3575
+ healthHistoryRecent = [...appended.history.snapshots]
3576
+ .sort((left, right) => left.generatedAtUtc.localeCompare(right.generatedAtUtc))
3577
+ .slice(-5);
3578
+ }
3579
+ catch {
3580
+ healthHistoryRecent = [snapshot];
3581
+ }
3582
+ try {
3583
+ await (0, validationProfiling_1.profileValidationPhase)('badge/history/artifact writing drift snapshot', () => (0, driftSnapshotWriter_1.writeLatestDriftSnapshot)(workspaceRoot, currentDriftSnapshot));
3584
+ }
3585
+ catch {
3586
+ // Ignore snapshot persistence failures so validation behavior remains stable.
3587
+ }
3588
+ statusExport.summary = {
3589
+ ...statusExport.summary,
3590
+ healthTrend,
3591
+ healthHistoryRecent,
3592
+ };
3593
+ statusExport.driftComparison = driftComparison;
3594
+ try {
3595
+ const violationCount = results.filter((result) => {
3596
+ if (result.passed) {
3597
+ return false;
3598
+ }
3599
+ return classifyValidationResult(result).kind === 'violation';
3600
+ }).length;
3601
+ const changeRisk = (0, changeRiskAssessment_1.assessChangeRisk)({
3602
+ errorCount: statusExport.summary.errors,
3603
+ warningCount: statusExport.summary.warnings,
3604
+ crossModuleDependencyViolations: results.filter((result) => !result.passed && result.id.startsWith('AP-DEP-')).length,
3605
+ diffContextAvailable: false,
3606
+ });
3607
+ const summaryRiskLevel = toTimelineRiskLevelFromValidationSummary(statusExport.summary);
3608
+ const snapshot = (0, architectureDriftTimeline_1.createArchitectureDriftTimelineSnapshot)({
3609
+ timestampUtcIso: timestampIso,
3610
+ architectureScore: statusExport.summary.healthScore,
3611
+ setupReadinessScore: statusExport.summary.readinessScore,
3612
+ moduleCount: statusExport.dependencyGraphSummary.modules.length,
3613
+ edgeCount: statusExport.dependencyGraphSummary.modules.reduce((total, moduleEntry) => total + moduleEntry.actualDependencies.length, 0),
3614
+ errorCount: statusExport.summary.errors,
3615
+ warningCount: statusExport.summary.warnings,
3616
+ infoCount: statusExport.summary.info,
3617
+ passedCount: statusExport.summary.passed,
3618
+ driftDetected: statusExport.drift.detected,
3619
+ violationCount,
3620
+ changeRiskLevel: changeRisk.riskLevel.toUpperCase(),
3621
+ governanceRiskLevel: summaryRiskLevel,
3622
+ enforcementOutcome: toTimelineEnforcementOutcomeFromValidationSummary(statusExport.summary),
3623
+ });
3624
+ await (0, validationProfiling_1.profileValidationPhase)('badge/history/artifact writing drift timeline', () => (0, architectureDriftTimeline_1.writeArchitectureDriftTimelineSnapshot)(workspaceRoot, snapshot));
3625
+ }
3626
+ catch {
3627
+ // Ignore timeline snapshot failures so validation execution remains stable.
3628
+ }
3629
+ const report = await (0, validationProfiling_1.profileValidationPhase)('report generation markdown render', () => Promise.resolve(buildValidationReportMarkdown(config, results, validationConfig, suppressedResults, approvedExceptions, ignoredSuppressions, suppressionEntriesFound, exceptionEntriesFound, driftSummary, dependencyGraphSummary, suppressionSummary, timestampIso, discoveredAdrs, healthTrend, healthHistoryRecent)), (result) => [{ name: 'bytes', value: result.length }]);
3630
+ await (0, validationProfiling_1.profileValidationPhase)('badge/history/artifact writing reports badge status', async () => {
3631
+ await node_fs_1.promises.mkdir(reportDir, { recursive: true });
3632
+ await Promise.all([
3633
+ node_fs_1.promises.writeFile(reportPath, report, { encoding: 'utf8' }),
3634
+ node_fs_1.promises.writeFile(statusPath, `${JSON.stringify(statusExport, null, 2)}\n`, { encoding: 'utf8' }),
3635
+ (0, architectureScoreBadge_1.writeArchitectureScoreBadge)(workspaceRoot, statusExport.summary.healthScore),
3636
+ ]);
3637
+ });
3638
+ return {
3639
+ reportPath,
3640
+ statusPath,
3641
+ status: statusExport,
3642
+ };
3643
+ }
3644
+ function filterResultsByModuleScope(results, moduleScope) {
3645
+ if (moduleScope.length === 0) {
3646
+ return results;
3647
+ }
3648
+ const scopeSet = new Set(moduleScope);
3649
+ const modulePatterns = [...scopeSet].map((moduleId) => ({
3650
+ moduleId,
3651
+ pathNeedle: `/modules/${moduleId.toLowerCase()}/`,
3652
+ srcPathNeedle: `/src/${moduleId.toLowerCase()}/`,
3653
+ contractNeedle: `.archpilot/contracts/${moduleId.toLowerCase()}.contract.json`,
3654
+ quotedNeedle: `'${moduleId.toLowerCase()}'`,
3655
+ }));
3656
+ return results.filter((result) => {
3657
+ const sourceFile = (result.sourceFile ?? '').replace(/\\/g, '/').toLowerCase();
3658
+ const message = result.message.toLowerCase();
3659
+ for (const pattern of modulePatterns) {
3660
+ if (sourceFile.includes(pattern.pathNeedle) ||
3661
+ sourceFile.includes(pattern.srcPathNeedle) ||
3662
+ sourceFile.includes(pattern.contractNeedle) ||
3663
+ message.includes(pattern.pathNeedle) ||
3664
+ message.includes(pattern.srcPathNeedle) ||
3665
+ message.includes(pattern.contractNeedle) ||
3666
+ message.includes(pattern.quotedNeedle)) {
3667
+ return true;
3668
+ }
3669
+ }
3670
+ return false;
3671
+ });
3672
+ }
3673
+ function filterDependencyGraphSummaryByModuleScope(dependencyGraphSummary, moduleScope) {
3674
+ if (moduleScope.length === 0) {
3675
+ return dependencyGraphSummary;
3676
+ }
3677
+ const scopeSet = new Set(moduleScope);
3678
+ const scopedModules = dependencyGraphSummary.modules
3679
+ .filter((moduleSummary) => scopeSet.has(moduleSummary.module))
3680
+ .map((moduleSummary) => ({
3681
+ ...moduleSummary,
3682
+ declaredDependencies: moduleSummary.declaredDependencies.filter((dependency) => scopeSet.has(dependency)),
3683
+ actualDependencies: moduleSummary.actualDependencies.filter((dependency) => scopeSet.has(dependency)),
3684
+ inboundDeclaredFrom: moduleSummary.inboundDeclaredFrom.filter((dependency) => scopeSet.has(dependency)),
3685
+ inboundActualFrom: moduleSummary.inboundActualFrom.filter((dependency) => scopeSet.has(dependency)),
3686
+ unusedDeclaredDependencies: moduleSummary.unusedDeclaredDependencies.filter((dependency) => scopeSet.has(dependency)),
3687
+ undeclaredActualDependencies: moduleSummary.undeclaredActualDependencies.filter((dependency) => scopeSet.has(dependency)),
3688
+ }));
3689
+ return {
3690
+ modules: scopedModules,
3691
+ hierarchyEdges: dependencyGraphSummary.hierarchyEdges.filter((edge) => scopeSet.has(edge.parentModule) && scopeSet.has(edge.childModule)),
3692
+ };
3693
+ }
3694
+ async function runArchitectureValidationForWorkspace(workspaceRoot, options) {
3695
+ const phase = async (label, run) => {
3696
+ const started = Date.now();
3697
+ (0, operationalLogging_1.emitOperationalMessage)(options?.onOperationalMessage, 'info', `${label}...`);
3698
+ try {
3699
+ return await run();
3700
+ }
3701
+ finally {
3702
+ (0, validationProfiling_1.emitValidationProfile)(label, Date.now() - started);
3703
+ (0, operationalLogging_1.emitOperationalMessage)(options?.onOperationalMessage, 'info', `${label} completed in ${Date.now() - started}ms.`);
3704
+ }
3705
+ };
3706
+ const config = await phase('Loading architecture configuration', () => readArchitectureConfig(workspaceRoot));
3707
+ if (!config) {
3708
+ throw new Error('ArchPilot is not initialized in this workspace.');
3709
+ }
3710
+ const architectureContract = await phase('Loading architecture contract', () => (0, architectureInit_1.loadArchitectureContract)(workspaceRoot));
3711
+ if (!architectureContract) {
3712
+ throw new Error('ArchPilot is not initialized in this workspace.');
3713
+ }
3714
+ const [validationConfigLoad, projectConfigLoad, projectIdentityLoad, adrValidation,] = await phase('Loading validation inputs', () => Promise.all([
3715
+ readValidationConfig(workspaceRoot),
3716
+ (0, projectConfig_1.readProjectConfig)(workspaceRoot),
3717
+ (0, projectIdentity_1.readProjectIdentity)(workspaceRoot),
3718
+ (0, adrValidation_1.validateAdrEnforcement)(workspaceRoot, config),
3719
+ ]));
3720
+ const policyStatus = {
3721
+ remotePolicyLoaded: false,
3722
+ };
3723
+ let remotePolicy;
3724
+ if (projectConfigLoad.config?.policySource) {
3725
+ policyStatus.policySource = projectConfigLoad.config.policySource;
3726
+ const loadedRemotePolicy = await (0, remotePolicyLoader_1.loadRemotePolicyWithStatus)(projectConfigLoad.config.policySource, { onOperationalMessage: options?.onOperationalMessage });
3727
+ remotePolicy = loadedRemotePolicy.policy;
3728
+ policyStatus.remotePolicyLoaded = !!remotePolicy;
3729
+ policyStatus.message = loadedRemotePolicy.message;
3730
+ }
3731
+ const localDependencyRulesRaw = await phase('Loading dependency rule configuration', () => readDependencyRulesRawConfig(workspaceRoot));
3732
+ const effectivePolicy = (0, effectivePolicyResolver_1.resolveEffectivePolicy)({
3733
+ localValidationConfig: validationConfigLoad.config,
3734
+ localDependencyRulesRaw,
3735
+ remotePolicy,
3736
+ knownRuleIds: getAllValidationRuleMetadata().map((meta) => meta.id),
3737
+ });
3738
+ const validationConfig = buildEffectiveValidationConfig(effectivePolicy.effectiveValidationConfig);
3739
+ const knownRuleIds = new Set(getAllValidationRuleMetadata().map((meta) => meta.id));
3740
+ const suppressionsConfig = await phase('Loading suppressions', () => (0, validationSuppressions_1.readValidationSuppressions)(workspaceRoot, knownRuleIds));
3741
+ let hasArchitectureModules = hasConfiguredArchitectureModules(architectureContract);
3742
+ const results = [];
3743
+ results.push(...validationConfigLoad.configResults);
3744
+ if (projectConfigLoad.invalidJson) {
3745
+ results.push({
3746
+ id: exports.ValidationRuleIds.ARCH_PROJECT_CONFIGURATION_VALID,
3747
+ severity: 'error',
3748
+ passed: false,
3749
+ message: projectConfigLoad.errorMessage ??
3750
+ 'ArchPilot project config is invalid. Please fix .archpilot/config.json.',
3751
+ });
3752
+ }
3753
+ if (projectIdentityLoad.invalidJson) {
3754
+ results.push({
3755
+ id: exports.ValidationRuleIds.ARCH_PROJECT_IDENTITY_CONFIGURATION_VALID,
3756
+ severity: 'error',
3757
+ passed: false,
3758
+ message: projectIdentityLoad.errorMessage ??
3759
+ 'ArchPilot project identity config is invalid. Please fix .archpilot/project.json.',
3760
+ });
3761
+ }
3762
+ results.push(...suppressionsConfig.configResults);
3763
+ const configuredModulesRoot = await phase('Discovering project modules', () => getConfiguredModulesRoot(workspaceRoot, architectureContract));
3764
+ let precomputedDependencyImports;
3765
+ const dependencyGraphSummary = await phase('Building dependency graph', () => (0, dependencyValidation_1.buildDependencyGraphSummary)(workspaceRoot, architectureContract, {
3766
+ modulesRootRelativePath: configuredModulesRoot,
3767
+ onCollectedImports: (imports) => {
3768
+ precomputedDependencyImports = imports;
3769
+ },
3770
+ }));
3771
+ hasArchitectureModules =
3772
+ hasArchitectureModules || dependencyGraphSummary.modules.length > 0;
3773
+ (0, operationalLogging_1.emitOperationalMessage)(options?.onOperationalMessage, 'info', `Dependency scan summary: modules=${dependencyGraphSummary.modules.length}, importEdges=${dependencyGraphSummary.modules.reduce((total, moduleEntry) => total + moduleEntry.actualDependencies.length, 0)}, ignoredDefaults=node_modules,.git,.archpilot,dist,build,out,coverage,fixtures.`);
3774
+ const sourceInventory = (0, validationSourceInventory_1.createValidationSourceInventory)(workspaceRoot);
3775
+ const astCache = (0, validationTypeScriptAstCache_1.createValidationTypeScriptAstCache)();
3776
+ const ruleExecutionResults = await phase('Executing validation rules', () => Promise.all([
3777
+ (0, validationProfiling_1.profileValidationPhase)('implementation profile analyzer', () => validateImplementationProfile(config, validationConfig)),
3778
+ (0, validationProfiling_1.profileValidationPhase)('API style analyzer', () => validateApiStyle(workspaceRoot, architectureContract, config, validationConfig)),
3779
+ (0, validationProfiling_1.profileValidationPhase)('module contract registry analyzer', () => hasArchitectureModules
3780
+ ? validateModuleContractRegistryIntegrity(workspaceRoot, architectureContract, validationConfig)
3781
+ : Promise.resolve([])),
3782
+ (0, validationProfiling_1.profileValidationPhase)('module dependency analyzer', () => validateModuleDependencies(workspaceRoot, architectureContract, validationConfig, options?.onDependencyConfigError, projectConfigLoad.config?.policySource
3783
+ ? effectivePolicy.effectiveDependencyRulesConfig
3784
+ : undefined, precomputedDependencyImports, hasArchitectureModules)),
3785
+ (0, validationProfiling_1.profileValidationPhase)('tenant model analyzer', () => validateTenantModel(workspaceRoot, architectureContract, config, validationConfig)),
3786
+ (0, validationProfiling_1.profileValidationPhase)('RBAC analyzer', () => validateRbac(workspaceRoot, architectureContract, config, validationConfig)),
3787
+ (0, validationProfiling_1.profileValidationPhase)('architecture style analyzer', () => validateArchitectureStyle(workspaceRoot, architectureContract, validationConfig)),
3788
+ (0, validationProfiling_1.profileValidationPhase)('database artifact analyzer', () => validateDatabaseArtifacts(workspaceRoot, architectureContract, config, validationConfig)),
3789
+ (0, validationProfiling_1.profileValidationPhase)('data/query risk analyzer', () => (0, dataQueryRiskDetection_1.detectDataQueryRiskFindings)({
3790
+ workspaceRoot,
3791
+ config,
3792
+ contract: architectureContract,
3793
+ disabledRuleIds: validationConfig.disabledRuleIdSet,
3794
+ sourceInventory,
3795
+ astCache,
3796
+ }), (result) => [{ name: 'findings', value: result.length }]),
3797
+ (0, validationProfiling_1.profileValidationPhase)('AP-API analyzer', () => (0, apiDesignAnalysis_1.detectApiDesignFindings)({
3798
+ workspaceRoot,
3799
+ config,
3800
+ contract: architectureContract,
3801
+ disabledRuleIds: validationConfig.disabledRuleIdSet,
3802
+ sourceInventory,
3803
+ }), (result) => [{ name: 'findings', value: result.length }]),
3804
+ (0, validationProfiling_1.profileValidationPhase)('AP-EVT analyzer', () => (0, eventArchitectureAnalysis_1.detectEventArchitectureFindings)({
3805
+ workspaceRoot,
3806
+ contract: architectureContract,
3807
+ disabledRuleIds: validationConfig.disabledRuleIdSet,
3808
+ sourceInventory,
3809
+ astCache,
3810
+ }), (result) => [{ name: 'findings', value: result.length }]),
3811
+ (0, validationProfiling_1.profileValidationPhase)('AP-APP analyzer', () => (0, applicationLayeringAnalysis_1.detectApplicationLayeringFindings)({
3812
+ workspaceRoot,
3813
+ contract: architectureContract,
3814
+ disabledRuleIds: validationConfig.disabledRuleIdSet,
3815
+ sourceInventory,
3816
+ astCache,
3817
+ }), (result) => [{ name: 'findings', value: result.length }]),
3818
+ (0, validationProfiling_1.profileValidationPhase)('AP-AUTH/AP-TEN analyzer', () => (0, authorizationTenantAnalysis_1.detectAuthorizationTenantFindings)({
3819
+ workspaceRoot,
3820
+ config,
3821
+ contract: architectureContract,
3822
+ disabledRuleIds: validationConfig.disabledRuleIdSet,
3823
+ sourceInventory,
3824
+ astCache,
3825
+ }), (result) => [{ name: 'findings', value: result.length }]),
3826
+ (0, validationProfiling_1.profileValidationPhase)('AP-TXN/AP-DOM analyzer', () => (0, transactionDomainAnalysis_1.detectTransactionDomainFindings)({
3827
+ workspaceRoot,
3828
+ config,
3829
+ contract: architectureContract,
3830
+ disabledRuleIds: validationConfig.disabledRuleIdSet,
3831
+ sourceInventory,
3832
+ astCache,
3833
+ }), (result) => [{ name: 'findings', value: result.length }]),
3834
+ ]));
3835
+ results.push(...ruleExecutionResults.flat(), ...adrValidation.results);
3836
+ if (hasArchitectureModules) {
3837
+ results.push(...(await phase('Analyzing dependency graph rules', () => validateDependencyLayerDirection(workspaceRoot, dependencyGraphSummary, validationConfig))), ...validateTransitiveModuleDependencyCycles(dependencyGraphSummary, validationConfig), ...validateHighInboundDependencyCount(architectureContract, dependencyGraphSummary, validationConfig), ...(await validateDependencyGraphIsolation(workspaceRoot, architectureContract, dependencyGraphSummary, validationConfig)));
3838
+ }
3839
+ const scopeModules = options?.moduleScope
3840
+ ? [...new Set(options.moduleScope)].sort((left, right) => left.localeCompare(right))
3841
+ : [];
3842
+ const applicableResults = applyRuleApplicability(results, architectureContract, validationConfig, hasArchitectureModules);
3843
+ const effectiveResults = applyEffectiveSeverities(applicableResults, validationConfig.config);
3844
+ const scopedResults = filterResultsByModuleScope(effectiveResults, scopeModules);
3845
+ const scopedResultsWithQuickFix = (0, quickFixProviders_1.attachQuickFixMetadataToResults)(scopedResults);
3846
+ const scopedDependencyGraphSummary = filterDependencyGraphSummaryByModuleScope(dependencyGraphSummary, scopeModules);
3847
+ const todayIsoDate = new Date().toISOString().slice(0, 10);
3848
+ const matchableSuppressions = suppressionsConfig.suppressions.filter((entry) => (0, suppressionValidator_1.getSuppressionExpiryState)(entry, todayIsoDate) !== 'expired');
3849
+ const appliedSuppressions = applyValidationSuppressions(scopedResultsWithQuickFix, matchableSuppressions);
3850
+ const shouldInitializeExceptionsFile = !(await (0, exceptionLoader_1.exceptionsFileExists)(workspaceRoot)) &&
3851
+ appliedSuppressions.activeResults.some((result) => !result.passed);
3852
+ if (shouldInitializeExceptionsFile) {
3853
+ await (0, exceptionLoader_1.ensureExceptionsFile)(workspaceRoot);
3854
+ }
3855
+ const exceptionsLoad = await (0, exceptionLoader_1.loadExceptions)(workspaceRoot);
3856
+ const appliedExceptions = applyValidationExceptions(appliedSuppressions.activeResults, exceptionsLoad.exceptions);
3857
+ const suppressionEvaluation = (0, suppressionValidator_1.evaluateSuppressions)({
3858
+ suppressions: suppressionsConfig.suppressions,
3859
+ suppressedResults: appliedSuppressions.suppressedResults,
3860
+ todayIsoDate,
3861
+ });
3862
+ appliedExceptions.activeResults.push(...suppressionEvaluation.validationResults);
3863
+ const driftSummary = (0, architectureDriftDetection_1.buildArchitectureDriftSummary)(appliedExceptions.activeResults, appliedSuppressions.suppressedResults);
3864
+ const written = await phase('Generating validation findings and reports', () => writeValidationReport(workspaceRoot, config, appliedExceptions.activeResults, validationConfig, appliedSuppressions.suppressedResults, appliedExceptions.approvedExceptions, suppressionsConfig.ignored, suppressionsConfig.suppressions.length, exceptionsLoad.exceptions.length, driftSummary, scopedDependencyGraphSummary, suppressionEvaluation.summary, suppressionEvaluation.entries, adrValidation.summary, policyStatus.policySource || policyStatus.message
3865
+ ? policyStatus
3866
+ : undefined, { openReportInEditor: options?.openReportInEditor }));
3867
+ await phase('Refreshing setup readiness and next steps', () => (0, setupNextSteps_1.refreshSetupNextStepsForWorkspace)({
3868
+ workspaceRoot,
3869
+ validationStatus: written.status,
3870
+ }));
3871
+ return {
3872
+ config,
3873
+ results: appliedExceptions.activeResults,
3874
+ status: written.status,
3875
+ reportPath: written.reportPath,
3876
+ statusPath: written.statusPath,
3877
+ };
3878
+ }
3879
+ //# sourceMappingURL=architectureValidation.js.map