@oneuptime/common 13.0.3 → 13.0.5

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 (687) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/RumApplication.ts +1 -1
  3. package/Models/DatabaseModels/SecurityEventConnection.ts +457 -0
  4. package/Models/DatabaseModels/SecurityEventConnectionRun.ts +263 -0
  5. package/Models/DatabaseModels/TelemetryIngestionKey.ts +2 -2
  6. package/Server/API/AIInvestigationAPI.ts +484 -24
  7. package/Server/API/GoogleSecOpsConnectionAPI.ts +249 -8
  8. package/Server/API/SecurityEventConnectionAPI.ts +250 -0
  9. package/Server/API/TelemetryAPI.ts +402 -265
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.ts +99 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  12. package/Server/Middleware/TelemetryIngest.ts +101 -6
  13. package/Server/Services/ExceptionAggregationService.ts +7 -16
  14. package/Server/Services/GoogleSecOpsConnectionService.ts +15 -0
  15. package/Server/Services/Index.ts +4 -0
  16. package/Server/Services/InventoryItemService.ts +28 -0
  17. package/Server/Services/LogAggregationService.ts +17 -23
  18. package/Server/Services/MetricAggregationService.ts +4 -14
  19. package/Server/Services/SecurityEventConnectionRunService.ts +10 -0
  20. package/Server/Services/SecurityEventConnectionService.ts +538 -0
  21. package/Server/Services/TraceAggregationService.ts +77 -28
  22. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  23. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +13 -1
  24. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +15 -1
  25. package/Server/Utils/AI/SRE/InvestigationEvidence.ts +658 -0
  26. package/Server/Utils/AI/SRE/InvestigationReferences.ts +324 -0
  27. package/Server/Utils/AI/Toolbox/Index.ts +38 -0
  28. package/Server/Utils/AI/Toolbox/TimelineTools.ts +9 -2
  29. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +80 -0
  30. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +5 -3
  31. package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.ts +1108 -0
  32. package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.ts +732 -0
  33. package/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.ts +495 -0
  34. package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.ts +689 -0
  35. package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.ts +767 -0
  36. package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.ts +571 -0
  37. package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.ts +827 -0
  38. package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.ts +610 -0
  39. package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.ts +549 -0
  40. package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.ts +830 -0
  41. package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.ts +684 -0
  42. package/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.ts +487 -0
  43. package/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.ts +778 -0
  44. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.ts +1002 -0
  45. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.ts +556 -0
  46. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.ts +275 -0
  47. package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.ts +53 -0
  48. package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.ts +709 -0
  49. package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.ts +596 -0
  50. package/Server/Utils/SecurityEvent/Connectors/Types.ts +191 -0
  51. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +353 -65
  52. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.ts +777 -0
  53. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +747 -148
  54. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +48 -2
  55. package/Server/Utils/SecurityEvent/SecurityEventDedupe.ts +81 -0
  56. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +741 -40
  57. package/Server/Utils/StartServer.ts +10 -0
  58. package/Server/Utils/Telemetry/EntityRegistry.ts +8 -0
  59. package/Server/Utils/Telemetry/KubernetesResourceAttributes.ts +108 -0
  60. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +174 -93
  61. package/Server/Utils/Telemetry/ResourceFacetPlanner.ts +251 -0
  62. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +248 -413
  63. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +668 -0
  64. package/Server/Utils/Telemetry/TraceContextPropagation.ts +348 -0
  65. package/Server/Utils/Telemetry.ts +18 -0
  66. package/Tests/App/Dashboard/AIInvestigationHeaderStatus.test.tsx +413 -0
  67. package/Tests/App/Dashboard/AIRootCauseFeedItem.test.tsx +285 -0
  68. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +242 -0
  69. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +601 -15
  70. package/Tests/App/Dashboard/ChangeAlertStateAIHeader.test.tsx +835 -0
  71. package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +41 -0
  72. package/Tests/App/Dashboard/ConnectionTestModal.test.tsx +604 -0
  73. package/Tests/App/Dashboard/ConnectorTestReportView.test.tsx +738 -0
  74. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +153 -6
  75. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +165 -0
  76. package/Tests/App/Dashboard/EpisodeChangeState.test.tsx +1069 -0
  77. package/Tests/App/Dashboard/EpisodeMembersCard.test.tsx +772 -0
  78. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +2354 -0
  79. package/Tests/App/Dashboard/EventOverviewSkeleton.test.tsx +210 -0
  80. package/Tests/App/Dashboard/EventStatBar.test.tsx +392 -0
  81. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +247 -0
  82. package/Tests/App/Dashboard/ExceptionAIAssistance.test.tsx +606 -0
  83. package/Tests/App/Dashboard/ExceptionBreadcrumbTimeline.test.tsx +273 -0
  84. package/Tests/App/Dashboard/ExceptionDetailSideMenu.test.tsx +176 -0
  85. package/Tests/App/Dashboard/ExceptionOverviewCards.test.tsx +366 -0
  86. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +371 -0
  87. package/Tests/App/Dashboard/ExceptionStackFrameViewer.test.tsx +440 -0
  88. package/Tests/App/Dashboard/ExceptionSummary.test.tsx +381 -0
  89. package/Tests/App/Dashboard/ExceptionTriage.test.tsx +306 -0
  90. package/Tests/App/Dashboard/ExceptionsResourceFacets.test.tsx +604 -0
  91. package/Tests/App/Dashboard/ExceptionsViewerEntityChips.test.tsx +706 -0
  92. package/Tests/App/Dashboard/FeedOrdering.test.ts +32 -0
  93. package/Tests/App/Dashboard/GoogleSecOpsConnectionForm.test.tsx +838 -0
  94. package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +18 -0
  95. package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +297 -20
  96. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +618 -13
  97. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +254 -207
  98. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +303 -135
  99. package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +1251 -0
  100. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +303 -0
  101. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +876 -8
  102. package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +35 -0
  103. package/Tests/App/Dashboard/InvestigationReferenceLink.test.tsx +177 -0
  104. package/Tests/App/Dashboard/InvestigationReportView.test.tsx +1295 -0
  105. package/Tests/App/Dashboard/KubernetesImageReferenceView.test.tsx +114 -0
  106. package/Tests/App/Dashboard/KubernetesLogsTabChips.test.tsx +285 -0
  107. package/Tests/App/Dashboard/LogsEntityChipNames.test.tsx +316 -0
  108. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +361 -0
  109. package/Tests/App/Dashboard/ProfileTableEntityNames.test.tsx +432 -0
  110. package/Tests/App/Dashboard/ProfileViewEntityName.test.tsx +250 -0
  111. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +10 -0
  112. package/Tests/App/Dashboard/ResourceLogsTabsBareLayout.test.tsx +700 -0
  113. package/Tests/App/Dashboard/ResourcePageEntityNameProps.test.tsx +803 -0
  114. package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +11 -1
  115. package/Tests/App/Dashboard/ScheduledMaintenanceChangeState.test.tsx +1026 -0
  116. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +436 -0
  117. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +1195 -0
  118. package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +1022 -0
  119. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +905 -0
  120. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +247 -0
  121. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +375 -274
  122. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +410 -0
  123. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +774 -0
  124. package/Tests/App/Dashboard/TopErrorsPanelResourceNames.test.tsx +316 -0
  125. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +30 -0
  126. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +88 -24
  127. package/Tests/App/Dashboard/TraceExplorer.test.tsx +1590 -0
  128. package/Tests/App/Dashboard/TraceWaterfall.test.tsx +488 -0
  129. package/Tests/App/Dashboard/TracesAnalyticsSeriesLabels.test.tsx +367 -0
  130. package/Tests/App/Dashboard/TracesEntityNames.test.tsx +585 -0
  131. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +613 -0
  132. package/Tests/App/Dashboard/UseServiceNames.test.tsx +359 -0
  133. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +7 -0
  134. package/Tests/Models/InventoryItemNaming.test.ts +132 -16
  135. package/Tests/Models/SecurityDomainAccessControl.test.ts +61 -17
  136. package/Tests/Server/API/AIInvestigationAPI.test.ts +611 -2
  137. package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +1098 -0
  138. package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +409 -0
  139. package/Tests/Server/API/SecurityEventConnectionAPI.test.ts +856 -0
  140. package/Tests/Server/API/SessionReplayAPI.test.ts +528 -1
  141. package/Tests/Server/API/TelemetryFacetsAPI.test.ts +810 -0
  142. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +240 -0
  143. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +229 -0
  144. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +66 -0
  145. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
  146. package/Tests/Server/Services/SecurityEventConnectionService.test.ts +1233 -0
  147. package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +53 -0
  148. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +218 -0
  149. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +155 -1
  150. package/Tests/Server/Services/TraceAggregationExceptionScope.test.ts +142 -0
  151. package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +85 -24
  152. package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +33 -0
  153. package/Tests/Server/Utils/AI/InvestigationEvidence.test.ts +1395 -0
  154. package/Tests/Server/Utils/AI/InvestigationReferences.test.ts +616 -0
  155. package/Tests/Server/Utils/AI/TimelineTools.test.ts +119 -0
  156. package/Tests/Server/Utils/AI/Toolbox/AIToolbox.test.ts +151 -0
  157. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +65 -0
  158. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorExceptionScope.test.ts +224 -0
  159. package/Tests/Server/Utils/LoggerCore.test.ts +597 -0
  160. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPerSeriesFanout.test.ts +55 -2
  161. package/Tests/Server/Utils/Monitor/MonitorTemplateUtil.test.ts +1109 -0
  162. package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.test.ts +1240 -0
  163. package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.test.ts +1853 -0
  164. package/Tests/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.test.ts +1129 -0
  165. package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.test.ts +1066 -0
  166. package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.test.ts +1568 -0
  167. package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.test.ts +930 -0
  168. package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.test.ts +1409 -0
  169. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.test.ts +763 -0
  170. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.test.ts +1296 -0
  171. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.test.ts +1102 -0
  172. package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.test.ts +1174 -0
  173. package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.test.ts +1032 -0
  174. package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.test.ts +1558 -0
  175. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.test.ts +3041 -0
  176. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.test.ts +1168 -0
  177. package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.test.ts +906 -0
  178. package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.test.ts +1068 -0
  179. package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.test.ts +1004 -0
  180. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.test.ts +784 -0
  181. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +113 -1
  182. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +5 -1
  183. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +93 -13
  184. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +23 -1
  185. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +25 -3
  186. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +22 -8
  187. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +82 -16
  188. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSearchDetections.test.ts +730 -0
  189. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsThreePassPoller.test.ts +1577 -0
  190. package/Tests/Server/Utils/SessionReplay/SessionReplayOriginAllowlist.test.ts +41 -0
  191. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1453 -18
  192. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +711 -0
  193. package/Tests/Server/Utils/Telemetry/KubernetesResourceAttributes.test.ts +162 -0
  194. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +460 -13
  195. package/Tests/Server/Utils/Telemetry/ResourceFacetPlanner.test.ts +678 -0
  196. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +868 -0
  197. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +525 -0
  198. package/Tests/Server/Utils/Telemetry/TraceContextPropagation.test.ts +420 -0
  199. package/Tests/Types/OnCallDutyPolicy/LayerUtilCorePaths.test.ts +1014 -0
  200. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +9 -6
  201. package/Tests/Types/Rules/RuleCriteriaFieldRegistry.test.ts +276 -0
  202. package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +73 -0
  203. package/Tests/Types/Rum/SessionReplayTransportContracts.test.ts +95 -0
  204. package/Tests/Types/SecurityEvent/Connectors/ConnectorDiagnostics.test.ts +82 -0
  205. package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +314 -0
  206. package/Tests/Types/Telemetry/ExceptionSpanScope.test.ts +89 -0
  207. package/Tests/Types/Telemetry/LlmConventions.test.ts +495 -0
  208. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +96 -0
  209. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +304 -0
  210. package/Tests/UI/Components/ActiveFilterChipsLockedFilters.test.tsx +357 -0
  211. package/Tests/UI/Components/Card.test.tsx +312 -1
  212. package/Tests/UI/Components/CardModelDetailHeaderLayout.test.tsx +253 -0
  213. package/Tests/UI/Components/CardModelDetailRefresher.test.tsx +342 -0
  214. package/Tests/UI/Components/Checkbox.test.tsx +79 -0
  215. package/Tests/UI/Components/CopyTextButton.test.tsx +142 -0
  216. package/Tests/UI/Components/DetailCompactStyle.test.tsx +621 -0
  217. package/Tests/UI/Components/ErrorBoundary.test.tsx +60 -0
  218. package/Tests/UI/Components/FacetSections.test.tsx +581 -0
  219. package/Tests/UI/Components/FacetSidebarControls.test.tsx +812 -0
  220. package/Tests/UI/Components/FacetVisibility.test.ts +429 -0
  221. package/Tests/UI/Components/LockedFilterActions.test.tsx +396 -0
  222. package/Tests/UI/Components/LockedFilterChip.test.tsx +651 -0
  223. package/Tests/UI/Components/LogsEntityNames.test.ts +1769 -0
  224. package/Tests/UI/Components/LogsEntityNamesComponents.test.tsx +696 -0
  225. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +600 -0
  226. package/Tests/UI/Components/LogsViewerEntityNames.test.tsx +713 -0
  227. package/Tests/UI/Components/MarkdownInlineReferences.test.tsx +2079 -0
  228. package/Tests/UI/Components/MemberRoleAssignment.test.tsx +446 -0
  229. package/Tests/UI/Components/ModelDetailRefetch.test.tsx +397 -0
  230. package/Tests/UI/Components/ModelListRefetch.test.tsx +233 -0
  231. package/Tests/UI/Components/ModelPageModelChange.test.tsx +409 -0
  232. package/Tests/UI/Components/PaginationIntegration.test.tsx +25 -0
  233. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +172 -0
  234. package/Tests/UI/Components/SavedViewsSidebarIntegration.test.tsx +32 -14
  235. package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +44 -1
  236. package/Tests/UI/Components/TelemetryActiveFilterChipsLockedFilters.test.tsx +304 -0
  237. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +718 -0
  238. package/Tests/UI/Monitor/EvaluationLogList.test.tsx +303 -0
  239. package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +55 -1
  240. package/Tests/UI/Rum/FidelityNotices.test.ts +22 -0
  241. package/Tests/UI/Rum/RecordingHealthCard.test.tsx +120 -136
  242. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +988 -0
  243. package/Tests/UI/Rum/ReplayCard.test.tsx +32 -0
  244. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +709 -12
  245. package/Tests/UI/Rum/ReplayHeader.test.tsx +50 -1
  246. package/Tests/UI/Rum/ReplayPinControl.test.tsx +67 -10
  247. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +41 -12
  248. package/Tests/UI/Rum/ReplayStage.test.tsx +663 -1
  249. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +173 -0
  250. package/Tests/UI/Rum/ReplayUi.test.tsx +189 -2
  251. package/Tests/UI/Rum/ReplayUserSessionsMenu.test.tsx +91 -0
  252. package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +1 -1
  253. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +585 -0
  254. package/Tests/UI/Rum/SessionReplayTable.test.tsx +844 -51
  255. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +29 -1
  256. package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +288 -0
  257. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +31 -1
  258. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +6 -0
  259. package/Tests/UI/Utils/Clipboard.test.ts +154 -0
  260. package/Tests/UI/Utils/Navigation.test.ts +24 -0
  261. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +2205 -0
  262. package/Tests/UI/Utils/Telemetry/UseTelemetryEntityNames.test.tsx +844 -0
  263. package/Tests/Utils/AI/InvestigationReport.test.ts +2430 -0
  264. package/Tests/Utils/APIOutgoingRequestTracer.test.ts +137 -0
  265. package/Tests/Utils/Rum/SessionReplayHealth.test.ts +884 -0
  266. package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +40 -0
  267. package/Tests/Utils/Rum/SessionReplayRecordingEnded.test.ts +619 -0
  268. package/Tests/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.test.ts +631 -0
  269. package/Tests/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.test.ts +479 -0
  270. package/Tests/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.test.ts +507 -0
  271. package/Tests/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.test.ts +686 -0
  272. package/Tests/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.test.ts +474 -0
  273. package/Tests/Utils/SecurityEvent/Connectors/OktaNormalizer.test.ts +736 -0
  274. package/Tests/Utils/SecurityEvent/Connectors/SplunkNormalizer.test.ts +407 -0
  275. package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.test.ts +129 -0
  276. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +57 -1
  277. package/Tests/Utils/Telemetry/EntityRelationship.test.ts +36 -0
  278. package/Tests/Utils/Telemetry/LockedFilterSearch.test.ts +600 -0
  279. package/Tests/Utils/Telemetry/NetworkHost.test.ts +48 -0
  280. package/Tests/Utils/Telemetry/OriginAllowList.test.ts +154 -0
  281. package/Tests/Utils/Traces/CriticalPath.test.ts +25 -0
  282. package/Types/AI/AIChatTypes.ts +8 -0
  283. package/Types/AI/InvestigationEvidence.ts +96 -0
  284. package/Types/Monitor/MonitorEvaluationSummary.ts +5 -0
  285. package/Types/OnCallDutyPolicy/Layer.ts +12 -0
  286. package/Types/Permission.ts +2 -2
  287. package/Types/Rules/RuleCriteriaFieldRegistry.ts +12 -0
  288. package/Types/Rum/SessionReplay.ts +109 -0
  289. package/Types/Rum/SessionReplayApi.ts +31 -0
  290. package/Types/SecurityEvent/Connectors/ConnectorDiagnostics.ts +111 -0
  291. package/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.ts +87 -0
  292. package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +492 -0
  293. package/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.ts +41 -0
  294. package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -1
  295. package/Types/SecurityEvent/GoogleSecOpsRegion.ts +34 -0
  296. package/Types/Telemetry/EntityType.ts +14 -0
  297. package/Types/Telemetry/ExceptionSpanScope.ts +83 -0
  298. package/Types/Telemetry/LockedFilterDetail.ts +46 -0
  299. package/Types/Telemetry/ResourceEntityFacet.ts +17 -10
  300. package/Types/Telemetry/ResourceFacetCatalog.ts +171 -0
  301. package/UI/Components/Card/Card.tsx +118 -83
  302. package/UI/Components/Checkbox/Checkbox.tsx +8 -5
  303. package/UI/Components/CopyTextButton/CopyTextButton.tsx +7 -1
  304. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +4 -1
  305. package/UI/Components/Detail/Detail.tsx +137 -22
  306. package/UI/Components/ErrorBoundary.tsx +38 -0
  307. package/UI/Components/LogsViewer/LogsEntityNames.ts +912 -0
  308. package/UI/Components/LogsViewer/LogsViewer.tsx +156 -77
  309. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +28 -11
  310. package/UI/Components/LogsViewer/components/FacetSection.tsx +66 -79
  311. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +17 -2
  312. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +45 -5
  313. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +100 -19
  314. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +156 -39
  315. package/UI/Components/LogsViewer/components/LogsTable.tsx +30 -8
  316. package/UI/Components/LogsViewer/types.ts +6 -0
  317. package/UI/Components/Markdown.tsx/InlineReferences.tsx +628 -0
  318. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +385 -312
  319. package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +50 -22
  320. package/UI/Components/ModelDetail/CardModelDetail.tsx +15 -0
  321. package/UI/Components/ModelDetail/ModelDetail.tsx +64 -19
  322. package/UI/Components/ModelList/ModelList.tsx +33 -8
  323. package/UI/Components/Page/ModelPage.tsx +119 -54
  324. package/UI/Components/Table/Table.tsx +8 -0
  325. package/UI/Components/Table/TableRow.tsx +13 -5
  326. package/UI/Components/TelemetryViewer/FacetVisibility.ts +229 -0
  327. package/UI/Components/TelemetryViewer/ResourceFacetConfigs.ts +46 -0
  328. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +11 -0
  329. package/UI/Components/TelemetryViewer/components/FacetSearchInput.tsx +79 -0
  330. package/UI/Components/TelemetryViewer/components/FacetSectionHeader.tsx +69 -0
  331. package/UI/Components/TelemetryViewer/components/FacetShowMoreButton.tsx +36 -0
  332. package/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.tsx +88 -0
  333. package/UI/Components/TelemetryViewer/components/LockedFilterActions.tsx +205 -0
  334. package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +496 -0
  335. package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +30 -10
  336. package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +203 -10
  337. package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +60 -73
  338. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +94 -7
  339. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +17 -2
  340. package/UI/Components/TelemetryViewer/types.ts +19 -0
  341. package/UI/Components/TelemetryViewer/useFacetSearchExemptions.ts +158 -0
  342. package/UI/Components/TelemetryViewer/useFacetSectionSearch.ts +102 -0
  343. package/UI/Utils/Clipboard.ts +68 -2
  344. package/UI/Utils/Navigation.ts +17 -2
  345. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +620 -0
  346. package/UI/Utils/Telemetry/UseTelemetryEntityNames.ts +151 -0
  347. package/Utils/AI/InvestigationReport.ts +1789 -0
  348. package/Utils/API.ts +90 -1
  349. package/Utils/Rum/SessionReplayHealth.ts +11 -0
  350. package/Utils/Rum/SessionReplayRecordingEnded.ts +134 -0
  351. package/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.ts +427 -0
  352. package/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.ts +197 -0
  353. package/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.ts +464 -0
  354. package/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.ts +567 -0
  355. package/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.ts +299 -0
  356. package/Utils/SecurityEvent/Connectors/OktaNormalizer.ts +467 -0
  357. package/Utils/SecurityEvent/Connectors/SplunkNormalizer.ts +439 -0
  358. package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +124 -12
  359. package/Utils/Telemetry/CrossSignalScope.ts +22 -8
  360. package/Utils/Telemetry/EntityRelationship.ts +8 -0
  361. package/Utils/Telemetry/LockedFilterSearch.ts +412 -0
  362. package/Utils/Telemetry/NetworkHost.ts +43 -0
  363. package/Utils/Telemetry/OriginAllowList.ts +139 -4
  364. package/Utils/Traces/CriticalPath.ts +10 -4
  365. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  366. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  367. package/build/dist/Models/DatabaseModels/RumApplication.js +1 -1
  368. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  369. package/build/dist/Models/DatabaseModels/SecurityEventConnection.js +490 -0
  370. package/build/dist/Models/DatabaseModels/SecurityEventConnection.js.map +1 -0
  371. package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js +287 -0
  372. package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js.map +1 -0
  373. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +2 -2
  374. package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
  375. package/build/dist/Server/API/AIInvestigationAPI.js +336 -27
  376. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  377. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +182 -7
  378. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -1
  379. package/build/dist/Server/API/SecurityEventConnectionAPI.js +171 -0
  380. package/build/dist/Server/API/SecurityEventConnectionAPI.js.map +1 -0
  381. package/build/dist/Server/API/TelemetryAPI.js +248 -170
  382. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  383. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js +40 -0
  384. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js.map +1 -0
  385. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  386. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  387. package/build/dist/Server/Middleware/TelemetryIngest.js +57 -4
  388. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  389. package/build/dist/Server/Services/ExceptionAggregationService.js +7 -16
  390. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
  391. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +8 -0
  392. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
  393. package/build/dist/Server/Services/Index.js +4 -0
  394. package/build/dist/Server/Services/Index.js.map +1 -1
  395. package/build/dist/Server/Services/InventoryItemService.js +22 -0
  396. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  397. package/build/dist/Server/Services/LogAggregationService.js +8 -17
  398. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  399. package/build/dist/Server/Services/MetricAggregationService.js +4 -14
  400. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  401. package/build/dist/Server/Services/SecurityEventConnectionRunService.js +9 -0
  402. package/build/dist/Server/Services/SecurityEventConnectionRunService.js.map +1 -0
  403. package/build/dist/Server/Services/SecurityEventConnectionService.js +356 -0
  404. package/build/dist/Server/Services/SecurityEventConnectionService.js.map +1 -0
  405. package/build/dist/Server/Services/TraceAggregationService.js +51 -25
  406. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  407. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  408. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  409. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +3 -0
  410. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  411. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +14 -7
  412. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  413. package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js +468 -0
  414. package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js.map +1 -0
  415. package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js +230 -0
  416. package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js.map +1 -0
  417. package/build/dist/Server/Utils/AI/Toolbox/Index.js +32 -0
  418. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  419. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +6 -2
  420. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -1
  421. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +59 -0
  422. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  423. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +5 -3
  424. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  425. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js +752 -0
  426. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js.map +1 -0
  427. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js +493 -0
  428. package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js.map +1 -0
  429. package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js +352 -0
  430. package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js.map +1 -0
  431. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js +405 -0
  432. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js.map +1 -0
  433. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js +466 -0
  434. package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js.map +1 -0
  435. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js +344 -0
  436. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js.map +1 -0
  437. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js +511 -0
  438. package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js.map +1 -0
  439. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js +406 -0
  440. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js.map +1 -0
  441. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js +336 -0
  442. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js.map +1 -0
  443. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js +474 -0
  444. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js.map +1 -0
  445. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js +417 -0
  446. package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js.map +1 -0
  447. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js +314 -0
  448. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js.map +1 -0
  449. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js +494 -0
  450. package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js.map +1 -0
  451. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js +719 -0
  452. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js.map +1 -0
  453. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js +404 -0
  454. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js.map +1 -0
  455. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js +209 -0
  456. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js.map +1 -0
  457. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js +40 -0
  458. package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js.map +1 -0
  459. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js +494 -0
  460. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js.map +1 -0
  461. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js +379 -0
  462. package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js.map +1 -0
  463. package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js +27 -0
  464. package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js.map +1 -0
  465. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +234 -61
  466. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  467. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js +537 -0
  468. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js.map +1 -0
  469. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +572 -115
  470. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  471. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +35 -1
  472. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -1
  473. package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js +52 -0
  474. package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js.map +1 -0
  475. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +521 -32
  476. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  477. package/build/dist/Server/Utils/StartServer.js +10 -0
  478. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  479. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +8 -0
  480. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  481. package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js +80 -0
  482. package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js.map +1 -0
  483. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +122 -67
  484. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  485. package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js +146 -0
  486. package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js.map +1 -0
  487. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +174 -248
  488. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  489. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +492 -0
  490. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -0
  491. package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js +259 -0
  492. package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js.map +1 -0
  493. package/build/dist/Server/Utils/Telemetry.js +11 -5
  494. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  495. package/build/dist/Types/AI/InvestigationEvidence.js +2 -0
  496. package/build/dist/Types/AI/InvestigationEvidence.js.map +1 -0
  497. package/build/dist/Types/OnCallDutyPolicy/Layer.js +11 -0
  498. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  499. package/build/dist/Types/Permission.js +2 -2
  500. package/build/dist/Types/Permission.js.map +1 -1
  501. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +9 -0
  502. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  503. package/build/dist/Types/Rum/SessionReplay.js +95 -0
  504. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  505. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
  506. package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js +18 -0
  507. package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js.map +1 -0
  508. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js +2 -0
  509. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js.map +1 -0
  510. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +374 -0
  511. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -0
  512. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js +35 -0
  513. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js.map +1 -0
  514. package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js +35 -0
  515. package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js.map +1 -0
  516. package/build/dist/Types/Telemetry/EntityType.js +14 -0
  517. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  518. package/build/dist/Types/Telemetry/ExceptionSpanScope.js +59 -0
  519. package/build/dist/Types/Telemetry/ExceptionSpanScope.js.map +1 -0
  520. package/build/dist/Types/Telemetry/LockedFilterDetail.js +16 -0
  521. package/build/dist/Types/Telemetry/LockedFilterDetail.js.map +1 -0
  522. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +16 -10
  523. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -1
  524. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +119 -0
  525. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -0
  526. package/build/dist/UI/Components/Card/Card.js +34 -17
  527. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  528. package/build/dist/UI/Components/Checkbox/Checkbox.js +6 -4
  529. package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
  530. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
  531. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
  532. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +1 -1
  533. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  534. package/build/dist/UI/Components/Detail/Detail.js +63 -13
  535. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  536. package/build/dist/UI/Components/ErrorBoundary.js +34 -0
  537. package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
  538. package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js +535 -0
  539. package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js.map +1 -0
  540. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +117 -57
  541. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  542. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +5 -7
  543. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
  544. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +31 -44
  545. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
  546. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +17 -3
  547. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  548. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -3
  549. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  550. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +64 -18
  551. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  552. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +100 -41
  553. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  554. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +13 -5
  555. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  556. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
  557. package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js +403 -0
  558. package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js.map +1 -0
  559. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +197 -179
  560. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  561. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +30 -21
  562. package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
  563. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +12 -0
  564. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  565. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +46 -12
  566. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  567. package/build/dist/UI/Components/ModelList/ModelList.js +26 -7
  568. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  569. package/build/dist/UI/Components/Page/ModelPage.js +62 -34
  570. package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
  571. package/build/dist/UI/Components/Table/Table.js +3 -1
  572. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  573. package/build/dist/UI/Components/Table/TableRow.js +8 -1
  574. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  575. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +111 -0
  576. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -0
  577. package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js +22 -0
  578. package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js.map +1 -0
  579. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  580. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  581. package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js +36 -0
  582. package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js.map +1 -0
  583. package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js +21 -0
  584. package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js.map +1 -0
  585. package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js +14 -0
  586. package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js.map +1 -0
  587. package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js +27 -0
  588. package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js.map +1 -0
  589. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js +88 -0
  590. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js.map +1 -0
  591. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +228 -0
  592. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -0
  593. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +6 -6
  594. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -1
  595. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +111 -12
  596. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
  597. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +27 -40
  598. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -1
  599. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +70 -10
  600. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  601. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +17 -3
  602. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  603. package/build/dist/UI/Components/TelemetryViewer/types.js +0 -4
  604. package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -1
  605. package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js +70 -0
  606. package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js.map +1 -0
  607. package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js +52 -0
  608. package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js.map +1 -0
  609. package/build/dist/UI/Utils/Clipboard.js +59 -2
  610. package/build/dist/UI/Utils/Clipboard.js.map +1 -1
  611. package/build/dist/UI/Utils/Navigation.js +11 -2
  612. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  613. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +471 -0
  614. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -0
  615. package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js +113 -0
  616. package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js.map +1 -0
  617. package/build/dist/Utils/AI/InvestigationReport.js +1241 -0
  618. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -0
  619. package/build/dist/Utils/API.js +25 -0
  620. package/build/dist/Utils/API.js.map +1 -1
  621. package/build/dist/Utils/Rum/SessionReplayHealth.js +10 -0
  622. package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -1
  623. package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js +54 -0
  624. package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js.map +1 -0
  625. package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js +311 -0
  626. package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js.map +1 -0
  627. package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js +150 -0
  628. package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js.map +1 -0
  629. package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js +328 -0
  630. package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js.map +1 -0
  631. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js +405 -0
  632. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js.map +1 -0
  633. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js +212 -0
  634. package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js.map +1 -0
  635. package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js +351 -0
  636. package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js.map +1 -0
  637. package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js +348 -0
  638. package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js.map +1 -0
  639. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +103 -10
  640. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
  641. package/build/dist/Utils/Telemetry/CrossSignalScope.js +16 -5
  642. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  643. package/build/dist/Utils/Telemetry/EntityRelationship.js +7 -0
  644. package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
  645. package/build/dist/Utils/Telemetry/LockedFilterSearch.js +277 -0
  646. package/build/dist/Utils/Telemetry/LockedFilterSearch.js.map +1 -0
  647. package/build/dist/Utils/Telemetry/NetworkHost.js +39 -0
  648. package/build/dist/Utils/Telemetry/NetworkHost.js.map +1 -0
  649. package/build/dist/Utils/Telemetry/OriginAllowList.js +105 -4
  650. package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -1
  651. package/build/dist/Utils/Traces/CriticalPath.js +9 -4
  652. package/build/dist/Utils/Traces/CriticalPath.js.map +1 -1
  653. package/package.json +1 -1
  654. package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +0 -600
  655. package/UI/Components/GanttChart/Bar/BarLabel.tsx +0 -17
  656. package/UI/Components/GanttChart/Bar/Index.tsx +0 -149
  657. package/UI/Components/GanttChart/ChartContainer.tsx +0 -50
  658. package/UI/Components/GanttChart/Index.tsx +0 -90
  659. package/UI/Components/GanttChart/Row/Index.tsx +0 -22
  660. package/UI/Components/GanttChart/Row/Row.tsx +0 -231
  661. package/UI/Components/GanttChart/Row/RowLabel.tsx +0 -30
  662. package/UI/Components/GanttChart/Rows.tsx +0 -59
  663. package/UI/Components/GanttChart/Timeline/Index.tsx +0 -46
  664. package/UI/Components/GanttChart/Timeline/TimelineInterval.tsx +0 -33
  665. package/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.tsx +0 -43
  666. package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js +0 -8
  667. package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js.map +0 -1
  668. package/build/dist/UI/Components/GanttChart/Bar/Index.js +0 -77
  669. package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +0 -1
  670. package/build/dist/UI/Components/GanttChart/ChartContainer.js +0 -33
  671. package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +0 -1
  672. package/build/dist/UI/Components/GanttChart/Index.js +0 -39
  673. package/build/dist/UI/Components/GanttChart/Index.js.map +0 -1
  674. package/build/dist/UI/Components/GanttChart/Row/Index.js +0 -7
  675. package/build/dist/UI/Components/GanttChart/Row/Index.js.map +0 -1
  676. package/build/dist/UI/Components/GanttChart/Row/Row.js +0 -106
  677. package/build/dist/UI/Components/GanttChart/Row/Row.js.map +0 -1
  678. package/build/dist/UI/Components/GanttChart/Row/RowLabel.js +0 -10
  679. package/build/dist/UI/Components/GanttChart/Row/RowLabel.js.map +0 -1
  680. package/build/dist/UI/Components/GanttChart/Rows.js +0 -19
  681. package/build/dist/UI/Components/GanttChart/Rows.js.map +0 -1
  682. package/build/dist/UI/Components/GanttChart/Timeline/Index.js +0 -16
  683. package/build/dist/UI/Components/GanttChart/Timeline/Index.js.map +0 -1
  684. package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js +0 -13
  685. package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js.map +0 -1
  686. package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js +0 -13
  687. package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js.map +0 -1
@@ -0,0 +1,1789 @@
1
+ /*
2
+ * Pure helpers for reading an AI investigation report.
3
+ *
4
+ * The investigation engine posts one markdown blob: a branded heading, the
5
+ * model's own "Summary / Most likely root cause / Evidence / Suggested next
6
+ * steps" note, a server-authored "Evidence checked" list and a footer
7
+ * (AIInvestigationEngine.buildBrandedMarkdown). The dashboard wants to lay
8
+ * those parts out separately, and the server wants the evidence labels and
9
+ * the incident/alert numbers the note mentions.
10
+ *
11
+ * Everything here is text-only and dependency-free so it loads the same in
12
+ * the dashboard bundle, the API server and Common jest. None of it turns
13
+ * model-authored text into links or HTML: callers get plain strings and
14
+ * numbers, and resolve anything clickable from their own database rows.
15
+ */
16
+
17
+ export const AI_ROOT_CAUSE_HEADING_TEXT: string =
18
+ "AI — Automated Root Cause Analysis";
19
+
20
+ export enum InvestigationReportSectionKind {
21
+ Summary = "Summary",
22
+ RootCause = "RootCause",
23
+ Evidence = "Evidence",
24
+ NextSteps = "NextSteps",
25
+ Other = "Other",
26
+ }
27
+
28
+ export interface InvestigationReportSection {
29
+ kind: InvestigationReportSectionKind;
30
+ // The label as written, e.g. "Most likely root cause".
31
+ title: string;
32
+ // Section body, label and its trailing separator removed, trimmed.
33
+ markdown: string;
34
+ }
35
+
36
+ export interface InvestigationEvidenceCheckedEntry {
37
+ // "C1"
38
+ citationId: string;
39
+ label: string;
40
+ rowCount: number;
41
+ }
42
+
43
+ export interface InvestigationReportFooter {
44
+ // Footer text without the surrounding * emphasis markers.
45
+ text: string;
46
+ modelName?: string | undefined;
47
+ queryCount?: number | undefined;
48
+ }
49
+
50
+ export interface ParsedInvestigationReport {
51
+ /*
52
+ * True when a Summary or a Most likely root cause section with a body was
53
+ * recognised — i.e. the report is worth laying out section by section.
54
+ */
55
+ isStructured: boolean;
56
+ /*
57
+ * Markdown before the first recognised section, brand heading removed,
58
+ * trimmed ("" when there is none). With no recognised section at all this
59
+ * is the whole body.
60
+ */
61
+ preamble: string;
62
+ /*
63
+ * Recognised sections with a non-empty body, in document order. The
64
+ * Evidence checked block and the footer are NOT included.
65
+ */
66
+ sections: Array<InvestigationReportSection>;
67
+ // Convenience: the first section body of each kind, or undefined.
68
+ summary?: string | undefined;
69
+ rootCause?: string | undefined;
70
+ evidence?: string | undefined;
71
+ nextSteps?: string | undefined;
72
+ /*
73
+ * The server-authored "Evidence checked" list: the LAST such block, unless
74
+ * the footer's query count shows the server could not have written it.
75
+ */
76
+ evidenceChecked: Array<InvestigationEvidenceCheckedEntry>;
77
+ footer?: InvestigationReportFooter | undefined;
78
+ /*
79
+ * The whole report with the brand heading, the Evidence checked block and
80
+ * the footer removed.
81
+ */
82
+ bodyMarkdown: string;
83
+ }
84
+
85
+ export interface InvestigationEventReferenceToken {
86
+ // null = no qualifier word; the caller applies a default.
87
+ kind: "incident" | "alert" | null;
88
+ number: number;
89
+ // Index of "#" in the input.
90
+ start: number;
91
+ // Index after the last digit.
92
+ end: number;
93
+ }
94
+
95
+ export interface ExtractedInvestigationEventReference {
96
+ kind: "incident" | "alert";
97
+ number: number;
98
+ }
99
+
100
+ export const DEFAULT_EVENT_REFERENCE_LIMIT: number = 25;
101
+
102
+ /*
103
+ * Citation marker "[C12]". A fresh RegExp per call because the global flag
104
+ * makes a shared instance stateful (lastIndex) across callers.
105
+ */
106
+ export function getCitationMarkerRegex(): RegExp {
107
+ return /\[(C\d{1,3})\]/g;
108
+ }
109
+
110
+ // "EvidenceChecked" is internal: it is parsed into entries, never a section.
111
+ type LabelKind = InvestigationReportSectionKind | "EvidenceChecked";
112
+
113
+ /*
114
+ * Known labels, keyed by their normalised (lower-cased, whitespace-collapsed)
115
+ * text. A Map rather than an object literal so a heading such as
116
+ * "constructor" can never resolve to an Object.prototype member.
117
+ */
118
+ const KNOWN_LABELS: Map<string, LabelKind> = new Map<string, LabelKind>([
119
+ ["summary", InvestigationReportSectionKind.Summary],
120
+ ["tl;dr", InvestigationReportSectionKind.Summary],
121
+ ["tl; dr", InvestigationReportSectionKind.Summary],
122
+ ["tldr", InvestigationReportSectionKind.Summary],
123
+ ["most likely root cause", InvestigationReportSectionKind.RootCause],
124
+ ["likely root cause", InvestigationReportSectionKind.RootCause],
125
+ ["probable root cause", InvestigationReportSectionKind.RootCause],
126
+ ["root cause", InvestigationReportSectionKind.RootCause],
127
+ ["root cause hypothesis", InvestigationReportSectionKind.RootCause],
128
+ ["evidence", InvestigationReportSectionKind.Evidence],
129
+ ["key evidence", InvestigationReportSectionKind.Evidence],
130
+ ["supporting evidence", InvestigationReportSectionKind.Evidence],
131
+ ["key findings", InvestigationReportSectionKind.Evidence],
132
+ ["findings", InvestigationReportSectionKind.Evidence],
133
+ ["suggested next steps", InvestigationReportSectionKind.NextSteps],
134
+ ["next steps", InvestigationReportSectionKind.NextSteps],
135
+ ["recommended next steps", InvestigationReportSectionKind.NextSteps],
136
+ ["recommended actions", InvestigationReportSectionKind.NextSteps],
137
+ ["recommendations", InvestigationReportSectionKind.NextSteps],
138
+ ["evidence checked", "EvidenceChecked"],
139
+ ]);
140
+
141
+ const BRAND_HEADING_MARKER: string = "automated root cause analysis";
142
+ const FOOTER_PREFIX: string = "Investigated automatically by OneUptime AI";
143
+
144
+ const LINE_ENDING_REGEX: RegExp = /\r\n?/g;
145
+ const WHITESPACE_RUN_REGEX: RegExp = /\s+/g;
146
+ const BLANK_LINE_SPLIT_REGEX: RegExp = /\n[ \t]*\n/;
147
+
148
+ // Fences may sit inside list items, so any indentation opens one.
149
+ const FENCE_OPEN_REGEX: RegExp = /^[ \t]*(`{3,}|~{3,})(.*)$/;
150
+ const FENCE_CLOSE_REGEX: RegExp = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
151
+
152
+ // Four spaces or a tab is an indented code block, never a section label.
153
+ const INDENTED_CODE_REGEX: RegExp = /^(?: {4}|\t)/;
154
+ const ATX_HEADING_REGEX: RegExp = /^ {0,3}#{1,6}(?:[ \t]+(.*))?$/;
155
+ const THEMATIC_BREAK_REGEX: RegExp =
156
+ /^ {0,3}(?:(?:-[ \t]*){3,}|(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$/;
157
+
158
+ const LEADING_PARENTHETICAL_REGEX: RegExp = /^[ \t]*(\([^()]{1,80}\))/;
159
+ /*
160
+ * Longest text worth looking up as a label: the longest known label plus a
161
+ * decorative prefix, a colon and a "(medium confidence)" style qualifier.
162
+ */
163
+ const MAX_KNOWN_LABEL_INPUT_LENGTH: number = 160;
164
+ const LEADING_NUMBERING_REGEX: RegExp = /^\d{1,2}[.)][ \t]+/;
165
+ const ENDS_WITH_SENTENCE_PUNCTUATION_REGEX: RegExp = /[.!?]$/;
166
+ // A label needs some text beyond emphasis markers and punctuation ("**_**").
167
+ const HAS_LABEL_TEXT_REGEX: RegExp = /[^\s*_~`#:.—–-]/;
168
+ const CITATION_MARKER_TEST_REGEX: RegExp = /\[C\d{1,3}\]/;
169
+
170
+ /*
171
+ * "**Label** rest" / "__Label__ rest". The optional second group lets
172
+ * "***Label***" (bold italic) unwrap cleanly instead of leaving a stray "*".
173
+ */
174
+ const STRONG_LEAD_REGEX: RegExp = /^(\*\*|__)(\*|_)?(.+?)\2?\1(.*)$/;
175
+ const SEPARATOR_PREFIX_REGEX: RegExp = /^(?:[:—–]|-{1,2})[ \t]*/;
176
+ // The text before the first ":" of a plain "Summary: body" line.
177
+ const PLAIN_LABEL_REGEX: RegExp = /^[A-Za-z][A-Za-z \t;,()]{0,60}$/;
178
+
179
+ // Bold-only lines longer than this read as emphasised prose, not a label.
180
+ const MAX_BOLD_LABEL_LENGTH: number = 80;
181
+
182
+ // Bold and plain labels rank below every ATX heading level (1-6).
183
+ const BOLD_LABEL_RANK: number = 7;
184
+
185
+ const LIST_ITEM_REGEX: RegExp = /^[ \t]*(?:[-*+]|\d{1,3}[.)])(?:[ \t]+|$)/;
186
+ const LIST_CONTINUATION_REGEX: RegExp = /^[ \t]{2,}\S/;
187
+ const LEADING_WHITESPACE_REGEX: RegExp = /^[ \t]/;
188
+ /*
189
+ * "- **[C1]** label — 7 row(s)". The label is greedy so a label that itself
190
+ * contains " — " keeps everything up to the LAST " — N row(s)".
191
+ */
192
+ const EVIDENCE_CHECKED_ENTRY_REGEX: RegExp =
193
+ /^[ \t]*(?:[-*+]|\d{1,3}[.)])[ \t]+(?:\*\*|__)?\[(C\d{1,3})\](?:\*\*|__)?(.+)[ \t][—–-][ \t]+(\d+)[ \t]+rows?(?:\(s\))?[ \t]*$/i;
194
+ // The entry regex backtracks over the label; never feed it a runaway line.
195
+ const MAX_EVIDENCE_CHECKED_LINE_LENGTH: number = 2000;
196
+
197
+ const FOOTER_EMPHASIS_REGEX: RegExp = /^(\*\*|__|\*|_)([\s\S]+)\1$/;
198
+ const FOOTER_USING_REGEX: RegExp = /\busing[ \t]+/;
199
+ const FOOTER_QUERIES_RUN_REGEX: RegExp = /\bquer(?:y|ies)[ \t]+run\b/i;
200
+ const DIGIT_REGEX: RegExp = /\d/;
201
+
202
+ const EVENT_REFERENCE_PATTERN: string = "#(\\d{1,9})(?!\\w)";
203
+ const WORD_CHARACTER_REGEX: RegExp = /\w/;
204
+ const WHITESPACE_CHARACTER_REGEX: RegExp = /\s/;
205
+ const FORBIDDEN_BEFORE_REFERENCE: string = "&/#";
206
+ /*
207
+ * Punctuation that may sit between a qualifier word and its "#": "incidents:
208
+ * #1", "alerts (#2", "alert [#3]" and, where the raw markdown is scanned,
209
+ * "**Alerts** #4". Bounded so a stray word far back is never read.
210
+ */
211
+ const QUALIFIER_GAP_PUNCTUATION: string = ":([*_";
212
+ const MAX_QUALIFIER_GAP_PUNCTUATION: number = 4;
213
+ /*
214
+ * Words that put a "#N" in some other numbering: a scheduled maintenance
215
+ * event, an episode, a pull request, a build, a list position. The number is
216
+ * not an incident or an alert, so it must not fall back to the subject's
217
+ * kind and link to whichever record happens to share it.
218
+ */
219
+ const NON_EVENT_QUALIFIER_WORDS: Set<string> = new Set<string>([
220
+ "maintenance",
221
+ "maintenances",
222
+ "episode",
223
+ "episodes",
224
+ "pr",
225
+ "prs",
226
+ "mr",
227
+ "mrs",
228
+ "pull",
229
+ "request",
230
+ "requests",
231
+ "issue",
232
+ "issues",
233
+ "ticket",
234
+ "tickets",
235
+ "bug",
236
+ "bugs",
237
+ "commit",
238
+ "commits",
239
+ "build",
240
+ "builds",
241
+ "deploy",
242
+ "deploys",
243
+ "deployment",
244
+ "deployments",
245
+ "release",
246
+ "releases",
247
+ "version",
248
+ "versions",
249
+ "run",
250
+ "runs",
251
+ "job",
252
+ "jobs",
253
+ "pipeline",
254
+ "pipelines",
255
+ "step",
256
+ "steps",
257
+ "attempt",
258
+ "attempts",
259
+ "retry",
260
+ "retries",
261
+ "item",
262
+ "items",
263
+ "line",
264
+ "lines",
265
+ "row",
266
+ "rows",
267
+ "port",
268
+ "ports",
269
+ "pod",
270
+ "pods",
271
+ "replica",
272
+ "replicas",
273
+ "node",
274
+ "nodes",
275
+ "shard",
276
+ "shards",
277
+ "worker",
278
+ "workers",
279
+ "instance",
280
+ "instances",
281
+ "page",
282
+ "pages",
283
+ "phase",
284
+ "phases",
285
+ "stage",
286
+ "stages",
287
+ "option",
288
+ "options",
289
+ "rank",
290
+ "priority",
291
+ "no",
292
+ "number",
293
+ "monitor",
294
+ "monitors",
295
+ ]);
296
+ // Longer than any qualifier or non-event word: never worth lower-casing.
297
+ const MAX_QUALIFIER_WORD_LENGTH: number = 16;
298
+ const SIBLING_SEPARATOR_REGEX: RegExp =
299
+ /^(?:[ \t\n]*,[ \t\n]*(?:(?:and|or)[ \t\n]+)?|[ \t\n]+(?:and|or)[ \t\n]+)$/i;
300
+ // ", and " with generous whitespace; a longer gap is never a list separator.
301
+ const MAX_SIBLING_SEPARATOR_LENGTH: number = 16;
302
+
303
+ /*
304
+ * Stands in for an inline code span while scanning for references: not
305
+ * whitespace (so a qualifier cannot reach across it), not a word character
306
+ * and not one of the characters that block a reference.
307
+ */
308
+ const CODE_SPAN_PLACEHOLDER: string = String.fromCharCode(0xfffc);
309
+
310
+ interface LabelLineMatch {
311
+ // null: not a known label (it may still open an "Other" section).
312
+ kind: LabelKind | null;
313
+ title: string;
314
+ inlineBody: string;
315
+ /*
316
+ * A heading, or a bold-only line that reads like a label. Only these may
317
+ * open an "Other" section or an Evidence checked block.
318
+ */
319
+ isBare: boolean;
320
+ // The ATX heading level (1-6); 0 or undefined for a bold or plain label.
321
+ headingLevel?: number | undefined;
322
+ }
323
+
324
+ interface SectionStart {
325
+ index: number;
326
+ kind: InvestigationReportSectionKind;
327
+ title: string;
328
+ inlineBody: string;
329
+ headingLevel: number;
330
+ /*
331
+ * Set when the section's label turned out to be a group heading: it had no
332
+ * body of its own and was directly followed by another label. The depth
333
+ * rank of that first sub-label (see getLabelRank); later labels at that
334
+ * rank or deeper stay inside the section too.
335
+ */
336
+ subLabelRank?: number | undefined;
337
+ }
338
+
339
+ interface SectionCandidate {
340
+ index: number;
341
+ match: LabelLineMatch;
342
+ }
343
+
344
+ interface FooterMatch {
345
+ breakIndex: number;
346
+ footer: InvestigationReportFooter;
347
+ }
348
+
349
+ function normaliseLineEndings(markdown: string): string {
350
+ if (typeof markdown !== "string") {
351
+ return "";
352
+ }
353
+
354
+ return markdown.replace(LINE_ENDING_REGEX, "\n");
355
+ }
356
+
357
+ /*
358
+ * Marks every line that belongs to a fenced code block, fence lines
359
+ * included. Nothing inside a fence may start a section, a footer or an
360
+ * evidence block. An unclosed fence runs to the end of the document, as it
361
+ * does when the markdown is rendered.
362
+ */
363
+ function markFencedLines(lines: Array<string>): Array<boolean> {
364
+ const fenced: Array<boolean> = [];
365
+ let openFence: string | null = null;
366
+
367
+ for (const line of lines) {
368
+ if (openFence === null) {
369
+ const open: RegExpExecArray | null = FENCE_OPEN_REGEX.exec(line);
370
+ const marker: string = open?.[1] || "";
371
+ const info: string = open?.[2] || "";
372
+
373
+ // A backtick fence's info string cannot contain a backtick ("```a```" is inline code).
374
+ if (marker && !(marker.charAt(0) === "`" && info.includes("`"))) {
375
+ openFence = marker;
376
+ fenced.push(true);
377
+ continue;
378
+ }
379
+
380
+ fenced.push(false);
381
+ continue;
382
+ }
383
+
384
+ fenced.push(true);
385
+
386
+ const close: RegExpExecArray | null = FENCE_CLOSE_REGEX.exec(line);
387
+ const closeMarker: string = close?.[1] || "";
388
+
389
+ if (
390
+ closeMarker &&
391
+ closeMarker.charAt(0) === openFence.charAt(0) &&
392
+ closeMarker.length >= openFence.length
393
+ ) {
394
+ openFence = null;
395
+ }
396
+ }
397
+
398
+ return fenced;
399
+ }
400
+
401
+ // Emoji and pictographic symbols a model likes to put in front of a heading.
402
+ function isDecorativeCodePoint(codePoint: number): boolean {
403
+ return (
404
+ (codePoint >= 0x2000 && codePoint <= 0x2bff) ||
405
+ codePoint >= 0x1f000 ||
406
+ codePoint === 0xfe0f ||
407
+ codePoint === 0x200d
408
+ );
409
+ }
410
+
411
+ function stripLeadingDecoration(label: string): string {
412
+ let cursor: number = 0;
413
+
414
+ while (cursor < label.length) {
415
+ const codePoint: number = label.codePointAt(cursor) || 0;
416
+ const character: string = String.fromCodePoint(codePoint);
417
+
418
+ if (
419
+ !WHITESPACE_CHARACTER_REGEX.test(character) &&
420
+ !isDecorativeCodePoint(codePoint)
421
+ ) {
422
+ break;
423
+ }
424
+
425
+ cursor += character.length;
426
+ }
427
+
428
+ return label.slice(cursor).replace(LEADING_NUMBERING_REGEX, "");
429
+ }
430
+
431
+ /*
432
+ * The trailing-edge helpers below are plain string scans on purpose. The
433
+ * obvious regexes (/[ \t]*:[ \t]*$/, /(?:^|[ \t]+)#+[ \t]*$/ ...) backtrack
434
+ * quadratically over a long run of spaces, and this text is model-authored:
435
+ * one 20 KB line froze the parser for a minute when they were regexes.
436
+ */
437
+ function endsWithColon(value: string): boolean {
438
+ return value.trimEnd().endsWith(":");
439
+ }
440
+
441
+ function stripTrailingColon(value: string): string {
442
+ const trimmed: string = value.trimEnd();
443
+
444
+ return trimmed.endsWith(":") ? trimmed.slice(0, -1).trimEnd() : trimmed;
445
+ }
446
+
447
+ // "Most likely root cause (medium confidence)" -> "Most likely root cause".
448
+ function stripTrailingParenthetical(value: string): string {
449
+ const trimmed: string = value.trimEnd();
450
+
451
+ if (!trimmed.endsWith(")")) {
452
+ return trimmed;
453
+ }
454
+
455
+ const open: number = trimmed.lastIndexOf("(");
456
+
457
+ if (open === -1 || trimmed.slice(open + 1, -1).includes(")")) {
458
+ return trimmed;
459
+ }
460
+
461
+ return trimmed.slice(0, open).trimEnd();
462
+ }
463
+
464
+ // "## Summary ##" -> "Summary"; "## C#" keeps its hash.
465
+ function stripAtxClosingSequence(value: string): string {
466
+ const trimmed: string = value.trimEnd();
467
+ let end: number = trimmed.length;
468
+
469
+ while (end > 0 && trimmed.charAt(end - 1) === "#") {
470
+ end--;
471
+ }
472
+
473
+ if (end === trimmed.length) {
474
+ return trimmed;
475
+ }
476
+
477
+ if (end === 0) {
478
+ return "";
479
+ }
480
+
481
+ const before: string = trimmed.charAt(end - 1);
482
+
483
+ return before === " " || before === "\t"
484
+ ? trimmed.slice(0, end).trimEnd()
485
+ : trimmed;
486
+ }
487
+
488
+ /*
489
+ * Splits "Summary — body" / "Summary – body" / "Summary - body" at the first
490
+ * dash separator. A hyphen only separates with whitespace on both sides, so
491
+ * "on-call" never splits.
492
+ */
493
+ function splitAtFirstDash(
494
+ value: string,
495
+ ): { label: string; rest: string } | null {
496
+ for (let index: number = 0; index < value.length; index++) {
497
+ const character: string = value.charAt(index);
498
+
499
+ if (character === "—" || character === "–") {
500
+ return { label: value.slice(0, index), rest: value.slice(index + 1) };
501
+ }
502
+
503
+ if (character !== "-" || index === 0) {
504
+ continue;
505
+ }
506
+
507
+ const previous: string = value.charAt(index - 1);
508
+ let after: number = index + 1;
509
+
510
+ if (value.charAt(after) === "-") {
511
+ after++;
512
+ }
513
+
514
+ const next: string = value.charAt(after);
515
+
516
+ if (
517
+ (previous === " " || previous === "\t") &&
518
+ (next === " " || next === "\t")
519
+ ) {
520
+ return { label: value.slice(0, index), rest: value.slice(after) };
521
+ }
522
+ }
523
+
524
+ return null;
525
+ }
526
+
527
+ /*
528
+ * Removes wrapping emphasis and a trailing colon, inside or outside the
529
+ * emphasis: "**Summary:**", "**Summary**:", "__Summary__", "***Summary***".
530
+ */
531
+ function cleanLabel(raw: string): string {
532
+ let label: string = stripTrailingColon(raw.trim());
533
+
534
+ for (const delimiter of ["**", "__", "*", "_"]) {
535
+ const isWrapped: boolean =
536
+ label.length > delimiter.length * 2 &&
537
+ label.startsWith(delimiter) &&
538
+ label.endsWith(delimiter) &&
539
+ !label
540
+ .slice(delimiter.length, label.length - delimiter.length)
541
+ .includes(delimiter);
542
+
543
+ if (isWrapped) {
544
+ label = stripTrailingColon(
545
+ label.slice(delimiter.length, label.length - delimiter.length).trim(),
546
+ );
547
+ }
548
+ }
549
+
550
+ return label;
551
+ }
552
+
553
+ function tidyTitle(label: string): string {
554
+ const collapsed: string = label.replace(WHITESPACE_RUN_REGEX, " ").trim();
555
+ const stripped: string = stripLeadingDecoration(collapsed).trim();
556
+
557
+ return stripped || collapsed;
558
+ }
559
+
560
+ function lookupLabel(label: string): LabelKind | null {
561
+ // Every known label is short; anything longer is prose.
562
+ if (label.length > MAX_KNOWN_LABEL_INPUT_LENGTH) {
563
+ return null;
564
+ }
565
+
566
+ const key: string = stripTrailingColon(
567
+ stripTrailingParenthetical(stripLeadingDecoration(label)),
568
+ )
569
+ .replace(WHITESPACE_RUN_REGEX, " ")
570
+ .trim()
571
+ .toLowerCase();
572
+
573
+ return KNOWN_LABELS.get(key) || null;
574
+ }
575
+
576
+ function looksLikeBoldLabel(title: string): boolean {
577
+ return (
578
+ HAS_LABEL_TEXT_REGEX.test(title) &&
579
+ title.length <= MAX_BOLD_LABEL_LENGTH &&
580
+ !ENDS_WITH_SENTENCE_PUNCTUATION_REGEX.test(title) &&
581
+ !CITATION_MARKER_TEST_REGEX.test(title)
582
+ );
583
+ }
584
+
585
+ /*
586
+ * "**Label**", "**Label:**", "**Label** — body", "**Label:** body",
587
+ * "**Label** (medium confidence) — body". A bold phrase followed by prose
588
+ * with no separator ("**Root cause** is unclear") is not a label line.
589
+ */
590
+ function matchStrongLead(content: string): LabelLineMatch | null {
591
+ const lead: RegExpExecArray | null = STRONG_LEAD_REGEX.exec(content);
592
+
593
+ if (!lead) {
594
+ return null;
595
+ }
596
+
597
+ const inner: string = lead[3] || "";
598
+ let rest: string = lead[4] || "";
599
+ const label: string = cleanLabel(inner);
600
+
601
+ if (!label) {
602
+ return null;
603
+ }
604
+
605
+ const innerEndsWithColon: boolean = endsWithColon(inner);
606
+ let qualifier: string = "";
607
+ const parenthetical: RegExpExecArray | null =
608
+ LEADING_PARENTHETICAL_REGEX.exec(rest);
609
+
610
+ if (parenthetical) {
611
+ qualifier = parenthetical[1] || "";
612
+ rest = rest.slice(parenthetical[0].length);
613
+ }
614
+
615
+ const restTrimmed: string = rest.trim();
616
+ let inlineBody: string = "";
617
+ let isBare: boolean = false;
618
+
619
+ if (restTrimmed === "" || restTrimmed === ":") {
620
+ isBare = qualifier === "";
621
+ } else {
622
+ const separator: RegExpExecArray | null =
623
+ SEPARATOR_PREFIX_REGEX.exec(restTrimmed);
624
+
625
+ if (separator) {
626
+ inlineBody = restTrimmed.slice(separator[0].length).trim();
627
+ } else if (innerEndsWithColon) {
628
+ // "**Root cause:** body" and "**Root cause:** (medium confidence) body".
629
+ inlineBody = restTrimmed;
630
+ } else {
631
+ return null;
632
+ }
633
+ }
634
+
635
+ const title: string = tidyTitle(qualifier ? `${label} ${qualifier}` : label);
636
+ const kind: LabelKind | null = lookupLabel(label);
637
+
638
+ isBare = isBare && looksLikeBoldLabel(title);
639
+
640
+ if (kind === null && !isBare) {
641
+ return null;
642
+ }
643
+
644
+ return { kind, title, inlineBody, isBare };
645
+ }
646
+
647
+ // "Summary: body" — only for known labels.
648
+ function matchPlainLabel(content: string): LabelLineMatch | null {
649
+ const colon: number = content.indexOf(":");
650
+
651
+ if (colon <= 0) {
652
+ return null;
653
+ }
654
+
655
+ const label: string = content.slice(0, colon).trim();
656
+
657
+ if (!PLAIN_LABEL_REGEX.test(label)) {
658
+ return null;
659
+ }
660
+
661
+ const kind: LabelKind | null = lookupLabel(label);
662
+
663
+ if (kind === null) {
664
+ return null;
665
+ }
666
+
667
+ const inlineBody: string = content.slice(colon + 1).trim();
668
+
669
+ return {
670
+ kind,
671
+ title: tidyTitle(label),
672
+ inlineBody,
673
+ isBare: inlineBody === "",
674
+ };
675
+ }
676
+
677
+ function matchHeading(headingText: string): LabelLineMatch | null {
678
+ const text: string = stripAtxClosingSequence(headingText).trim();
679
+
680
+ if (!text) {
681
+ return null;
682
+ }
683
+
684
+ const full: string = cleanLabel(text);
685
+ const fullKind: LabelKind | null = lookupLabel(full);
686
+
687
+ if (fullKind !== null) {
688
+ return {
689
+ kind: fullKind,
690
+ title: tidyTitle(full),
691
+ inlineBody: "",
692
+ isBare: true,
693
+ };
694
+ }
695
+
696
+ // "## Summary: body", "## **Summary** — body", "## Summary — body".
697
+ const lead: LabelLineMatch | null =
698
+ text.startsWith("**") || text.startsWith("__")
699
+ ? matchStrongLead(text)
700
+ : matchPlainLabel(text);
701
+
702
+ if (lead && lead.kind !== null) {
703
+ return { ...lead, isBare: lead.inlineBody === "" };
704
+ }
705
+
706
+ const dash: { label: string; rest: string } | null = splitAtFirstDash(text);
707
+
708
+ if (dash) {
709
+ const dashLabel: string = cleanLabel(dash.label);
710
+ const dashKind: LabelKind | null = lookupLabel(dashLabel);
711
+ const dashBody: string = dash.rest.trim();
712
+
713
+ if (dashKind !== null) {
714
+ return {
715
+ kind: dashKind,
716
+ title: tidyTitle(dashLabel),
717
+ inlineBody: dashBody,
718
+ isBare: dashBody === "",
719
+ };
720
+ }
721
+ }
722
+
723
+ // Any other heading may open an "Other" section, titled as written.
724
+ return {
725
+ kind: null,
726
+ title: tidyTitle(full || text),
727
+ inlineBody: "",
728
+ isBare: true,
729
+ };
730
+ }
731
+
732
+ // "### Evidence" -> 3. Only called on a line ATX_HEADING_REGEX accepted.
733
+ function countLeadingHashes(line: string): number {
734
+ const content: string = line.trimStart();
735
+ let level: number = 0;
736
+
737
+ while (level < content.length && content.charAt(level) === "#") {
738
+ level++;
739
+ }
740
+
741
+ return level;
742
+ }
743
+
744
+ /*
745
+ * How deep a label sits in the outline: its heading level, with bold and
746
+ * plain labels below every heading.
747
+ */
748
+ function getLabelRank(match: LabelLineMatch): number {
749
+ return match.headingLevel || BOLD_LABEL_RANK;
750
+ }
751
+
752
+ function matchSectionStart(line: string): LabelLineMatch | null {
753
+ // "*****" is a thematic break, not an empty bold label.
754
+ if (INDENTED_CODE_REGEX.test(line) || THEMATIC_BREAK_REGEX.test(line)) {
755
+ return null;
756
+ }
757
+
758
+ const heading: RegExpExecArray | null = ATX_HEADING_REGEX.exec(line);
759
+
760
+ if (heading) {
761
+ const headingMatch: LabelLineMatch | null = matchHeading(heading[1] || "");
762
+
763
+ return headingMatch
764
+ ? { ...headingMatch, headingLevel: countLeadingHashes(line) }
765
+ : null;
766
+ }
767
+
768
+ const content: string = line.trim();
769
+
770
+ if (!content) {
771
+ return null;
772
+ }
773
+
774
+ if (content.startsWith("**") || content.startsWith("__")) {
775
+ return matchStrongLead(content);
776
+ }
777
+
778
+ return matchPlainLabel(content);
779
+ }
780
+
781
+ function isBrandHeading(line: string): boolean {
782
+ const heading: RegExpExecArray | null = ATX_HEADING_REGEX.exec(line);
783
+
784
+ return Boolean(
785
+ heading && (heading[1] || "").toLowerCase().includes(BRAND_HEADING_MARKER),
786
+ );
787
+ }
788
+
789
+ /*
790
+ * The footer is the LAST thematic break (outside code) followed by nothing
791
+ * but one emphasis paragraph starting with the OneUptime AI footer text.
792
+ */
793
+ function findFooter(
794
+ lines: Array<string>,
795
+ fenced: Array<boolean>,
796
+ ): FooterMatch | null {
797
+ let breakIndex: number = -1;
798
+
799
+ for (let index: number = lines.length - 1; index >= 0; index--) {
800
+ if (!fenced[index] && THEMATIC_BREAK_REGEX.test(lines[index] || "")) {
801
+ breakIndex = index;
802
+ break;
803
+ }
804
+ }
805
+
806
+ if (breakIndex < 0) {
807
+ return null;
808
+ }
809
+
810
+ const trailing: string = lines
811
+ .slice(breakIndex + 1)
812
+ .join("\n")
813
+ .trim();
814
+
815
+ if (!trailing || BLANK_LINE_SPLIT_REGEX.test(trailing)) {
816
+ return null;
817
+ }
818
+
819
+ const paragraph: string = trailing
820
+ .split("\n")
821
+ .map((line: string): string => {
822
+ return line.trim();
823
+ })
824
+ .join(" ");
825
+
826
+ const emphasis: RegExpExecArray | null =
827
+ FOOTER_EMPHASIS_REGEX.exec(paragraph);
828
+ const text: string = (emphasis?.[2] || "").trim();
829
+
830
+ if (!text.startsWith(FOOTER_PREFIX)) {
831
+ return null;
832
+ }
833
+
834
+ const footer: InvestigationReportFooter = { text };
835
+
836
+ const modelName: string | undefined = readFooterModelName(text);
837
+
838
+ if (modelName) {
839
+ footer.modelName = modelName;
840
+ }
841
+
842
+ const queryCount: number | undefined = readFooterQueryCount(text);
843
+
844
+ if (queryCount !== undefined) {
845
+ footer.queryCount = queryCount;
846
+ }
847
+
848
+ return { breakIndex, footer };
849
+ }
850
+
851
+ /*
852
+ * "... telemetry using gpt-4.1-mini. This is ..." -> "gpt-4.1-mini". The
853
+ * name ends at the first "." followed by whitespace or the end, so dots
854
+ * inside a model name survive. A scan rather than a lazy regex, which
855
+ * rescans the rest of the text for every "using".
856
+ */
857
+ function readFooterModelName(text: string): string | undefined {
858
+ const using: RegExpExecArray | null = FOOTER_USING_REGEX.exec(text);
859
+
860
+ if (!using) {
861
+ return undefined;
862
+ }
863
+
864
+ const start: number = using.index + using[0].length;
865
+
866
+ for (let index: number = start; index < text.length; index++) {
867
+ if (
868
+ text.charAt(index) === "." &&
869
+ (index + 1 === text.length ||
870
+ WHITESPACE_CHARACTER_REGEX.test(text.charAt(index + 1)))
871
+ ) {
872
+ return text.slice(start, index).trim() || undefined;
873
+ }
874
+ }
875
+
876
+ return undefined;
877
+ }
878
+
879
+ // "... 7 queries run ..." / "... 1 query run ..." -> 7 / 1.
880
+ function readFooterQueryCount(text: string): number | undefined {
881
+ const queries: RegExpExecArray | null = FOOTER_QUERIES_RUN_REGEX.exec(text);
882
+
883
+ if (!queries) {
884
+ return undefined;
885
+ }
886
+
887
+ let cursor: number = queries.index - 1;
888
+ const spaceEnd: number = cursor;
889
+
890
+ while (
891
+ cursor >= 0 &&
892
+ (text.charAt(cursor) === " " || text.charAt(cursor) === "\t")
893
+ ) {
894
+ cursor--;
895
+ }
896
+
897
+ if (cursor === spaceEnd) {
898
+ return undefined;
899
+ }
900
+
901
+ const digitsEnd: number = cursor + 1;
902
+
903
+ while (cursor >= 0 && DIGIT_REGEX.test(text.charAt(cursor))) {
904
+ cursor--;
905
+ }
906
+
907
+ const digits: string = text.slice(cursor + 1, digitsEnd);
908
+
909
+ // The engine's tool-call budget is tiny; a huge number is not a count.
910
+ if (!digits || digits.length > 9) {
911
+ return undefined;
912
+ }
913
+
914
+ return parseInt(digits, 10);
915
+ }
916
+
917
+ /*
918
+ * The lines an "Evidence checked" block owns: its label line, then list
919
+ * items (with their indented continuations) and the blank lines between
920
+ * them. The first other line ends the block, so prose that follows it is
921
+ * never swallowed.
922
+ */
923
+ function findEvidenceCheckedBlockEnd(
924
+ lines: Array<string>,
925
+ fenced: Array<boolean>,
926
+ labelIndex: number,
927
+ contentEnd: number,
928
+ ): number {
929
+ let end: number = labelIndex + 1;
930
+ let cursor: number = labelIndex + 1;
931
+ let insideList: boolean = false;
932
+
933
+ while (cursor < contentEnd) {
934
+ const line: string = lines[cursor] || "";
935
+
936
+ if (fenced[cursor]) {
937
+ break;
938
+ }
939
+
940
+ if (!line.trim()) {
941
+ cursor++;
942
+ continue;
943
+ }
944
+
945
+ if (LIST_ITEM_REGEX.test(line)) {
946
+ insideList = true;
947
+ } else if (!(insideList && LIST_CONTINUATION_REGEX.test(line))) {
948
+ break;
949
+ }
950
+
951
+ cursor++;
952
+ end = cursor;
953
+ }
954
+
955
+ return end;
956
+ }
957
+
958
+ function parseEvidenceCheckedEntries(
959
+ lines: Array<string>,
960
+ ): Array<InvestigationEvidenceCheckedEntry> {
961
+ const entries: Array<InvestigationEvidenceCheckedEntry> = [];
962
+ const seen: Set<string> = new Set<string>();
963
+
964
+ for (const line of lines) {
965
+ if (line.length > MAX_EVIDENCE_CHECKED_LINE_LENGTH) {
966
+ continue;
967
+ }
968
+
969
+ const entry: RegExpExecArray | null =
970
+ EVIDENCE_CHECKED_ENTRY_REGEX.exec(line);
971
+
972
+ if (!entry) {
973
+ continue;
974
+ }
975
+
976
+ const citationId: string = (entry[1] || "").toUpperCase();
977
+ const label: string = (entry[2] || "").trim();
978
+
979
+ if (!citationId || !label || seen.has(citationId)) {
980
+ continue;
981
+ }
982
+
983
+ seen.add(citationId);
984
+ entries.push({
985
+ citationId,
986
+ label,
987
+ rowCount: parseInt(entry[3] || "0", 10),
988
+ });
989
+ }
990
+
991
+ return entries;
992
+ }
993
+
994
+ function createEmptyReport(): ParsedInvestigationReport {
995
+ return {
996
+ isStructured: false,
997
+ preamble: "",
998
+ sections: [],
999
+ summary: undefined,
1000
+ rootCause: undefined,
1001
+ evidence: undefined,
1002
+ nextSteps: undefined,
1003
+ evidenceChecked: [],
1004
+ footer: undefined,
1005
+ bodyMarkdown: "",
1006
+ };
1007
+ }
1008
+
1009
+ /*
1010
+ * Joins the kept lines of [from, to). Untrimmed on purpose: a section body
1011
+ * is appended to its inline first line, and the blank line between them is
1012
+ * what keeps them separate paragraphs.
1013
+ */
1014
+ function joinKeptLines(
1015
+ lines: Array<string>,
1016
+ kept: Array<boolean>,
1017
+ from: number,
1018
+ to: number,
1019
+ ): string {
1020
+ const output: Array<string> = [];
1021
+ let lastWasRemoved: boolean = false;
1022
+
1023
+ for (let index: number = from; index < to; index++) {
1024
+ if (!kept[index]) {
1025
+ lastWasRemoved = true;
1026
+ continue;
1027
+ }
1028
+
1029
+ const line: string = lines[index] || "";
1030
+ const previous: string | undefined = output[output.length - 1];
1031
+
1032
+ /*
1033
+ * Keep a blank line where a block was cut out, so the text on either
1034
+ * side cannot merge into one paragraph.
1035
+ */
1036
+ if (
1037
+ lastWasRemoved &&
1038
+ line.trim() !== "" &&
1039
+ (previous === undefined || previous.trim() !== "")
1040
+ ) {
1041
+ output.push("");
1042
+ }
1043
+
1044
+ lastWasRemoved = false;
1045
+ output.push(line);
1046
+ }
1047
+
1048
+ return output.join("\n");
1049
+ }
1050
+
1051
+ /*
1052
+ * Every kept line outside code that reads like a section label, in order.
1053
+ * An indented line inside a list continues a list item ("1. Roll back", then
1054
+ * " Evidence: pool at 100%"), so it never starts a section: that would cut
1055
+ * the list in two and could hand the rest of it to another section kind.
1056
+ */
1057
+ function findSectionCandidates(
1058
+ lines: Array<string>,
1059
+ fenced: Array<boolean>,
1060
+ kept: Array<boolean>,
1061
+ contentEnd: number,
1062
+ ): Array<SectionCandidate> {
1063
+ const candidates: Array<SectionCandidate> = [];
1064
+ let insideList: boolean = false;
1065
+
1066
+ for (let index: number = 0; index < contentEnd; index++) {
1067
+ const line: string = lines[index] || "";
1068
+
1069
+ // Blank lines keep the list context: a list item may span paragraphs.
1070
+ if (!kept[index] || fenced[index] || !line.trim()) {
1071
+ continue;
1072
+ }
1073
+
1074
+ const isIndented: boolean = LEADING_WHITESPACE_REGEX.test(line);
1075
+
1076
+ if (THEMATIC_BREAK_REGEX.test(line)) {
1077
+ insideList = false;
1078
+ } else if (LIST_ITEM_REGEX.test(line)) {
1079
+ insideList = true;
1080
+ } else if (!isIndented) {
1081
+ insideList = false;
1082
+ } else if (insideList) {
1083
+ continue;
1084
+ }
1085
+
1086
+ const match: LabelLineMatch | null = matchSectionStart(line);
1087
+
1088
+ if (match) {
1089
+ candidates.push({ index, match });
1090
+ }
1091
+ }
1092
+
1093
+ return candidates;
1094
+ }
1095
+
1096
+ /*
1097
+ * Turns label candidates into the section starts the report is cut at.
1098
+ *
1099
+ * Recognised labels always start a section (one with no body is dropped
1100
+ * later, as before). Unknown headings and bold-only labels, including a
1101
+ * model-authored "Evidence checked", start an "Other" section only once the
1102
+ * structured part of the note has begun, and only when they are not part of
1103
+ * the section already open:
1104
+ *
1105
+ * - a heading deeper than the recognised heading that opened the section
1106
+ * ("## Evidence" then "### Metrics") belongs to it;
1107
+ * - a label directly under a label with no body of its own makes that
1108
+ * label a group heading ("## Most likely root cause" then "### Pool
1109
+ * exhaustion"), and the group's later labels at the same depth or deeper
1110
+ * stay in it too;
1111
+ * - a label with nothing under it ("**Confidence: medium**" at the end) is
1112
+ * a line of the open section, not an empty section of its own.
1113
+ *
1114
+ * So a section title is never lost because its body opens with a sub-label,
1115
+ * and no content is ever dropped.
1116
+ */
1117
+ function resolveSectionStarts(
1118
+ lines: Array<string>,
1119
+ kept: Array<boolean>,
1120
+ allCandidates: Array<SectionCandidate>,
1121
+ contentEnd: number,
1122
+ ): Array<SectionStart> {
1123
+ /*
1124
+ * Kept lines with text before each index, so "is there any content in
1125
+ * [from, to)" is O(1) however many labels a section swallows.
1126
+ */
1127
+ const contentBefore: Array<number> = [0];
1128
+
1129
+ for (let index: number = 0; index < lines.length; index++) {
1130
+ const hasText: boolean =
1131
+ kept[index] === true && (lines[index] || "").trim() !== "";
1132
+ contentBefore.push((contentBefore[index] || 0) + (hasText ? 1 : 0));
1133
+ }
1134
+
1135
+ const hasKeptContent: (from: number, to: number) => boolean = (
1136
+ from: number,
1137
+ to: number,
1138
+ ): boolean => {
1139
+ return (
1140
+ to > from && (contentBefore[to] || 0) - (contentBefore[from] || 0) > 0
1141
+ );
1142
+ };
1143
+
1144
+ const isRecognised: (match: LabelLineMatch) => boolean = (
1145
+ match: LabelLineMatch,
1146
+ ): boolean => {
1147
+ return match.kind !== null && match.kind !== "EvidenceChecked";
1148
+ };
1149
+
1150
+ /*
1151
+ * A heading at the same or a shallower level than the heading before it is
1152
+ * its peer, never its sub-label ("## Impact" then "## Timeline").
1153
+ */
1154
+ const isPeerHeading: (openLevel: number, match: LabelLineMatch) => boolean = (
1155
+ openLevel: number,
1156
+ match: LabelLineMatch,
1157
+ ): boolean => {
1158
+ const level: number = match.headingLevel || 0;
1159
+
1160
+ return openLevel > 0 && level > 0 && level <= openLevel;
1161
+ };
1162
+
1163
+ // A label with inline text ("Evidence checked: logs") is just a line of prose.
1164
+ const candidates: Array<SectionCandidate> = allCandidates.filter(
1165
+ (candidate: SectionCandidate): boolean => {
1166
+ return (
1167
+ kept[candidate.index] === true &&
1168
+ (isRecognised(candidate.match) || candidate.match.isBare)
1169
+ );
1170
+ },
1171
+ );
1172
+
1173
+ /*
1174
+ * Whether an "Other" label at this position has a body: text before the
1175
+ * next label, or a next label that would become its sub-label and itself
1176
+ * has a body. Walked backwards so a run of labels costs linear time.
1177
+ */
1178
+ const hasBody: Array<boolean> = candidates.map((): boolean => {
1179
+ return false;
1180
+ });
1181
+
1182
+ for (
1183
+ let position: number = candidates.length - 1;
1184
+ position >= 0;
1185
+ position--
1186
+ ) {
1187
+ const candidate: SectionCandidate | undefined = candidates[position];
1188
+ const next: SectionCandidate | undefined = candidates[position + 1];
1189
+
1190
+ if (!candidate) {
1191
+ continue;
1192
+ }
1193
+
1194
+ hasBody[position] =
1195
+ hasKeptContent(candidate.index + 1, next ? next.index : contentEnd) ||
1196
+ Boolean(
1197
+ next &&
1198
+ !isRecognised(next.match) &&
1199
+ !isPeerHeading(candidate.match.headingLevel || 0, next.match) &&
1200
+ hasBody[position + 1],
1201
+ );
1202
+ }
1203
+
1204
+ const starts: Array<SectionStart> = [];
1205
+
1206
+ candidates.forEach((candidate: SectionCandidate, position: number): void => {
1207
+ const match: LabelLineMatch = candidate.match;
1208
+ const headingLevel: number = match.headingLevel || 0;
1209
+
1210
+ if (match.kind !== null && match.kind !== "EvidenceChecked") {
1211
+ starts.push({
1212
+ index: candidate.index,
1213
+ kind: match.kind,
1214
+ title: match.title,
1215
+ inlineBody: match.inlineBody,
1216
+ headingLevel,
1217
+ });
1218
+ return;
1219
+ }
1220
+
1221
+ const open: SectionStart | undefined = starts[starts.length - 1];
1222
+
1223
+ // Before the first recognised section, labels are preamble content.
1224
+ if (!open) {
1225
+ return;
1226
+ }
1227
+
1228
+ const rank: number = getLabelRank(match);
1229
+
1230
+ // A sub-heading of the recognised heading that opened the section.
1231
+ if (
1232
+ open.kind !== InvestigationReportSectionKind.Other &&
1233
+ open.headingLevel > 0 &&
1234
+ headingLevel > open.headingLevel
1235
+ ) {
1236
+ return;
1237
+ }
1238
+
1239
+ // The first label under a label with no body: that label is a group heading.
1240
+ if (
1241
+ open.inlineBody === "" &&
1242
+ !hasKeptContent(open.index + 1, candidate.index) &&
1243
+ !isPeerHeading(open.headingLevel, match)
1244
+ ) {
1245
+ open.subLabelRank = rank;
1246
+ return;
1247
+ }
1248
+
1249
+ // The group's later labels, at its sub-labels' depth or deeper.
1250
+ if (open.subLabelRank !== undefined && rank >= open.subLabelRank) {
1251
+ return;
1252
+ }
1253
+
1254
+ // A label with nothing under it is a line of the open section.
1255
+ if (!hasBody[position]) {
1256
+ return;
1257
+ }
1258
+
1259
+ starts.push({
1260
+ index: candidate.index,
1261
+ kind: InvestigationReportSectionKind.Other,
1262
+ title: match.title,
1263
+ inlineBody: "",
1264
+ headingLevel,
1265
+ });
1266
+ });
1267
+
1268
+ return starts;
1269
+ }
1270
+
1271
+ export function parseInvestigationReport(
1272
+ markdown: string,
1273
+ ): ParsedInvestigationReport {
1274
+ const normalised: string = normaliseLineEndings(markdown);
1275
+
1276
+ if (!normalised.trim()) {
1277
+ return createEmptyReport();
1278
+ }
1279
+
1280
+ const lines: Array<string> = normalised.split("\n");
1281
+ const fenced: Array<boolean> = markFencedLines(lines);
1282
+ const kept: Array<boolean> = lines.map((): boolean => {
1283
+ return true;
1284
+ });
1285
+
1286
+ // 1. The branded heading, when it is the first thing in the document.
1287
+ const firstContentIndex: number = lines.findIndex((line: string): boolean => {
1288
+ return line.trim() !== "";
1289
+ });
1290
+
1291
+ if (
1292
+ firstContentIndex >= 0 &&
1293
+ !fenced[firstContentIndex] &&
1294
+ isBrandHeading(lines[firstContentIndex] || "")
1295
+ ) {
1296
+ kept[firstContentIndex] = false;
1297
+ }
1298
+
1299
+ // 2. The footer.
1300
+ const footerMatch: FooterMatch | null = findFooter(lines, fenced);
1301
+ const contentEnd: number = footerMatch
1302
+ ? footerMatch.breakIndex
1303
+ : lines.length;
1304
+
1305
+ for (let index: number = contentEnd; index < lines.length; index++) {
1306
+ kept[index] = false;
1307
+ }
1308
+
1309
+ // 3. Every line that could start a section.
1310
+ const candidates: Array<SectionCandidate> = findSectionCandidates(
1311
+ lines,
1312
+ fenced,
1313
+ kept,
1314
+ contentEnd,
1315
+ );
1316
+
1317
+ /*
1318
+ * 4. Only the LAST bare "Evidence checked" label can be the server's list.
1319
+ * Earlier ones were written by the model and stay ordinary content.
1320
+ */
1321
+ let evidenceChecked: Array<InvestigationEvidenceCheckedEntry> = [];
1322
+ let evidenceCheckedIndex: number = -1;
1323
+
1324
+ for (const candidate of candidates) {
1325
+ if (candidate.match.kind === "EvidenceChecked" && candidate.match.isBare) {
1326
+ evidenceCheckedIndex = candidate.index;
1327
+ }
1328
+ }
1329
+
1330
+ if (evidenceCheckedIndex >= 0) {
1331
+ const blockEnd: number = findEvidenceCheckedBlockEnd(
1332
+ lines,
1333
+ fenced,
1334
+ evidenceCheckedIndex,
1335
+ contentEnd,
1336
+ );
1337
+ const entries: Array<InvestigationEvidenceCheckedEntry> =
1338
+ parseEvidenceCheckedEntries(
1339
+ lines.slice(evidenceCheckedIndex + 1, blockEnd),
1340
+ );
1341
+ const queryCount: number | undefined = footerMatch?.footer.queryCount;
1342
+
1343
+ /*
1344
+ * The server writes one entry per citation, at most one citation per
1345
+ * query, and no block at all when there are no citations. A footer
1346
+ * saying "0 queries run", or fewer queries than entries, therefore means
1347
+ * the model wrote this block: it stays in the body as model content.
1348
+ * Without a readable count (no footer, an older format) it is trusted.
1349
+ */
1350
+ const isServerBlock: boolean =
1351
+ queryCount === undefined ||
1352
+ (queryCount > 0 && entries.length <= queryCount);
1353
+
1354
+ if (isServerBlock) {
1355
+ evidenceChecked = entries;
1356
+
1357
+ for (
1358
+ let index: number = evidenceCheckedIndex;
1359
+ index < blockEnd;
1360
+ index++
1361
+ ) {
1362
+ kept[index] = false;
1363
+ }
1364
+ }
1365
+ }
1366
+
1367
+ // 5. Resolve candidates into section starts.
1368
+ const starts: Array<SectionStart> = resolveSectionStarts(
1369
+ lines,
1370
+ kept,
1371
+ candidates,
1372
+ contentEnd,
1373
+ );
1374
+
1375
+ const firstStart: SectionStart | undefined = starts[0];
1376
+ const preamble: string = joinKeptLines(
1377
+ lines,
1378
+ kept,
1379
+ 0,
1380
+ firstStart ? firstStart.index : contentEnd,
1381
+ ).trim();
1382
+
1383
+ const sections: Array<InvestigationReportSection> = [];
1384
+
1385
+ starts.forEach((start: SectionStart, position: number): void => {
1386
+ const next: SectionStart | undefined = starts[position + 1];
1387
+ const body: string = joinKeptLines(
1388
+ lines,
1389
+ kept,
1390
+ start.index + 1,
1391
+ next ? next.index : contentEnd,
1392
+ );
1393
+ const sectionMarkdown: string = `${start.inlineBody}\n${body}`.trim();
1394
+
1395
+ if (!sectionMarkdown) {
1396
+ return;
1397
+ }
1398
+
1399
+ sections.push({
1400
+ kind: start.kind,
1401
+ title: start.title,
1402
+ markdown: sectionMarkdown,
1403
+ });
1404
+ });
1405
+
1406
+ const firstOfKind: (
1407
+ kind: InvestigationReportSectionKind,
1408
+ ) => string | undefined = (
1409
+ kind: InvestigationReportSectionKind,
1410
+ ): string | undefined => {
1411
+ return sections.find((section: InvestigationReportSection): boolean => {
1412
+ return section.kind === kind;
1413
+ })?.markdown;
1414
+ };
1415
+
1416
+ const summary: string | undefined = firstOfKind(
1417
+ InvestigationReportSectionKind.Summary,
1418
+ );
1419
+ const rootCause: string | undefined = firstOfKind(
1420
+ InvestigationReportSectionKind.RootCause,
1421
+ );
1422
+
1423
+ return {
1424
+ isStructured: summary !== undefined || rootCause !== undefined,
1425
+ preamble,
1426
+ sections,
1427
+ summary,
1428
+ rootCause,
1429
+ evidence: firstOfKind(InvestigationReportSectionKind.Evidence),
1430
+ nextSteps: firstOfKind(InvestigationReportSectionKind.NextSteps),
1431
+ evidenceChecked,
1432
+ footer: footerMatch?.footer,
1433
+ bodyMarkdown: joinKeptLines(lines, kept, 0, lines.length).trim(),
1434
+ };
1435
+ }
1436
+
1437
+ type EventReferenceQualifier = "incident" | "alert" | "blocked";
1438
+
1439
+ /*
1440
+ * The qualifier word directly before a reference: "incident #12", "Alerts
1441
+ * #3", "prior incidents: #6954", "alerts (#100". "blocked" means a word from
1442
+ * another numbering ("PR #45"). Scans backwards over a bounded gap and word,
1443
+ * so the cost never grows with the text.
1444
+ */
1445
+ function readQualifierBefore(
1446
+ text: string,
1447
+ hashIndex: number,
1448
+ ): EventReferenceQualifier | null {
1449
+ let cursor: number = hashIndex - 1;
1450
+ let punctuationCount: number = 0;
1451
+
1452
+ while (cursor >= 0) {
1453
+ const character: string = text.charAt(cursor);
1454
+
1455
+ if (WHITESPACE_CHARACTER_REGEX.test(character)) {
1456
+ cursor--;
1457
+ continue;
1458
+ }
1459
+
1460
+ if (
1461
+ QUALIFIER_GAP_PUNCTUATION.includes(character) &&
1462
+ punctuationCount < MAX_QUALIFIER_GAP_PUNCTUATION
1463
+ ) {
1464
+ punctuationCount++;
1465
+ cursor--;
1466
+ continue;
1467
+ }
1468
+
1469
+ break;
1470
+ }
1471
+
1472
+ // Nothing between the word and "#" ("incident-#12" has a hyphen there).
1473
+ if (cursor === hashIndex - 1) {
1474
+ return null;
1475
+ }
1476
+
1477
+ const wordEnd: number = cursor + 1;
1478
+ const wordStartLimit: number = wordEnd - MAX_QUALIFIER_WORD_LENGTH - 1;
1479
+
1480
+ while (
1481
+ cursor >= 0 &&
1482
+ cursor >= wordStartLimit &&
1483
+ WORD_CHARACTER_REGEX.test(text.charAt(cursor))
1484
+ ) {
1485
+ cursor--;
1486
+ }
1487
+
1488
+ // The word runs on past the longest qualifier, so it is none of them.
1489
+ if (cursor >= 0 && WORD_CHARACTER_REGEX.test(text.charAt(cursor))) {
1490
+ return null;
1491
+ }
1492
+
1493
+ let wordStart: number = cursor + 1;
1494
+
1495
+ // "__Alerts__ #3": "_" is a word character, but here it is emphasis.
1496
+ while (wordStart < wordEnd && text.charAt(wordStart) === "_") {
1497
+ wordStart++;
1498
+ }
1499
+
1500
+ const word: string = text.slice(wordStart, wordEnd).toLowerCase();
1501
+
1502
+ if (word === "incident" || word === "incidents") {
1503
+ return "incident";
1504
+ }
1505
+
1506
+ if (word === "alert" || word === "alerts") {
1507
+ return "alert";
1508
+ }
1509
+
1510
+ if (NON_EVENT_QUALIFIER_WORDS.has(word)) {
1511
+ return "blocked";
1512
+ }
1513
+
1514
+ return null;
1515
+ }
1516
+
1517
+ interface ScannedEventReference {
1518
+ qualifier: EventReferenceQualifier | null;
1519
+ number: number;
1520
+ start: number;
1521
+ end: number;
1522
+ }
1523
+
1524
+ /*
1525
+ * Every "#123" in one text run with the qualifier it reads, "blocked" ones
1526
+ * included. tokenizeEventReferences documents the rules.
1527
+ */
1528
+ function scanEventReferences(text: string): Array<ScannedEventReference> {
1529
+ const scanned: Array<ScannedEventReference> = [];
1530
+
1531
+ if (typeof text !== "string" || !text.includes("#")) {
1532
+ return scanned;
1533
+ }
1534
+
1535
+ const referenceRegex: RegExp = new RegExp(EVENT_REFERENCE_PATTERN, "g");
1536
+ let match: RegExpExecArray | null = referenceRegex.exec(text);
1537
+
1538
+ while (match) {
1539
+ const start: number = match.index;
1540
+ const digits: string = match[1] || "";
1541
+ const before: string = start > 0 ? text.charAt(start - 1) : "";
1542
+ const isBlocked: boolean =
1543
+ before !== "" &&
1544
+ (WORD_CHARACTER_REGEX.test(before) ||
1545
+ FORBIDDEN_BEFORE_REFERENCE.includes(before));
1546
+
1547
+ if (!isBlocked && digits) {
1548
+ const end: number = start + 1 + digits.length;
1549
+ const previous: ScannedEventReference | undefined =
1550
+ scanned[scanned.length - 1];
1551
+ let qualifier: EventReferenceQualifier | null = readQualifierBefore(
1552
+ text,
1553
+ start,
1554
+ );
1555
+
1556
+ if (
1557
+ qualifier === null &&
1558
+ previous &&
1559
+ previous.qualifier !== null &&
1560
+ start - previous.end <= MAX_SIBLING_SEPARATOR_LENGTH &&
1561
+ SIBLING_SEPARATOR_REGEX.test(text.slice(previous.end, start))
1562
+ ) {
1563
+ qualifier = previous.qualifier;
1564
+ }
1565
+
1566
+ scanned.push({
1567
+ qualifier,
1568
+ number: parseInt(digits, 10),
1569
+ start,
1570
+ end,
1571
+ });
1572
+ }
1573
+
1574
+ match = referenceRegex.exec(text);
1575
+ }
1576
+
1577
+ return scanned;
1578
+ }
1579
+
1580
+ /*
1581
+ * Finds "#123" style references in plain text (one text run). A reference is
1582
+ * "#" + 1-9 digits, not preceded by a word character, "&", "/" or "#", and not
1583
+ * followed by a word character.
1584
+ *
1585
+ * An immediately preceding qualifier word ("incident", "incidents", "alert",
1586
+ * "alerts", case-insensitive) sets `kind`. It may be separated from the "#"
1587
+ * by whitespace and a little punctuation (":", "(", "[", "*", "_"). A
1588
+ * qualifier also carries to directly following comma/"and"/"or"-separated
1589
+ * siblings ("incidents #1, #2 and #3").
1590
+ *
1591
+ * A number after a word from another numbering ("scheduled maintenance #42",
1592
+ * "PR #45", "step #1") is not returned at all, and neither are its siblings
1593
+ * ("PRs #45, #46"): it is not an incident or alert of any kind.
1594
+ */
1595
+ export function tokenizeEventReferences(
1596
+ text: string,
1597
+ ): Array<InvestigationEventReferenceToken> {
1598
+ const tokens: Array<InvestigationEventReferenceToken> = [];
1599
+
1600
+ for (const reference of scanEventReferences(text)) {
1601
+ if (reference.qualifier === "blocked") {
1602
+ continue;
1603
+ }
1604
+
1605
+ tokens.push({
1606
+ kind: reference.qualifier,
1607
+ number: reference.number,
1608
+ start: reference.start,
1609
+ end: reference.end,
1610
+ });
1611
+ }
1612
+
1613
+ return tokens;
1614
+ }
1615
+
1616
+ /*
1617
+ * Replaces inline code spans with a placeholder so references inside them
1618
+ * are ignored. A span opens with a run of N backticks and closes at the next
1619
+ * run of exactly N; an unmatched run is literal text.
1620
+ */
1621
+ function maskInlineCode(text: string): string {
1622
+ if (!text.includes("`")) {
1623
+ return text;
1624
+ }
1625
+
1626
+ let output: string = "";
1627
+ let cursor: number = 0;
1628
+
1629
+ while (cursor < text.length) {
1630
+ const openStart: number = text.indexOf("`", cursor);
1631
+
1632
+ if (openStart === -1) {
1633
+ output += text.slice(cursor);
1634
+ break;
1635
+ }
1636
+
1637
+ let openEnd: number = openStart;
1638
+
1639
+ while (openEnd < text.length && text.charAt(openEnd) === "`") {
1640
+ openEnd++;
1641
+ }
1642
+
1643
+ const runLength: number = openEnd - openStart;
1644
+ let search: number = openEnd;
1645
+ let closeEnd: number = -1;
1646
+
1647
+ while (search < text.length) {
1648
+ const nextStart: number = text.indexOf("`", search);
1649
+
1650
+ if (nextStart === -1) {
1651
+ break;
1652
+ }
1653
+
1654
+ let nextEnd: number = nextStart;
1655
+
1656
+ while (nextEnd < text.length && text.charAt(nextEnd) === "`") {
1657
+ nextEnd++;
1658
+ }
1659
+
1660
+ if (nextEnd - nextStart === runLength) {
1661
+ closeEnd = nextEnd;
1662
+ break;
1663
+ }
1664
+
1665
+ search = nextEnd;
1666
+ }
1667
+
1668
+ if (closeEnd === -1) {
1669
+ output += text.slice(cursor, openEnd);
1670
+ cursor = openEnd;
1671
+ continue;
1672
+ }
1673
+
1674
+ output += text.slice(cursor, openStart) + CODE_SPAN_PLACEHOLDER;
1675
+ cursor = closeEnd;
1676
+ }
1677
+
1678
+ return output;
1679
+ }
1680
+
1681
+ /*
1682
+ * References across a whole markdown document, skipping fenced code blocks
1683
+ * and inline code spans. Returns unique {kind, number} pairs in first-seen
1684
+ * order, with null kinds replaced by `defaultKind`, capped at `limit`. An
1685
+ * unqualified number the document also qualifies as the other kind is left
1686
+ * out (see below). Callers resolving a posted report should pass its
1687
+ * bodyMarkdown, so the server's own evidence labels are not read as prose.
1688
+ */
1689
+ export function extractEventReferences(
1690
+ markdown: string,
1691
+ defaultKind: "incident" | "alert",
1692
+ limit: number = DEFAULT_EVENT_REFERENCE_LIMIT,
1693
+ ): Array<ExtractedInvestigationEventReference> {
1694
+ const references: Array<ExtractedInvestigationEventReference> = [];
1695
+ const maxReferences: number = Math.floor(limit);
1696
+
1697
+ if (!(maxReferences > 0)) {
1698
+ return references;
1699
+ }
1700
+
1701
+ const normalised: string = normaliseLineEndings(markdown);
1702
+
1703
+ if (!normalised.includes("#")) {
1704
+ return references;
1705
+ }
1706
+
1707
+ const lines: Array<string> = normalised.split("\n");
1708
+ const fenced: Array<boolean> = markFencedLines(lines);
1709
+
1710
+ /*
1711
+ * Group the non-code lines into blocks separated by fences and blank
1712
+ * lines: a code span never crosses either, so an unmatched backtick in one
1713
+ * paragraph cannot hide references in the next.
1714
+ */
1715
+ const blocks: Array<string> = [];
1716
+ let current: Array<string> = [];
1717
+
1718
+ lines.forEach((line: string, index: number): void => {
1719
+ if (fenced[index] || !line.trim()) {
1720
+ if (current.length > 0) {
1721
+ blocks.push(current.join("\n"));
1722
+ current = [];
1723
+ }
1724
+ return;
1725
+ }
1726
+
1727
+ current.push(line);
1728
+ });
1729
+
1730
+ if (current.length > 0) {
1731
+ blocks.push(current.join("\n"));
1732
+ }
1733
+
1734
+ const scanned: Array<ScannedEventReference> = [];
1735
+
1736
+ for (const block of blocks) {
1737
+ for (const reference of scanEventReferences(maskInlineCode(block))) {
1738
+ scanned.push(reference);
1739
+ }
1740
+ }
1741
+
1742
+ /*
1743
+ * An unqualified "#12" is taken as the subject's kind, except when the
1744
+ * same document also names 12 as something else: explicitly the OTHER
1745
+ * kind ("alert #12 fired ... #12 kept firing" on an incident) or in another
1746
+ * numbering ("scheduled maintenance #12 ... during #12"). The bare number
1747
+ * most likely means that, so it is not looked up as the subject's kind at
1748
+ * all: it stays plain text rather than linking to an unrelated record.
1749
+ */
1750
+ const otherKind: "incident" | "alert" =
1751
+ defaultKind === "incident" ? "alert" : "incident";
1752
+ const numbersNamedOtherwise: Set<number> = new Set<number>();
1753
+
1754
+ for (const reference of scanned) {
1755
+ if (
1756
+ reference.qualifier === otherKind ||
1757
+ reference.qualifier === "blocked"
1758
+ ) {
1759
+ numbersNamedOtherwise.add(reference.number);
1760
+ }
1761
+ }
1762
+
1763
+ const seen: Set<string> = new Set<string>();
1764
+
1765
+ for (const token of scanned) {
1766
+ if (
1767
+ token.qualifier === "blocked" ||
1768
+ (token.qualifier === null && numbersNamedOtherwise.has(token.number))
1769
+ ) {
1770
+ continue;
1771
+ }
1772
+
1773
+ const kind: "incident" | "alert" = token.qualifier || defaultKind;
1774
+ const key: string = `${kind}:${token.number}`;
1775
+
1776
+ if (seen.has(key)) {
1777
+ continue;
1778
+ }
1779
+
1780
+ seen.add(key);
1781
+ references.push({ kind, number: token.number });
1782
+
1783
+ if (references.length >= maxReferences) {
1784
+ return references;
1785
+ }
1786
+ }
1787
+
1788
+ return references;
1789
+ }