@skyhook-io/radar-app 1.13.4 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/RadarApp.tsx +5 -0
  4. package/src/api/client.authRedirect.test.ts +12 -1
  5. package/src/api/client.prometheusStatus.test.ts +24 -0
  6. package/src/api/client.ts +325 -16
  7. package/src/api/diagnose.ts +62 -15
  8. package/src/api/drain-plan.test.ts +86 -0
  9. package/src/api/quotas.test.ts +26 -0
  10. package/src/api/quotas.ts +20 -0
  11. package/src/api/workloadMetrics.test.ts +40 -0
  12. package/src/api/workloadMetrics.ts +68 -0
  13. package/src/components/CloudConnectFlow.tsx +179 -42
  14. package/src/components/CloudFunnelButton.tsx +107 -36
  15. package/src/components/ConnectionErrorView.test.tsx +30 -0
  16. package/src/components/ConnectionErrorView.tsx +31 -19
  17. package/src/components/SyncProgressPanel.tsx +73 -0
  18. package/src/components/audit/AuditView.tsx +2 -0
  19. package/src/components/cloudConnectHandoff.test.ts +140 -0
  20. package/src/components/cloudConnectHandoff.ts +124 -0
  21. package/src/components/diagnose/AgentCase.tsx +131 -0
  22. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  23. package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
  24. package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
  25. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
  26. package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
  27. package/src/components/diagnose/InvestigationView.tsx +222 -5
  28. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  29. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  30. package/src/components/diagnose/investigationCase.ts +439 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  48. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  49. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  50. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  51. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  52. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  53. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  54. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  55. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  56. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  57. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  58. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  59. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  60. package/src/components/diagnose/investigationMetrics.ts +393 -0
  61. package/src/components/diagnose/investigationSourceFocus.ts +2 -0
  62. package/src/components/diagnose/investigationState.test.ts +322 -0
  63. package/src/components/diagnose/investigationState.ts +147 -25
  64. package/src/components/diagnose/parts.test.tsx +482 -0
  65. package/src/components/diagnose/parts.tsx +418 -41
  66. package/src/components/diagnose/target.test.ts +19 -0
  67. package/src/components/diagnose/target.ts +3 -2
  68. package/src/components/gitops/GitOpsView.tsx +7 -0
  69. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  70. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  71. package/src/components/resource/PrometheusCharts.tsx +4 -3
  72. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
  74. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  75. package/src/components/resource/RightsizingStrip.tsx +15 -3
  76. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  77. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  78. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  79. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  80. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  81. package/src/components/resource/workloadMetricValues.ts +30 -0
  82. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  83. package/src/components/resources/PodFilePreview.tsx +394 -0
  84. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  85. package/src/components/resources/ResourcesView.tsx +44 -6
  86. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  87. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  88. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  89. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  90. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  91. package/src/components/settings/SettingsDialog.tsx +94 -14
  92. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  93. package/src/components/traffic/TrafficGraph.tsx +6 -6
  94. package/src/components/traffic/TrafficView.tsx +10 -9
  95. package/src/components/traffic/trafficFilters.test.ts +28 -1
  96. package/src/components/traffic/trafficFilters.ts +31 -0
  97. package/src/components/workload/WorkloadView.tsx +19 -8
  98. package/src/context/ConnectionContext.tsx +48 -3
  99. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  100. package/src/context/DiagnoseCustomization.tsx +15 -2
  101. package/src/index.css +17 -0
  102. package/src/index.ts +1 -0
  103. package/src/utils/navigation.ts +1 -1
  104. package/src/utils/shell-safe.test.ts +25 -1
  105. package/src/utils/shell-safe.ts +16 -0
  106. package/src/utils/topology-namespace.test.ts +64 -0
  107. package/src/utils/topology-namespace.ts +18 -0
  108. package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
  109. package/src/components/diagnose/investigationEvidence.ts +0 -4784
  110. package/src/components/gitops/GitOpsActions.tsx +0 -1
  111. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  112. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  113. package/src/components/gitops/SyncCountdown.tsx +0 -1
  114. package/src/components/gitops/index.ts +0 -4
  115. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  116. package/src/components/logs/JsonLogLine.tsx +0 -1
  117. package/src/components/logs/LogCore.tsx +0 -2
  118. package/src/components/logs/useLogBuffer.ts +0 -2
  119. package/src/components/logs/useLogSearch.ts +0 -1
  120. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  121. package/src/components/resources/drawer-components.tsx +0 -1
  122. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  191. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  198. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  199. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  200. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  201. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  202. package/src/components/resources/renderers/index.ts +0 -94
  203. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  204. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  205. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  206. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  207. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  208. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  209. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  210. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  211. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  212. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  213. package/src/components/resources/resource-utils-argo.ts +0 -2
  214. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  215. package/src/components/resources/resource-utils-eso.ts +0 -2
  216. package/src/components/resources/resource-utils-flux.ts +0 -2
  217. package/src/components/resources/resource-utils-istio.ts +0 -2
  218. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  219. package/src/components/resources/resource-utils-knative.ts +0 -2
  220. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  221. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  222. package/src/components/resources/resource-utils-traefik.ts +0 -1
  223. package/src/components/resources/resource-utils-trivy.ts +0 -2
  224. package/src/components/resources/resource-utils-velero.ts +0 -2
  225. package/src/components/shared/EditableYamlView.tsx +0 -24
  226. package/src/components/timeline/DiffViewer.tsx +0 -1
  227. package/src/components/traffic/index.ts +0 -3
  228. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  229. package/src/components/ui/MetricsChart.tsx +0 -1
  230. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -0,0 +1,418 @@
1
+ import { apiVersionToGroup, knownKindForPluralWithGroup } from "../../../utils/navigation";
2
+ import { adaptChanges } from "./adapters/changes";
3
+ import { adaptDiagnose } from "./adapters/diagnose";
4
+ import { adaptEvents } from "./adapters/events";
5
+ import { adaptHelmRelease } from "./adapters/helm";
6
+ import { adaptIssues } from "./adapters/issues";
7
+ import { adaptPodLogs, adaptWorkloadLogs } from "./adapters/logs";
8
+ import { adaptSubjectPermissions } from "./adapters/permissions";
9
+ import {
10
+ adaptPrometheusRules,
11
+ adaptQueryPrometheus,
12
+ } from "./adapters/prometheus";
13
+ import { adaptGetResource, adaptListResources } from "./adapters/resource";
14
+ import { adaptNeighborhood, adaptTopology } from "./adapters/topology";
15
+ import {
16
+ investigationEvidenceRefRe,
17
+ investigationEvidenceSourceId,
18
+ } from "./identity";
19
+ import {
20
+ ProjectionBuilder,
21
+ invalidPayload,
22
+ investigationResultLabel,
23
+ relevanceForResource,
24
+ scopeFromArgs,
25
+ } from "./observations";
26
+ import { kubernetesResource, nonEmptyString, parseJSON, record } from "./parse";
27
+ import {
28
+ type InvestigationEvidenceGroup,
29
+ type InvestigationEvidenceKind,
30
+ type InvestigationEvidenceObservation,
31
+ type InvestigationEvidenceProjection,
32
+ type InvestigationEvidenceSource,
33
+ type InvestigationEvidenceTarget,
34
+ type InvestigationEvidenceTier,
35
+ type InvestigationEvidenceTurn,
36
+ } from "./types";
37
+
38
+ /**
39
+ * Adapters whose verdict depends on which pods a producer established as the
40
+ * target's. They run after every other tool in the transcript, so the answer
41
+ * does not depend on whether the agent happened to query Prometheus before or
42
+ * after the read that named the pods. Their source order is captured before
43
+ * they are queued, so deferring the classification does not reorder evidence.
44
+ */
45
+ const MEMBERSHIP_DEPENDENT_ADAPTERS = new Set([
46
+ "query_prometheus",
47
+ "get_prometheus_rules",
48
+ ]);
49
+ const ADAPTERS: Record<
50
+ string,
51
+ (
52
+ builder: ProjectionBuilder,
53
+ source: InvestigationEvidenceSource,
54
+ payload: unknown,
55
+ ) => void
56
+ > = {
57
+ diagnose: adaptDiagnose,
58
+ issues: adaptIssues,
59
+ get_resource: adaptGetResource,
60
+ list_resources: adaptListResources,
61
+ get_events: adaptEvents,
62
+ get_pod_logs: adaptPodLogs,
63
+ get_changes: adaptChanges,
64
+ get_neighborhood: adaptNeighborhood,
65
+ get_topology: adaptTopology,
66
+ get_workload_logs: adaptWorkloadLogs,
67
+ get_prometheus_rules: adaptPrometheusRules,
68
+ get_helm_release: adaptHelmRelease,
69
+ get_subject_permissions: adaptSubjectPermissions,
70
+ query_prometheus: adaptQueryPrometheus,
71
+ };
72
+ /**
73
+ * The pods every diagnose of the target listed as its own, gathered before
74
+ * anything is classified. A metrics query is target evidence when it names
75
+ * pods a producer established, and that must not depend on whether the agent
76
+ * happened to run diagnose before or after the query: the same investigation
77
+ * would otherwise read differently for the same facts.
78
+ */
79
+ function collectEstablishedTargetPods(
80
+ builder: ProjectionBuilder,
81
+ turns: readonly InvestigationEvidenceTurn[],
82
+ ): void {
83
+ for (const turn of turns) {
84
+ for (const item of turn.timeline) {
85
+ if (
86
+ item.kind !== "tool" ||
87
+ item.tool !== "diagnose" ||
88
+ item.radarEvidence !== true ||
89
+ item.status !== "done" ||
90
+ // Confirmed success, the same test the sources use. This set is what
91
+ // proves a Prometheus selector is about the target, so a result that
92
+ // never said it succeeded must not put pods into it.
93
+ item.isError !== false ||
94
+ !nonEmptyString(item.result)
95
+ ) {
96
+ continue;
97
+ }
98
+ const value = record(parseJSON(item.result));
99
+ const resource = kubernetesResource(value?.resource);
100
+ if (!value || !resource || !Array.isArray(value.podNames)) continue;
101
+ if (
102
+ relevanceForResource(builder, {
103
+ kind: resource.kind,
104
+ group: apiVersionToGroup(resource.apiVersion),
105
+ namespace: resource.metadata.namespace,
106
+ name: resource.metadata.name,
107
+ }) !== "target"
108
+ ) {
109
+ continue;
110
+ }
111
+ for (const pod of value.podNames) {
112
+ if (nonEmptyString(pod)) builder.establishedTargetPods.add(pod);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ export function projectInvestigationEvidence(
118
+ turns: readonly InvestigationEvidenceTurn[],
119
+ target: InvestigationEvidenceTarget,
120
+ ): InvestigationEvidenceProjection {
121
+ const builder = new ProjectionBuilder({
122
+ ...target,
123
+ kind: knownKindForPluralWithGroup(target.kind, target.group) ?? target.kind,
124
+ });
125
+ collectEstablishedTargetPods(builder, turns);
126
+ const evidenceRefSources: InvestigationEvidenceSource[] = [];
127
+ const citableSources: InvestigationEvidenceSource[] = [];
128
+ const deferred: {
129
+ adapt: (
130
+ builder: ProjectionBuilder,
131
+ source: InvestigationEvidenceSource,
132
+ payload: unknown,
133
+ ) => void;
134
+ source: InvestigationEvidenceSource;
135
+ payload: unknown;
136
+ }[] = [];
137
+ let order = 0;
138
+ for (const [turnIndex, turn] of turns.entries()) {
139
+ for (const [timelineIndex, item] of turn.timeline.entries()) {
140
+ const itemOrder = order;
141
+ order += 1;
142
+ if (item.kind !== "tool") continue;
143
+ // Full-local agents may load user MCP servers whose bare tool names collide
144
+ // with Radar's. Only results matched by the server to the active private
145
+ // transport ledger may enter the surface labelled "Radar evidence".
146
+ if (item.radarEvidence !== true) continue;
147
+ const source: InvestigationEvidenceSource = {
148
+ id: investigationEvidenceSourceId(turnIndex, item.id),
149
+ turnIndex,
150
+ timelineIndex,
151
+ stepId: item.id,
152
+ tool: item.tool,
153
+ args: item.summary,
154
+ order: itemOrder,
155
+ phase: turn.verify
156
+ ? "verification"
157
+ : turn.apply
158
+ ? "apply"
159
+ : turn.question
160
+ ? "followup"
161
+ : "initial",
162
+ confirmedSuccess: item.status === "done" && item.isError === false,
163
+ evidenceRef: item.evidenceRef,
164
+ };
165
+ if (item.evidenceRef) evidenceRefSources.push(source);
166
+ if (item.status !== "done") continue;
167
+ if (
168
+ item.evidenceRef &&
169
+ investigationEvidenceRefRe.test(item.evidenceRef) &&
170
+ item.isError === false &&
171
+ !item.truncated &&
172
+ nonEmptyString(item.result)
173
+ ) {
174
+ citableSources.push(source);
175
+ }
176
+ const adapt = ADAPTERS[item.tool];
177
+ if (!adapt) continue;
178
+ builder.addSource(source);
179
+ if (item.isError === true) {
180
+ builder.limit(
181
+ source,
182
+ investigationResultLabel(source),
183
+ item.result || "This investigation step failed.",
184
+ "error",
185
+ );
186
+ continue;
187
+ }
188
+ if (item.truncated) {
189
+ builder.limit(
190
+ source,
191
+ investigationResultLabel(source),
192
+ "Only part of this investigation result was saved, so Radar could not summarize it here.",
193
+ "truncated",
194
+ );
195
+ continue;
196
+ }
197
+ if (!nonEmptyString(item.result)) {
198
+ builder.limit(
199
+ source,
200
+ investigationResultLabel(source),
201
+ "This investigation step did not return details Radar could summarize.",
202
+ "unknown",
203
+ );
204
+ continue;
205
+ }
206
+ const payload = parseJSON(item.result);
207
+ if (payload === undefined) {
208
+ invalidPayload(builder, source);
209
+ continue;
210
+ }
211
+ if (MEMBERSHIP_DEPENDENT_ADAPTERS.has(item.tool)) {
212
+ deferred.push({ adapt, source, payload });
213
+ } else {
214
+ adapt(builder, source, payload);
215
+ }
216
+ if (item.isError !== false) {
217
+ builder.limit(
218
+ source,
219
+ investigationResultLabel(source),
220
+ "Radar cannot confirm whether this investigation step completed successfully. Available evidence is shown, but an empty result cannot confirm that nothing was found.",
221
+ "unknown",
222
+ );
223
+ }
224
+ }
225
+ }
226
+
227
+ // Membership is settled now: every read that could name one of the target's
228
+ // pods has been adapted, so these see the same set whatever order the agent
229
+ // worked in.
230
+ for (const { adapt, source, payload } of deferred) {
231
+ adapt(builder, source, payload);
232
+ }
233
+
234
+ const tierRank: Record<InvestigationEvidenceTier, number> = {
235
+ key: 0,
236
+ supporting: 1,
237
+ context: 2,
238
+ checked: 3,
239
+ };
240
+ const primaryBySource = new Map<
241
+ string,
242
+ { groupId: string; rank: number; order: number }
243
+ >();
244
+ for (const group of builder.groups) {
245
+ for (const observation of group.observations) {
246
+ const candidate = {
247
+ groupId: group.id,
248
+ rank: tierRank[observation.tier],
249
+ order: group.firstOrder,
250
+ };
251
+ const current = primaryBySource.get(observation.source.id);
252
+ if (
253
+ !current ||
254
+ candidate.rank < current.rank ||
255
+ (candidate.rank === current.rank && candidate.order < current.order)
256
+ ) {
257
+ primaryBySource.set(observation.source.id, candidate);
258
+ }
259
+ }
260
+ }
261
+ for (const source of builder.sources) {
262
+ source.primaryGroupId = primaryBySource.get(source.id)?.groupId;
263
+ }
264
+
265
+ // The investigation target, rather than the producer tool, is the proof
266
+ // boundary for semantic diagnosis domains. A later successful target
267
+ // diagnosis can therefore retire an exact-target issue first observed by
268
+ // `issues`, while a broad or sibling read still cannot clear it.
269
+ const targetProofScope = [
270
+ builder.target.group.toLowerCase(),
271
+ builder.target.kind.toLowerCase(),
272
+ builder.target.namespace ?? "",
273
+ builder.target.name,
274
+ ].join("/");
275
+ const semanticCoverageKey = (kind: InvestigationEvidenceKind) =>
276
+ `semantic:${kind}:${targetProofScope}`;
277
+ const collectionCoverageKey = (
278
+ kind: InvestigationEvidenceKind,
279
+ identity: string,
280
+ ) => `collection:${kind}:${identity}`;
281
+ const previousLogCoverageKey = (
282
+ source: InvestigationEvidenceSource,
283
+ podContainer: string,
284
+ ) => `previous-log:${source.tool}:${scopeFromArgs(source)}:${podContainer}`;
285
+ const retirementKey = (
286
+ group: InvestigationEvidenceGroup,
287
+ observation: InvestigationEvidenceObservation,
288
+ ): string | undefined => {
289
+ switch (group.kind) {
290
+ case "issue":
291
+ case "startup":
292
+ case "crash":
293
+ case "dns":
294
+ return semanticCoverageKey(group.kind);
295
+ case "events":
296
+ case "changes":
297
+ return collectionCoverageKey(group.kind, group.identity);
298
+ case "logs":
299
+ return group.identity.startsWith("logs:previous:")
300
+ ? previousLogCoverageKey(
301
+ observation.source,
302
+ group.identity.slice("logs:previous:".length),
303
+ )
304
+ : undefined;
305
+ case "network":
306
+ return collectionCoverageKey(group.kind, group.identity);
307
+ default:
308
+ return undefined;
309
+ }
310
+ };
311
+
312
+ // Each completed verification contributes only the exact proof scopes its
313
+ // successful producers covered. Keep every verification: supersession is
314
+ // monotonic until a newer relevant observation reopens that semantic item.
315
+ const verificationCoverage: Array<{
316
+ turnIndex: number;
317
+ keys: Set<string>;
318
+ }> = [];
319
+ turns.forEach((turn, turnIndex) => {
320
+ if (!turn.verify || turn.status !== "done") return;
321
+ const keys = new Set<string>();
322
+ for (const group of builder.groups) {
323
+ for (const observation of group.observations) {
324
+ if (
325
+ observation.source.turnIndex !== turnIndex ||
326
+ !observation.source.confirmedSuccess ||
327
+ observation.relevance === "broader"
328
+ ) {
329
+ continue;
330
+ }
331
+ if (observation.data.type === "receipt") {
332
+ switch (observation.data.checked) {
333
+ case "issues":
334
+ keys.add(semanticCoverageKey("issue"));
335
+ break;
336
+ case "events":
337
+ keys.add(collectionCoverageKey("events", group.identity));
338
+ break;
339
+ case "changes":
340
+ keys.add(collectionCoverageKey("changes", group.identity));
341
+ break;
342
+ case "logs":
343
+ if (group.identity.startsWith("previous-log-absence:")) {
344
+ keys.add(
345
+ previousLogCoverageKey(
346
+ observation.source,
347
+ group.identity.slice("previous-log-absence:".length),
348
+ ),
349
+ );
350
+ }
351
+ break;
352
+ case "inventory":
353
+ case "alerts":
354
+ break;
355
+ }
356
+ continue;
357
+ }
358
+ if (
359
+ observation.source.tool === "diagnose" &&
360
+ observation.data.type === "resource"
361
+ ) {
362
+ for (const kind of builder.semanticCoverageBySource.get(
363
+ observation.source.id,
364
+ ) ?? []) {
365
+ keys.add(semanticCoverageKey(kind));
366
+ }
367
+ }
368
+ if (
369
+ observation.source.tool === "diagnose" &&
370
+ group.kind === "network"
371
+ ) {
372
+ keys.add(collectionCoverageKey("network", group.identity));
373
+ }
374
+ }
375
+ }
376
+ verificationCoverage.push({ turnIndex, keys });
377
+ });
378
+
379
+ for (const group of builder.groups) {
380
+ for (const observation of group.observations) {
381
+ const key =
382
+ observation.relevance !== "broader"
383
+ ? retirementKey(group, observation)
384
+ : undefined;
385
+ observation.historical = Boolean(
386
+ key &&
387
+ verificationCoverage.some(
388
+ (verification) =>
389
+ verification.turnIndex > observation.source.turnIndex &&
390
+ verification.keys.has(key),
391
+ ),
392
+ );
393
+ }
394
+ const latestRelevantObservation = [...group.observations]
395
+ .reverse()
396
+ .find((observation) => observation.relevance !== "broader");
397
+ group.historical = latestRelevantObservation?.historical ?? false;
398
+ }
399
+
400
+ return {
401
+ groups: builder.groups,
402
+ // Qualifications read in the order the investigation produced them, which
403
+ // is the transcript's order and not the order the adapters happened to
404
+ // run in: the membership-dependent ones are adapted last.
405
+ limitations: [...builder.limitations].sort(
406
+ (a, b) => a.firstOrder - b.firstOrder,
407
+ ),
408
+ sources: builder.sources,
409
+ evidenceRefSources,
410
+ citableSources,
411
+ coverage: {
412
+ attempted: builder.sources.length,
413
+ projected: builder.projectedSources.size,
414
+ limited: builder.limitedSources.size,
415
+ checked: builder.checkedSources.size,
416
+ },
417
+ };
418
+ }
@@ -0,0 +1,179 @@
1
+ // Shared fixtures for the evidence suites. A projection test needs a target,
2
+ // a well-formed tool result and a turn to hang them on before it can assert
3
+ // anything, so these live apart from any one suite that uses them.
4
+
5
+ import {
6
+ projectInvestigationEvidence,
7
+ type InvestigationEvidenceGroup,
8
+ type InvestigationEvidenceTimelineItem,
9
+ type InvestigationEvidenceTurn,
10
+ } from "./index";
11
+
12
+ export const target = {
13
+ kind: "Deployment",
14
+ group: "apps",
15
+ namespace: "shop",
16
+ name: "api",
17
+ };
18
+
19
+ export function evidenceRef(scope: string, nonce: string): string {
20
+ return `ev_${scope.repeat(26)}_${nonce.repeat(26)}`;
21
+ }
22
+
23
+ export function tool(
24
+ id: string,
25
+ name: string,
26
+ result: unknown,
27
+ patch: Partial<
28
+ Extract<InvestigationEvidenceTimelineItem, { kind: "tool" }>
29
+ > = {},
30
+ ): Extract<InvestigationEvidenceTimelineItem, { kind: "tool" }> {
31
+ return {
32
+ kind: "tool",
33
+ id,
34
+ tool: name,
35
+ status: "done",
36
+ summary: JSON.stringify({ namespace: "shop", name: "api" }),
37
+ result: typeof result === "string" ? result : JSON.stringify(result),
38
+ isError: false,
39
+ radarEvidence: true,
40
+ ...patch,
41
+ };
42
+ }
43
+
44
+ export function project(...timelines: InvestigationEvidenceTimelineItem[][]) {
45
+ const turns: InvestigationEvidenceTurn[] = timelines.map((timeline) => ({
46
+ timeline,
47
+ }));
48
+ return projectInvestigationEvidence(turns, target);
49
+ }
50
+
51
+ export function groupsOf(
52
+ groups: InvestigationEvidenceGroup[],
53
+ kind: InvestigationEvidenceGroup["kind"],
54
+ ) {
55
+ return groups.filter((group) => group.kind === kind);
56
+ }
57
+
58
+ export const deployment = {
59
+ apiVersion: "apps/v1",
60
+ kind: "Deployment",
61
+ metadata: { namespace: "shop", name: "api" },
62
+ status: { readyReplicas: 0, replicas: 1 },
63
+ };
64
+
65
+ export const warningEvent = {
66
+ reason: "BackOff",
67
+ message: "Back-off restarting failed container",
68
+ type: "Warning",
69
+ count: 4,
70
+ lastTimestamp: "2026-09-02T10:00:00Z",
71
+ };
72
+
73
+ export const criticalIssue = {
74
+ id: "issue-api-crash",
75
+ severity: "critical",
76
+ source: "problem",
77
+ category: "crashloop",
78
+ category_group: "runtime",
79
+ grouping_scope: "workload",
80
+ kind: "Deployment",
81
+ group: "apps",
82
+ namespace: "shop",
83
+ name: "api",
84
+ reason: "CrashLoopBackOff",
85
+ message: "The API container keeps restarting.",
86
+ };
87
+
88
+ export function completeLogProof(container = "api") {
89
+ return {
90
+ logsCurrent: [
91
+ {
92
+ pod: "api-abc",
93
+ container,
94
+ logs: {
95
+ lines: ["server ready"],
96
+ totalLines: 1,
97
+ matchedLines: 0,
98
+ fallback: true,
99
+ },
100
+ },
101
+ ],
102
+ logsPrevious: [
103
+ {
104
+ pod: "api-abc",
105
+ container,
106
+ logs: {
107
+ lines: null,
108
+ totalLines: 0,
109
+ matchedLines: 0,
110
+ fallback: false,
111
+ },
112
+ error: "failed to get logs: previous terminated container not found",
113
+ },
114
+ ],
115
+ expectedPreviousLogAbsences: [{ pod: "api-abc", container }],
116
+ logCoverage: {
117
+ resolvedPods: 1,
118
+ selectedPods: 1,
119
+ shownLines: 1,
120
+ totalLines: 1,
121
+ shownPods: 1,
122
+ totalPods: 1,
123
+ },
124
+ };
125
+ }
126
+
127
+ export const workloadLogsPayload = {
128
+ workload: "deployments/shop/api",
129
+ pods: 2,
130
+ logs: [
131
+ {
132
+ pod: "api-68c7b766dc-fmphn",
133
+ container: "api",
134
+ logs: {
135
+ lines: [
136
+ "2026-09-07T08:00:20.234687659Z [Nest] 1 - 09/07/2026, 8:00:20 AM  ERROR [MongooseModule] Unable to connect to the database. Retrying (9)...",
137
+ "2026-09-07T08:00:20.234756499Z MongoServerError: Authentication failed.",
138
+ ],
139
+ totalLines: 50,
140
+ matchedLines: 4,
141
+ fallback: false,
142
+ },
143
+ },
144
+ {
145
+ pod: "api-68c7b766dc-fmphn",
146
+ container: "istio-proxy",
147
+ logs: {
148
+ lines: ["2026-09-07T08:00:19Z info Envoy proxy is ready"],
149
+ totalLines: 12,
150
+ matchedLines: 0,
151
+ fallback: true,
152
+ },
153
+ },
154
+ {
155
+ pod: "api-68c7b766dc-q2zxr",
156
+ container: "api",
157
+ logs: {
158
+ lines: [
159
+ "2026-09-07T08:00:21Z MongoServerError: Authentication failed.",
160
+ ],
161
+ totalLines: 50,
162
+ matchedLines: 1,
163
+ fallback: false,
164
+ },
165
+ },
166
+ ],
167
+ narrowHint:
168
+ "at least one pod's log stream tailed to 50 lines (cap reached) — narrow with since= (e.g. 10m), grep= regex, container=, or raise tail_lines",
169
+ warnings: [
170
+ "1 of 2 pod(s) have container restarts on record; the error(s) that killed prior containers are in the previous instance's logs — call again with `previous: true` to see them.",
171
+ ],
172
+ };
173
+
174
+ export const workloadLogsArgs = JSON.stringify({
175
+ namespace: "shop",
176
+ name: "api",
177
+ kind: "deployment",
178
+ tail_lines: 50,
179
+ });