@skyhook-io/radar-app 1.13.5 → 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 (201) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/api/client.authRedirect.test.ts +12 -1
  4. package/src/api/client.prometheusStatus.test.ts +24 -0
  5. package/src/api/client.ts +321 -16
  6. package/src/api/diagnose.ts +1 -0
  7. package/src/api/drain-plan.test.ts +86 -0
  8. package/src/api/quotas.test.ts +26 -0
  9. package/src/api/quotas.ts +20 -0
  10. package/src/api/workloadMetrics.test.ts +40 -0
  11. package/src/api/workloadMetrics.ts +68 -0
  12. package/src/components/CloudConnectFlow.tsx +179 -42
  13. package/src/components/CloudFunnelButton.tsx +107 -36
  14. package/src/components/SyncProgressPanel.tsx +73 -0
  15. package/src/components/audit/AuditView.tsx +2 -0
  16. package/src/components/cloudConnectHandoff.test.ts +140 -0
  17. package/src/components/cloudConnectHandoff.ts +124 -0
  18. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  19. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  20. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  21. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  22. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  23. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  24. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  25. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  26. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  27. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  28. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  29. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  30. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  35. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  36. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  37. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  38. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  39. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  40. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  41. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  42. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  43. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  44. package/src/components/diagnose/target.test.ts +19 -0
  45. package/src/components/diagnose/target.ts +3 -2
  46. package/src/components/gitops/GitOpsView.tsx +7 -0
  47. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  48. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  49. package/src/components/resource/PrometheusCharts.tsx +4 -3
  50. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  51. package/src/components/resource/PrometheusChartsGrid.tsx +75 -35
  52. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  53. package/src/components/resource/RightsizingStrip.tsx +15 -3
  54. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  55. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  56. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  57. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  58. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  59. package/src/components/resource/workloadMetricValues.ts +30 -0
  60. package/src/components/resources/ResourcesView.tsx +44 -6
  61. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  62. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  63. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  64. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  65. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  66. package/src/components/settings/SettingsDialog.tsx +94 -14
  67. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  68. package/src/components/traffic/TrafficGraph.tsx +6 -6
  69. package/src/components/traffic/TrafficView.tsx +10 -9
  70. package/src/components/traffic/trafficFilters.test.ts +28 -1
  71. package/src/components/traffic/trafficFilters.ts +31 -0
  72. package/src/components/workload/WorkloadView.tsx +19 -8
  73. package/src/context/ConnectionContext.tsx +48 -3
  74. package/src/context/DiagnoseCustomization.tsx +1 -0
  75. package/src/index.css +17 -0
  76. package/src/utils/navigation.ts +1 -1
  77. package/src/utils/topology-namespace.test.ts +64 -0
  78. package/src/utils/topology-namespace.ts +18 -0
  79. package/src/components/diagnose/investigationEvidence.test.ts +0 -6617
  80. package/src/components/diagnose/investigationEvidence.ts +0 -5572
  81. package/src/components/gitops/GitOpsActions.tsx +0 -1
  82. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  83. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  84. package/src/components/gitops/SyncCountdown.tsx +0 -1
  85. package/src/components/gitops/index.ts +0 -4
  86. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  87. package/src/components/logs/JsonLogLine.tsx +0 -1
  88. package/src/components/logs/LogCore.tsx +0 -2
  89. package/src/components/logs/useLogBuffer.ts +0 -2
  90. package/src/components/logs/useLogSearch.ts +0 -1
  91. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  92. package/src/components/resources/drawer-components.tsx +0 -1
  93. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  94. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  95. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  96. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  97. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  98. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  99. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  100. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  101. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  102. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  103. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  104. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  105. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  106. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  107. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  108. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  109. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  110. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  111. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  112. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  113. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  114. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  115. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  116. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  117. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  118. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  119. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  120. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  121. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  122. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  162. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  169. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  170. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  171. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  172. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  173. package/src/components/resources/renderers/index.ts +0 -94
  174. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  175. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  176. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  177. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  178. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  179. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  180. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  181. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  182. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  183. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  184. package/src/components/resources/resource-utils-argo.ts +0 -2
  185. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  186. package/src/components/resources/resource-utils-eso.ts +0 -2
  187. package/src/components/resources/resource-utils-flux.ts +0 -2
  188. package/src/components/resources/resource-utils-istio.ts +0 -2
  189. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  190. package/src/components/resources/resource-utils-knative.ts +0 -2
  191. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  192. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  193. package/src/components/resources/resource-utils-traefik.ts +0 -1
  194. package/src/components/resources/resource-utils-trivy.ts +0 -2
  195. package/src/components/resources/resource-utils-velero.ts +0 -2
  196. package/src/components/shared/EditableYamlView.tsx +0 -24
  197. package/src/components/timeline/DiffViewer.tsx +0 -1
  198. package/src/components/traffic/index.ts +0 -3
  199. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  200. package/src/components/ui/MetricsChart.tsx +0 -1
  201. 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
+ }