@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,54 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { groupsOf, project, tool } from "../evidenceFixtures";
3
+
4
+ describe("resource cards scaled by an HPA", () => {
5
+ const scaled = (state: string) =>
6
+ tool("workload", "diagnose", {
7
+ resource: {
8
+ apiVersion: "apps/v1",
9
+ kind: "Deployment",
10
+ metadata: { namespace: "shop", name: "api" },
11
+ },
12
+ resourceContext: {
13
+ tier: "basic",
14
+ workloadSummary: { replicas: { desired: 5, ready: 5, available: 5 } },
15
+ scaledBy: [
16
+ {
17
+ kind: "HorizontalPodAutoscaler",
18
+ namespace: "shop",
19
+ name: "api-hpa",
20
+ hpaSummary: {
21
+ state,
22
+ summary: "summary from Radar",
23
+ bounds: { min: 1, max: 5, current: 5, desired: 5 },
24
+ },
25
+ },
26
+ ],
27
+ },
28
+ });
29
+
30
+ it("lifts a healthy workload whose autoscaler cannot act", () => {
31
+ const result = project([scaled("metrics_unavailable")]);
32
+ const card = groupsOf(result.groups, "resource")[0].latest;
33
+ expect(card.tier).toBe("supporting");
34
+ expect(card.tone).toBe("warning");
35
+ expect(card.summary).toBe("5/5 replicas ready · HPA: Metrics unavailable");
36
+ });
37
+
38
+ it("treats a maxed-out autoscaler as adverse", () => {
39
+ const card = groupsOf(
40
+ project([scaled("limited_max")]).groups,
41
+ "resource",
42
+ )[0].latest;
43
+ expect(card.tone).toBe("warning");
44
+ expect(card.summary).toBe("5/5 replicas ready · HPA: Maxed");
45
+ });
46
+
47
+ it("leaves an autoscaler that is simply scaling as context", () => {
48
+ const card = groupsOf(project([scaled("scaling_up")]).groups, "resource")[0]
49
+ .latest;
50
+ expect(card.tier).toBe("context");
51
+ expect(card.tone).toBe("neutral");
52
+ expect(card.summary).toBe("5/5 replicas ready");
53
+ });
54
+ });
@@ -0,0 +1,257 @@
1
+ import {
2
+ type IssueRecentChange,
3
+ displayKind,
4
+ englishPlural,
5
+ kindToPlural,
6
+ } from "@skyhook-io/k8s-ui";
7
+ import { apiVersionToGroup } from "../../../../utils/navigation";
8
+ import {
9
+ ProjectionBuilder,
10
+ addChanges,
11
+ addEvents,
12
+ addNarrowHint,
13
+ addResourceObservation,
14
+ contextFrom,
15
+ invalidPayload,
16
+ relevanceForResource,
17
+ scopeFromArgs,
18
+ } from "../observations";
19
+ import {
20
+ event,
21
+ kubernetesResource,
22
+ nonEmptyString,
23
+ parseJSON,
24
+ recentChange,
25
+ record,
26
+ resourceSummary,
27
+ stringArray,
28
+ } from "../parse";
29
+ import {
30
+ type InvestigationEventEvidence,
31
+ type InvestigationEvidenceSource,
32
+ type InvestigationResourceSummary,
33
+ } from "../types";
34
+
35
+ export function adaptGetResource(
36
+ builder: ProjectionBuilder,
37
+ source: InvestigationEvidenceSource,
38
+ payload: unknown,
39
+ ): void {
40
+ // Current producer modes are discriminated by the resource's own identity,
41
+ // never by guessing an undocumented legacy wrapper:
42
+ // 1. bare Kubernetes resource
43
+ // 2. {resource, resourceContext, warnings}
44
+ // 3. the same wrapper with requested extras.
45
+ // Core Secrets use the producer's deliberately value-free detail shape,
46
+ // normalized by kubernetesResource above.
47
+ const bare = kubernetesResource(payload);
48
+ const wrapper = record(payload);
49
+ const resource = bare ?? kubernetesResource(wrapper?.resource);
50
+ if (!resource) {
51
+ invalidPayload(builder, source);
52
+ return;
53
+ }
54
+ const value = bare ? undefined : wrapper;
55
+ const context = value ? contextFrom(value.resourceContext) : undefined;
56
+ if (value?.resourceContext !== undefined && !context) {
57
+ invalidPayload(builder, source, "Resource context");
58
+ }
59
+ const warnings = stringArray(value?.warnings) ?? [];
60
+ const relevance = relevanceForResource(builder, {
61
+ kind: resource.kind,
62
+ group: apiVersionToGroup(resource.apiVersion),
63
+ namespace: resource.metadata.namespace,
64
+ name: resource.metadata.name,
65
+ });
66
+ addResourceObservation(
67
+ builder,
68
+ source,
69
+ resource,
70
+ context,
71
+ warnings,
72
+ undefined,
73
+ false,
74
+ relevance,
75
+ );
76
+ if (!value) return;
77
+ addNarrowHint(builder, source, value);
78
+
79
+ const errors: Array<[string, string]> = [
80
+ ["eventsError", "Events"],
81
+ ["recentChangesError", "Recent changes"],
82
+ ["metricsError", "Metrics"],
83
+ ["revisionsError", "Revisions"],
84
+ ["includeError", "Requested include"],
85
+ ];
86
+ for (const [field, label] of errors) {
87
+ if (nonEmptyString(value[field])) {
88
+ builder.limit(source, label, value[field] as string, "error");
89
+ }
90
+ }
91
+
92
+ if (Array.isArray(value.events)) {
93
+ const events = value.events
94
+ .map(event)
95
+ .filter((item): item is InvestigationEventEvidence => Boolean(item));
96
+ if (events.length === value.events.length) {
97
+ addEvents(
98
+ builder,
99
+ source,
100
+ events,
101
+ `events:get-resource:${scopeFromArgs(source)}`,
102
+ (typeof value.eventsTotalGroups !== "number" ||
103
+ value.eventsTotalGroups <= events.length) &&
104
+ !nonEmptyString(value.eventsError),
105
+ true,
106
+ relevance,
107
+ );
108
+ if (
109
+ typeof value.eventsTotalGroups === "number" &&
110
+ value.eventsTotalGroups > events.length
111
+ ) {
112
+ builder.limit(
113
+ source,
114
+ "Events",
115
+ `Radar received ${events.length} of ${value.eventsTotalGroups} event groups.`,
116
+ "truncated",
117
+ );
118
+ }
119
+ } else {
120
+ invalidPayload(builder, source, "Events");
121
+ }
122
+ }
123
+ const resourceChangesSubject = {
124
+ kind: resource.kind,
125
+ namespace: resource.metadata.namespace,
126
+ name: resource.metadata.name,
127
+ };
128
+ const recentChangesRaw = value.recentChanges;
129
+ const recentChangesSaturatedRaw = value.recentChangesSaturated;
130
+ const recentChangesCoverageLimitedRaw = value.recentChangesCoverageLimited;
131
+ const hasRecentChangesResult =
132
+ recentChangesRaw !== undefined ||
133
+ recentChangesSaturatedRaw !== undefined ||
134
+ recentChangesCoverageLimitedRaw !== undefined;
135
+ const recentChangesMetadataValid =
136
+ typeof recentChangesSaturatedRaw === "boolean" &&
137
+ typeof recentChangesCoverageLimitedRaw === "boolean";
138
+ if (hasRecentChangesResult && !recentChangesMetadataValid) {
139
+ invalidPayload(builder, source, "Recent changes");
140
+ }
141
+ if (Array.isArray(recentChangesRaw)) {
142
+ const changes = recentChangesRaw
143
+ .map(recentChange)
144
+ .filter((item): item is IssueRecentChange => Boolean(item));
145
+ if (changes.length === recentChangesRaw.length) {
146
+ addChanges(
147
+ builder,
148
+ source,
149
+ changes,
150
+ `changes:get-resource:${scopeFromArgs(source)}`,
151
+ undefined,
152
+ recentChangesMetadataValid &&
153
+ recentChangesSaturatedRaw === false &&
154
+ recentChangesCoverageLimitedRaw === false &&
155
+ !nonEmptyString(value.recentChangesError),
156
+ true,
157
+ relevance,
158
+ resourceChangesSubject,
159
+ );
160
+ } else {
161
+ invalidPayload(builder, source, "Recent changes");
162
+ }
163
+ } else if (
164
+ recentChangesRaw === undefined &&
165
+ recentChangesMetadataValid &&
166
+ !nonEmptyString(value.recentChangesError)
167
+ ) {
168
+ addChanges(
169
+ builder,
170
+ source,
171
+ [],
172
+ `changes:get-resource:${scopeFromArgs(source)}`,
173
+ undefined,
174
+ recentChangesSaturatedRaw === false &&
175
+ recentChangesCoverageLimitedRaw === false,
176
+ true,
177
+ relevance,
178
+ resourceChangesSubject,
179
+ );
180
+ } else if (recentChangesRaw !== undefined) {
181
+ invalidPayload(builder, source, "Recent changes");
182
+ }
183
+ if (recentChangesSaturatedRaw === true) {
184
+ builder.limit(
185
+ source,
186
+ "Recent changes",
187
+ "The recent-change result limit was reached; additional changes may exist in the requested window.",
188
+ "truncated",
189
+ );
190
+ }
191
+ if (recentChangesCoverageLimitedRaw === true) {
192
+ builder.limit(
193
+ source,
194
+ "Recent changes",
195
+ `Change history for ${scopeFromArgs(source)} is incomplete.`,
196
+ "unknown",
197
+ "history",
198
+ );
199
+ }
200
+ }
201
+ export function adaptListResources(
202
+ builder: ProjectionBuilder,
203
+ source: InvestigationEvidenceSource,
204
+ payload: unknown,
205
+ ): void {
206
+ if (!Array.isArray(payload)) {
207
+ invalidPayload(builder, source);
208
+ return;
209
+ }
210
+ const resources = payload
211
+ .map(resourceSummary)
212
+ .filter((item): item is InvestigationResourceSummary => Boolean(item));
213
+ if (resources.length !== payload.length) {
214
+ invalidPayload(builder, source);
215
+ return;
216
+ }
217
+ const scope = scopeFromArgs(source);
218
+ const args = record(parseJSON(source.args ?? ""));
219
+ const kinds = [...new Set(resources.map((resource) => resource.kind))];
220
+ const noun =
221
+ kinds.length === 1
222
+ ? kindToPlural(kinds[0]) === kinds[0].toLowerCase()
223
+ ? displayKind(kinds[0])
224
+ : englishPlural(displayKind(kinds[0]))
225
+ : "Resources";
226
+ const namespace = nonEmptyString(args?.namespace)
227
+ ? args.namespace
228
+ : undefined;
229
+ const title = namespace ? `${noun} in ${namespace}` : noun;
230
+ if (resources.length === 0) {
231
+ // list_resources intentionally returns [] for some RBAC-filtered reads;
232
+ // even a successful transport outcome therefore cannot prove absence.
233
+ builder.limit(
234
+ source,
235
+ "Resource inventory",
236
+ `Radar found no matching resources for ${scope}. Anything you do not have permission to read was not searched.`,
237
+ "unknown",
238
+ );
239
+ return;
240
+ }
241
+ const hasAdverseResource = resources.some((resource) => {
242
+ const health = resource.summaryContext?.health?.toLowerCase();
243
+ return (
244
+ health === "unhealthy" ||
245
+ health === "degraded" ||
246
+ (resource.summaryContext?.issueCount ?? 0) > 0
247
+ );
248
+ });
249
+ builder.observe(`inventory:${source.args ?? scope}`, "inventory", source, {
250
+ tier: "context",
251
+ relevance: "broader",
252
+ tone: hasAdverseResource ? "warning" : "neutral",
253
+ title,
254
+ summary: `${resources.length} returned${nonEmptyString(args?.group) ? ` · ${args.group}` : ""}`,
255
+ data: { type: "inventory", resources, scope },
256
+ });
257
+ }
@@ -0,0 +1,207 @@
1
+ import {
2
+ ProjectionBuilder,
3
+ addNarrowHint,
4
+ addTopologyLimitations,
5
+ invalidPayload,
6
+ relevanceForResource,
7
+ scopeFromArgs,
8
+ topologyPartiality,
9
+ } from "../observations";
10
+ import {
11
+ nonEmptyString,
12
+ record,
13
+ resourceRef,
14
+ stringArray,
15
+ topologyEdge,
16
+ topologyNode,
17
+ } from "../parse";
18
+ import {
19
+ type InvestigationEvidenceSource,
20
+ type InvestigationTopologyEdge,
21
+ type InvestigationTopologyNode,
22
+ } from "../types";
23
+
24
+ export function adaptNeighborhood(
25
+ builder: ProjectionBuilder,
26
+ source: InvestigationEvidenceSource,
27
+ payload: unknown,
28
+ ): void {
29
+ const value = record(payload);
30
+ const root = resourceRef(value?.root);
31
+ const subgraph = record(value?.subgraph);
32
+ if (
33
+ !value ||
34
+ !root ||
35
+ !subgraph ||
36
+ !Array.isArray(subgraph.nodes) ||
37
+ !Array.isArray(subgraph.edges) ||
38
+ typeof value.truncated !== "boolean"
39
+ ) {
40
+ invalidPayload(builder, source);
41
+ return;
42
+ }
43
+ const nodes = subgraph.nodes
44
+ .map(topologyNode)
45
+ .filter((item): item is InvestigationTopologyNode => Boolean(item));
46
+ const edges = subgraph.edges
47
+ .map(topologyEdge)
48
+ .filter((item): item is InvestigationTopologyEdge => Boolean(item));
49
+ if (
50
+ nodes.length !== subgraph.nodes.length ||
51
+ edges.length !== subgraph.edges.length
52
+ ) {
53
+ invalidPayload(builder, source);
54
+ return;
55
+ }
56
+ addNarrowHint(builder, source, value);
57
+ if (value.truncated === true && !nonEmptyString(value.narrowHint)) {
58
+ builder.limit(
59
+ source,
60
+ "Relationships",
61
+ "The relationship view reached its resource limit and may be incomplete.",
62
+ "truncated",
63
+ );
64
+ }
65
+ for (const raw of Array.isArray(value.omitted) ? value.omitted : []) {
66
+ const omitted = record(raw);
67
+ if (
68
+ !omitted ||
69
+ !nonEmptyString(omitted.field) ||
70
+ !nonEmptyString(omitted.reason)
71
+ )
72
+ continue;
73
+ builder.limit(
74
+ source,
75
+ omitted.field,
76
+ `Relationship context omitted: ${omitted.reason.replaceAll("_", " ")}.`,
77
+ omitted.reason === "budget_exceeded" ? "truncated" : "unknown",
78
+ );
79
+ }
80
+ builder.observe(
81
+ `relationships:${root.group ?? ""}:${root.kind}:${root.namespace ?? ""}:${root.name}`,
82
+ "relationships",
83
+ source,
84
+ {
85
+ tier: "context",
86
+ relevance: relevanceForResource(builder, {
87
+ kind: root.kind,
88
+ group: root.group ?? "",
89
+ namespace: root.namespace,
90
+ name: root.name,
91
+ }),
92
+ tone: "info",
93
+ title: `Relationships around ${root.kind} ${root.name}`,
94
+ summary: `${nodes.length} nodes · ${edges.length} relationships`,
95
+ data: {
96
+ type: "relationships",
97
+ root,
98
+ nodes,
99
+ edges,
100
+ truncated: value.truncated,
101
+ },
102
+ },
103
+ );
104
+ }
105
+ export function adaptTopology(
106
+ builder: ProjectionBuilder,
107
+ source: InvestigationEvidenceSource,
108
+ payload: unknown,
109
+ ): void {
110
+ const value = record(payload);
111
+ if (!value) {
112
+ invalidPayload(builder, source);
113
+ return;
114
+ }
115
+ const partiality = topologyPartiality(value);
116
+ if (!partiality) {
117
+ invalidPayload(builder, source, "Topology coverage metadata");
118
+ return;
119
+ }
120
+ const stats = record(value.stats);
121
+ if (
122
+ stats &&
123
+ typeof stats.nodes === "number" &&
124
+ typeof stats.edges === "number" &&
125
+ Array.isArray(value.namespaces)
126
+ ) {
127
+ const namespaces = value.namespaces.flatMap((raw) => {
128
+ const namespace = record(raw);
129
+ const chains = stringArray(namespace?.chains);
130
+ return namespace && nonEmptyString(namespace.namespace) && chains
131
+ ? [{ namespace: namespace.namespace, chains }]
132
+ : [];
133
+ });
134
+ if (namespaces.length !== value.namespaces.length) {
135
+ invalidPayload(builder, source);
136
+ return;
137
+ }
138
+ const problems =
139
+ value.problems === undefined ? [] : stringArray(value.problems);
140
+ if (!problems) {
141
+ invalidPayload(builder, source, "Topology problems");
142
+ return;
143
+ }
144
+ addTopologyLimitations(builder, source, partiality);
145
+ builder.observe(
146
+ `topology:${source.args ?? scopeFromArgs(source)}`,
147
+ "topology",
148
+ source,
149
+ {
150
+ tier: "context",
151
+ relevance: "broader",
152
+ tone: problems.length > 0 ? "warning" : "info",
153
+ title: "Resource topology",
154
+ summary: `${stats.nodes} nodes · ${stats.edges} relationships`,
155
+ data: {
156
+ type: "topology",
157
+ stats: { nodes: stats.nodes, edges: stats.edges },
158
+ namespaces,
159
+ problems,
160
+ warnings: partiality.warnings,
161
+ },
162
+ },
163
+ );
164
+ return;
165
+ }
166
+
167
+ if (!Array.isArray(value.nodes) || !Array.isArray(value.edges)) {
168
+ invalidPayload(builder, source);
169
+ return;
170
+ }
171
+ const nodes = value.nodes
172
+ .map(topologyNode)
173
+ .filter((item): item is InvestigationTopologyNode => Boolean(item));
174
+ const edges = value.edges
175
+ .map(topologyEdge)
176
+ .filter((item): item is InvestigationTopologyEdge => Boolean(item));
177
+ if (
178
+ nodes.length !== value.nodes.length ||
179
+ edges.length !== value.edges.length
180
+ ) {
181
+ invalidPayload(builder, source);
182
+ return;
183
+ }
184
+ const problems = nodes
185
+ .filter((node) => node.status === "unhealthy" || node.status === "degraded")
186
+ .map((node) => `${node.kind} ${node.name}: ${node.status}`);
187
+ addTopologyLimitations(builder, source, partiality);
188
+ builder.observe(
189
+ `topology:${source.args ?? scopeFromArgs(source)}`,
190
+ "topology",
191
+ source,
192
+ {
193
+ tier: "context",
194
+ relevance: "broader",
195
+ tone: problems.length > 0 ? "warning" : "info",
196
+ title: "Resource topology",
197
+ summary: `${nodes.length} nodes · ${edges.length} relationships`,
198
+ data: {
199
+ type: "topology",
200
+ stats: { nodes: nodes.length, edges: edges.length },
201
+ namespaces: [],
202
+ problems,
203
+ warnings: partiality.warnings,
204
+ },
205
+ },
206
+ );
207
+ }