@oneuptime/common 13.0.4 → 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.
- package/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/RumApplication.ts +1 -1
- package/Models/DatabaseModels/SecurityEventConnection.ts +457 -0
- package/Models/DatabaseModels/SecurityEventConnectionRun.ts +263 -0
- package/Models/DatabaseModels/TelemetryIngestionKey.ts +2 -2
- package/Server/API/AIInvestigationAPI.ts +484 -24
- package/Server/API/GoogleSecOpsConnectionAPI.ts +249 -8
- package/Server/API/SecurityEventConnectionAPI.ts +250 -0
- package/Server/API/TelemetryAPI.ts +402 -265
- package/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.ts +99 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/TelemetryIngest.ts +101 -6
- package/Server/Services/ExceptionAggregationService.ts +7 -16
- package/Server/Services/GoogleSecOpsConnectionService.ts +15 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/InventoryItemService.ts +28 -0
- package/Server/Services/LogAggregationService.ts +17 -23
- package/Server/Services/MetricAggregationService.ts +4 -14
- package/Server/Services/SecurityEventConnectionRunService.ts +10 -0
- package/Server/Services/SecurityEventConnectionService.ts +538 -0
- package/Server/Services/TraceAggregationService.ts +77 -28
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +13 -1
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +15 -1
- package/Server/Utils/AI/SRE/InvestigationEvidence.ts +658 -0
- package/Server/Utils/AI/SRE/InvestigationReferences.ts +324 -0
- package/Server/Utils/AI/Toolbox/Index.ts +38 -0
- package/Server/Utils/AI/Toolbox/TimelineTools.ts +9 -2
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +80 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +5 -3
- package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.ts +1108 -0
- package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.ts +732 -0
- package/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.ts +495 -0
- package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.ts +689 -0
- package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.ts +767 -0
- package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.ts +571 -0
- package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.ts +827 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.ts +610 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.ts +549 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.ts +830 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.ts +684 -0
- package/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.ts +487 -0
- package/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.ts +778 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.ts +1002 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.ts +556 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.ts +275 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.ts +53 -0
- package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.ts +709 -0
- package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.ts +596 -0
- package/Server/Utils/SecurityEvent/Connectors/Types.ts +191 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +353 -65
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.ts +777 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +747 -148
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +48 -2
- package/Server/Utils/SecurityEvent/SecurityEventDedupe.ts +81 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +741 -40
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +8 -0
- package/Server/Utils/Telemetry/KubernetesResourceAttributes.ts +108 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +174 -93
- package/Server/Utils/Telemetry/ResourceFacetPlanner.ts +251 -0
- package/Server/Utils/Telemetry/ResourceFacetResolver.ts +248 -413
- package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +668 -0
- package/Server/Utils/Telemetry/TraceContextPropagation.ts +348 -0
- package/Server/Utils/Telemetry.ts +18 -0
- package/Tests/App/Dashboard/AIInvestigationHeaderStatus.test.tsx +413 -0
- package/Tests/App/Dashboard/AIRootCauseFeedItem.test.tsx +285 -0
- package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +242 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +601 -15
- package/Tests/App/Dashboard/ChangeAlertStateAIHeader.test.tsx +835 -0
- package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +41 -0
- package/Tests/App/Dashboard/ConnectionTestModal.test.tsx +604 -0
- package/Tests/App/Dashboard/ConnectorTestReportView.test.tsx +738 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +153 -6
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +165 -0
- package/Tests/App/Dashboard/EpisodeChangeState.test.tsx +1069 -0
- package/Tests/App/Dashboard/EpisodeMembersCard.test.tsx +772 -0
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +2354 -0
- package/Tests/App/Dashboard/EventOverviewSkeleton.test.tsx +210 -0
- package/Tests/App/Dashboard/EventStatBar.test.tsx +392 -0
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +247 -0
- package/Tests/App/Dashboard/ExceptionAIAssistance.test.tsx +606 -0
- package/Tests/App/Dashboard/ExceptionBreadcrumbTimeline.test.tsx +273 -0
- package/Tests/App/Dashboard/ExceptionDetailSideMenu.test.tsx +176 -0
- package/Tests/App/Dashboard/ExceptionOverviewCards.test.tsx +366 -0
- package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +371 -0
- package/Tests/App/Dashboard/ExceptionStackFrameViewer.test.tsx +440 -0
- package/Tests/App/Dashboard/ExceptionSummary.test.tsx +381 -0
- package/Tests/App/Dashboard/ExceptionTriage.test.tsx +306 -0
- package/Tests/App/Dashboard/ExceptionsResourceFacets.test.tsx +604 -0
- package/Tests/App/Dashboard/ExceptionsViewerEntityChips.test.tsx +706 -0
- package/Tests/App/Dashboard/FeedOrdering.test.ts +32 -0
- package/Tests/App/Dashboard/GoogleSecOpsConnectionForm.test.tsx +838 -0
- package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +18 -0
- package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +297 -20
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +618 -13
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +254 -207
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +303 -135
- package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +1251 -0
- package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +303 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +876 -8
- package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +35 -0
- package/Tests/App/Dashboard/InvestigationReferenceLink.test.tsx +177 -0
- package/Tests/App/Dashboard/InvestigationReportView.test.tsx +1295 -0
- package/Tests/App/Dashboard/KubernetesImageReferenceView.test.tsx +114 -0
- package/Tests/App/Dashboard/KubernetesLogsTabChips.test.tsx +285 -0
- package/Tests/App/Dashboard/LogsEntityChipNames.test.tsx +316 -0
- package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +361 -0
- package/Tests/App/Dashboard/ProfileTableEntityNames.test.tsx +432 -0
- package/Tests/App/Dashboard/ProfileViewEntityName.test.tsx +250 -0
- package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +10 -0
- package/Tests/App/Dashboard/ResourceLogsTabsBareLayout.test.tsx +700 -0
- package/Tests/App/Dashboard/ResourcePageEntityNameProps.test.tsx +803 -0
- package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +11 -1
- package/Tests/App/Dashboard/ScheduledMaintenanceChangeState.test.tsx +1026 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +436 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +1022 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +905 -0
- package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +247 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +375 -274
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +410 -0
- package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +774 -0
- package/Tests/App/Dashboard/TopErrorsPanelResourceNames.test.tsx +316 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +30 -0
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +88 -24
- package/Tests/App/Dashboard/TraceExplorer.test.tsx +1590 -0
- package/Tests/App/Dashboard/TraceWaterfall.test.tsx +488 -0
- package/Tests/App/Dashboard/TracesAnalyticsSeriesLabels.test.tsx +367 -0
- package/Tests/App/Dashboard/TracesEntityNames.test.tsx +585 -0
- package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +613 -0
- package/Tests/App/Dashboard/UseServiceNames.test.tsx +359 -0
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +7 -0
- package/Tests/Models/InventoryItemNaming.test.ts +132 -16
- package/Tests/Models/SecurityDomainAccessControl.test.ts +61 -17
- package/Tests/Server/API/AIInvestigationAPI.test.ts +611 -2
- package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +1098 -0
- package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +409 -0
- package/Tests/Server/API/SecurityEventConnectionAPI.test.ts +856 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +528 -1
- package/Tests/Server/API/TelemetryFacetsAPI.test.ts +810 -0
- package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +240 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +229 -0
- package/Tests/Server/Services/InventoryItemDisplayName.test.ts +66 -0
- package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
- package/Tests/Server/Services/SecurityEventConnectionService.test.ts +1233 -0
- package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +53 -0
- package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +218 -0
- package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +155 -1
- package/Tests/Server/Services/TraceAggregationExceptionScope.test.ts +142 -0
- package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +85 -24
- package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +33 -0
- package/Tests/Server/Utils/AI/InvestigationEvidence.test.ts +1395 -0
- package/Tests/Server/Utils/AI/InvestigationReferences.test.ts +616 -0
- package/Tests/Server/Utils/AI/TimelineTools.test.ts +119 -0
- package/Tests/Server/Utils/AI/Toolbox/AIToolbox.test.ts +151 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +65 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorExceptionScope.test.ts +224 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPerSeriesFanout.test.ts +55 -2
- package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.test.ts +1240 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.test.ts +1853 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.test.ts +1129 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.test.ts +1066 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.test.ts +1568 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.test.ts +930 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.test.ts +1409 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.test.ts +763 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.test.ts +1296 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.test.ts +1102 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.test.ts +1174 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.test.ts +1032 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.test.ts +1558 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.test.ts +3041 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.test.ts +1168 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.test.ts +906 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.test.ts +1068 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.test.ts +1004 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.test.ts +784 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +113 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +5 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +93 -13
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +23 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +25 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +22 -8
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +82 -16
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSearchDetections.test.ts +730 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsThreePassPoller.test.ts +1577 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayOriginAllowlist.test.ts +41 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1453 -18
- package/Tests/Server/Utils/Telemetry/KubernetesResourceAttributes.test.ts +162 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +460 -13
- package/Tests/Server/Utils/Telemetry/ResourceFacetPlanner.test.ts +678 -0
- package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +348 -2
- package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +525 -0
- package/Tests/Server/Utils/Telemetry/TraceContextPropagation.test.ts +420 -0
- package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +73 -0
- package/Tests/Types/Rum/SessionReplayTransportContracts.test.ts +95 -0
- package/Tests/Types/SecurityEvent/Connectors/ConnectorDiagnostics.test.ts +82 -0
- package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +314 -0
- package/Tests/Types/Telemetry/ExceptionSpanScope.test.ts +89 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +96 -0
- package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +304 -0
- package/Tests/UI/Components/ActiveFilterChipsLockedFilters.test.tsx +357 -0
- package/Tests/UI/Components/Card.test.tsx +312 -1
- package/Tests/UI/Components/CardModelDetailHeaderLayout.test.tsx +253 -0
- package/Tests/UI/Components/CardModelDetailRefresher.test.tsx +342 -0
- package/Tests/UI/Components/Checkbox.test.tsx +79 -0
- package/Tests/UI/Components/CopyTextButton.test.tsx +142 -0
- package/Tests/UI/Components/DetailCompactStyle.test.tsx +621 -0
- package/Tests/UI/Components/ErrorBoundary.test.tsx +60 -0
- package/Tests/UI/Components/FacetSections.test.tsx +581 -0
- package/Tests/UI/Components/FacetSidebarControls.test.tsx +812 -0
- package/Tests/UI/Components/FacetVisibility.test.ts +429 -0
- package/Tests/UI/Components/LockedFilterActions.test.tsx +396 -0
- package/Tests/UI/Components/LockedFilterChip.test.tsx +651 -0
- package/Tests/UI/Components/LogsEntityNames.test.ts +1769 -0
- package/Tests/UI/Components/LogsEntityNamesComponents.test.tsx +696 -0
- package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +600 -0
- package/Tests/UI/Components/LogsViewerEntityNames.test.tsx +713 -0
- package/Tests/UI/Components/MarkdownInlineReferences.test.tsx +2079 -0
- package/Tests/UI/Components/MemberRoleAssignment.test.tsx +446 -0
- package/Tests/UI/Components/ModelDetailRefetch.test.tsx +397 -0
- package/Tests/UI/Components/ModelListRefetch.test.tsx +233 -0
- package/Tests/UI/Components/ModelPageModelChange.test.tsx +409 -0
- package/Tests/UI/Components/PaginationIntegration.test.tsx +25 -0
- package/Tests/UI/Components/ResourceFacetConfigs.test.ts +172 -0
- package/Tests/UI/Components/SavedViewsSidebarIntegration.test.tsx +32 -14
- package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +44 -1
- package/Tests/UI/Components/TelemetryActiveFilterChipsLockedFilters.test.tsx +304 -0
- package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +718 -0
- package/Tests/UI/Monitor/EvaluationLogList.test.tsx +303 -0
- package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +55 -1
- package/Tests/UI/Rum/FidelityNotices.test.ts +22 -0
- package/Tests/UI/Rum/RecordingHealthCard.test.tsx +120 -136
- package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +988 -0
- package/Tests/UI/Rum/ReplayCard.test.tsx +32 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +709 -12
- package/Tests/UI/Rum/ReplayHeader.test.tsx +50 -1
- package/Tests/UI/Rum/ReplayPinControl.test.tsx +67 -10
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +41 -12
- package/Tests/UI/Rum/ReplayStage.test.tsx +663 -1
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +173 -0
- package/Tests/UI/Rum/ReplayUi.test.tsx +189 -2
- package/Tests/UI/Rum/ReplayUserSessionsMenu.test.tsx +91 -0
- package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +1 -1
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +585 -0
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +844 -51
- package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +29 -1
- package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +288 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +31 -1
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +6 -0
- package/Tests/UI/Utils/Clipboard.test.ts +154 -0
- package/Tests/UI/Utils/Navigation.test.ts +24 -0
- package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +2205 -0
- package/Tests/UI/Utils/Telemetry/UseTelemetryEntityNames.test.tsx +844 -0
- package/Tests/Utils/AI/InvestigationReport.test.ts +2430 -0
- package/Tests/Utils/APIOutgoingRequestTracer.test.ts +137 -0
- package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +40 -0
- package/Tests/Utils/Rum/SessionReplayRecordingEnded.test.ts +619 -0
- package/Tests/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.test.ts +631 -0
- package/Tests/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.test.ts +479 -0
- package/Tests/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.test.ts +507 -0
- package/Tests/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.test.ts +686 -0
- package/Tests/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.test.ts +474 -0
- package/Tests/Utils/SecurityEvent/Connectors/OktaNormalizer.test.ts +736 -0
- package/Tests/Utils/SecurityEvent/Connectors/SplunkNormalizer.test.ts +407 -0
- package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.test.ts +129 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +57 -1
- package/Tests/Utils/Telemetry/EntityRelationship.test.ts +36 -0
- package/Tests/Utils/Telemetry/LockedFilterSearch.test.ts +600 -0
- package/Tests/Utils/Telemetry/NetworkHost.test.ts +48 -0
- package/Tests/Utils/Telemetry/OriginAllowList.test.ts +154 -0
- package/Tests/Utils/Traces/CriticalPath.test.ts +25 -0
- package/Types/AI/AIChatTypes.ts +8 -0
- package/Types/AI/InvestigationEvidence.ts +96 -0
- package/Types/Monitor/MonitorEvaluationSummary.ts +5 -0
- package/Types/Permission.ts +2 -2
- package/Types/Rum/SessionReplay.ts +109 -0
- package/Types/Rum/SessionReplayApi.ts +31 -0
- package/Types/SecurityEvent/Connectors/ConnectorDiagnostics.ts +111 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.ts +87 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +492 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.ts +41 -0
- package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -1
- package/Types/SecurityEvent/GoogleSecOpsRegion.ts +34 -0
- package/Types/Telemetry/EntityType.ts +14 -0
- package/Types/Telemetry/ExceptionSpanScope.ts +83 -0
- package/Types/Telemetry/LockedFilterDetail.ts +46 -0
- package/Types/Telemetry/ResourceEntityFacet.ts +17 -10
- package/Types/Telemetry/ResourceFacetCatalog.ts +171 -0
- package/UI/Components/Card/Card.tsx +118 -83
- package/UI/Components/Checkbox/Checkbox.tsx +8 -5
- package/UI/Components/CopyTextButton/CopyTextButton.tsx +7 -1
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +4 -1
- package/UI/Components/Detail/Detail.tsx +137 -22
- package/UI/Components/ErrorBoundary.tsx +38 -0
- package/UI/Components/LogsViewer/LogsEntityNames.ts +912 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +156 -77
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +28 -11
- package/UI/Components/LogsViewer/components/FacetSection.tsx +66 -79
- package/UI/Components/LogsViewer/components/FacetValueRow.tsx +17 -2
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +45 -5
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +100 -19
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +156 -39
- package/UI/Components/LogsViewer/components/LogsTable.tsx +30 -8
- package/UI/Components/LogsViewer/types.ts +6 -0
- package/UI/Components/Markdown.tsx/InlineReferences.tsx +628 -0
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +385 -312
- package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +50 -22
- package/UI/Components/ModelDetail/CardModelDetail.tsx +15 -0
- package/UI/Components/ModelDetail/ModelDetail.tsx +64 -19
- package/UI/Components/ModelList/ModelList.tsx +33 -8
- package/UI/Components/Page/ModelPage.tsx +119 -54
- package/UI/Components/Table/Table.tsx +8 -0
- package/UI/Components/Table/TableRow.tsx +13 -5
- package/UI/Components/TelemetryViewer/FacetVisibility.ts +229 -0
- package/UI/Components/TelemetryViewer/ResourceFacetConfigs.ts +46 -0
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +11 -0
- package/UI/Components/TelemetryViewer/components/FacetSearchInput.tsx +79 -0
- package/UI/Components/TelemetryViewer/components/FacetSectionHeader.tsx +69 -0
- package/UI/Components/TelemetryViewer/components/FacetShowMoreButton.tsx +36 -0
- package/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.tsx +88 -0
- package/UI/Components/TelemetryViewer/components/LockedFilterActions.tsx +205 -0
- package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +496 -0
- package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +30 -10
- package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +203 -10
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +60 -73
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +94 -7
- package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +17 -2
- package/UI/Components/TelemetryViewer/types.ts +19 -0
- package/UI/Components/TelemetryViewer/useFacetSearchExemptions.ts +158 -0
- package/UI/Components/TelemetryViewer/useFacetSectionSearch.ts +102 -0
- package/UI/Utils/Clipboard.ts +68 -2
- package/UI/Utils/Navigation.ts +17 -2
- package/UI/Utils/Telemetry/TelemetryEntityNames.ts +620 -0
- package/UI/Utils/Telemetry/UseTelemetryEntityNames.ts +151 -0
- package/Utils/AI/InvestigationReport.ts +1789 -0
- package/Utils/API.ts +90 -1
- package/Utils/Rum/SessionReplayHealth.ts +11 -0
- package/Utils/Rum/SessionReplayRecordingEnded.ts +134 -0
- package/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.ts +427 -0
- package/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.ts +197 -0
- package/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.ts +464 -0
- package/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.ts +567 -0
- package/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.ts +299 -0
- package/Utils/SecurityEvent/Connectors/OktaNormalizer.ts +467 -0
- package/Utils/SecurityEvent/Connectors/SplunkNormalizer.ts +439 -0
- package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +124 -12
- package/Utils/Telemetry/CrossSignalScope.ts +22 -8
- package/Utils/Telemetry/EntityRelationship.ts +8 -0
- package/Utils/Telemetry/LockedFilterSearch.ts +412 -0
- package/Utils/Telemetry/NetworkHost.ts +43 -0
- package/Utils/Telemetry/OriginAllowList.ts +139 -4
- package/Utils/Traces/CriticalPath.ts +10 -4
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/SecurityEventConnection.js +490 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnection.js.map +1 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js +287 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js.map +1 -0
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +2 -2
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +336 -27
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +182 -7
- package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -1
- package/build/dist/Server/API/SecurityEventConnectionAPI.js +171 -0
- package/build/dist/Server/API/SecurityEventConnectionAPI.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +248 -170
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +57 -4
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/ExceptionAggregationService.js +7 -16
- package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +8 -0
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +22 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +8 -17
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricAggregationService.js +4 -14
- package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
- package/build/dist/Server/Services/SecurityEventConnectionRunService.js +9 -0
- package/build/dist/Server/Services/SecurityEventConnectionRunService.js.map +1 -0
- package/build/dist/Server/Services/SecurityEventConnectionService.js +356 -0
- package/build/dist/Server/Services/SecurityEventConnectionService.js.map +1 -0
- package/build/dist/Server/Services/TraceAggregationService.js +51 -25
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +3 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +14 -7
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js +468 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js +230 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +32 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +6 -2
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +59 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +5 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js +752 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js +493 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js +352 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js +405 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js +466 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js +344 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js +511 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js +406 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js +336 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js +474 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js +417 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js +314 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js +494 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js +719 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js +404 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js +209 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js +40 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js +494 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js +379 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js +27 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +234 -61
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js +537 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +572 -115
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +35 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js +52 -0
- package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +521 -32
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +10 -0
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +8 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js +80 -0
- package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +122 -67
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js +146 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +174 -248
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +492 -0
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js +259 -0
- package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry.js +11 -5
- package/build/dist/Server/Utils/Telemetry.js.map +1 -1
- package/build/dist/Types/AI/InvestigationEvidence.js +2 -0
- package/build/dist/Types/AI/InvestigationEvidence.js.map +1 -0
- package/build/dist/Types/Permission.js +2 -2
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +95 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
- package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js +18 -0
- package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js +2 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +374 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js +35 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js.map +1 -0
- package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js +35 -0
- package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +14 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/ExceptionSpanScope.js +59 -0
- package/build/dist/Types/Telemetry/ExceptionSpanScope.js.map +1 -0
- package/build/dist/Types/Telemetry/LockedFilterDetail.js +16 -0
- package/build/dist/Types/Telemetry/LockedFilterDetail.js.map +1 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js +16 -10
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -1
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +119 -0
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -0
- package/build/dist/UI/Components/Card/Card.js +34 -17
- package/build/dist/UI/Components/Card/Card.js.map +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js +6 -4
- package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +63 -13
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/ErrorBoundary.js +34 -0
- package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js +535 -0
- package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +117 -57
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +5 -7
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +31 -44
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +17 -3
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -3
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +64 -18
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +100 -41
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +13 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js +403 -0
- package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +197 -179
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +30 -21
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +12 -0
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js +46 -12
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelList/ModelList.js +26 -7
- package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
- package/build/dist/UI/Components/Page/ModelPage.js +62 -34
- package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +3 -1
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +8 -1
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +111 -0
- package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js +22 -0
- package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js +36 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js +21 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js +14 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js +27 -0
- package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js +88 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +228 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +6 -6
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +111 -12
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +27 -40
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +70 -10
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +17 -3
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/types.js +0 -4
- package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js +70 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js +52 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js.map +1 -0
- package/build/dist/UI/Utils/Clipboard.js +59 -2
- package/build/dist/UI/Utils/Clipboard.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +11 -2
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +471 -0
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -0
- package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js +113 -0
- package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js.map +1 -0
- package/build/dist/Utils/AI/InvestigationReport.js +1241 -0
- package/build/dist/Utils/AI/InvestigationReport.js.map +1 -0
- package/build/dist/Utils/API.js +25 -0
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayHealth.js +10 -0
- package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js +54 -0
- package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js +311 -0
- package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js +150 -0
- package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js +328 -0
- package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js +405 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js +212 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js +351 -0
- package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js +348 -0
- package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +103 -10
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +16 -5
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityRelationship.js +7 -0
- package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
- package/build/dist/Utils/Telemetry/LockedFilterSearch.js +277 -0
- package/build/dist/Utils/Telemetry/LockedFilterSearch.js.map +1 -0
- package/build/dist/Utils/Telemetry/NetworkHost.js +39 -0
- package/build/dist/Utils/Telemetry/NetworkHost.js.map +1 -0
- package/build/dist/Utils/Telemetry/OriginAllowList.js +105 -4
- package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -1
- package/build/dist/Utils/Traces/CriticalPath.js +9 -4
- package/build/dist/Utils/Traces/CriticalPath.js.map +1 -1
- package/package.json +1 -1
- package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +0 -600
- package/UI/Components/GanttChart/Bar/BarLabel.tsx +0 -17
- package/UI/Components/GanttChart/Bar/Index.tsx +0 -149
- package/UI/Components/GanttChart/ChartContainer.tsx +0 -50
- package/UI/Components/GanttChart/Index.tsx +0 -90
- package/UI/Components/GanttChart/Row/Index.tsx +0 -22
- package/UI/Components/GanttChart/Row/Row.tsx +0 -231
- package/UI/Components/GanttChart/Row/RowLabel.tsx +0 -30
- package/UI/Components/GanttChart/Rows.tsx +0 -59
- package/UI/Components/GanttChart/Timeline/Index.tsx +0 -46
- package/UI/Components/GanttChart/Timeline/TimelineInterval.tsx +0 -33
- package/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.tsx +0 -43
- package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js +0 -8
- package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Bar/Index.js +0 -77
- package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/ChartContainer.js +0 -33
- package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Index.js +0 -39
- package/build/dist/UI/Components/GanttChart/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/Index.js +0 -7
- package/build/dist/UI/Components/GanttChart/Row/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/Row.js +0 -106
- package/build/dist/UI/Components/GanttChart/Row/Row.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/RowLabel.js +0 -10
- package/build/dist/UI/Components/GanttChart/Row/RowLabel.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Rows.js +0 -19
- package/build/dist/UI/Components/GanttChart/Rows.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/Index.js +0 -16
- package/build/dist/UI/Components/GanttChart/Timeline/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js +0 -13
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js +0 -13
- 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
|
+
}
|