@skyhook-io/radar-app 1.13.5 → 1.14.1

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 (242) hide show
  1. package/package.json +9 -9
  2. package/src/App.tsx +66 -28
  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 +324 -16
  6. package/src/api/diagnose.ts +32 -1
  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 +343 -46
  13. package/src/components/CloudFunnelButton.tsx +110 -37
  14. package/src/components/ConnectionErrorView.tsx +9 -10
  15. package/src/components/DebugOverlay.tsx +10 -6
  16. package/src/components/SyncProgressPanel.tsx +73 -0
  17. package/src/components/audit/AuditView.tsx +2 -0
  18. package/src/components/cloudConnectHandoff.test.ts +245 -0
  19. package/src/components/cloudConnectHandoff.ts +276 -0
  20. package/src/components/curl/ServiceCurlButton.tsx +19 -26
  21. package/src/components/diagnose/AgentCase.tsx +18 -5
  22. package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
  23. package/src/components/diagnose/AnalysisStory.tsx +564 -0
  24. package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
  25. package/src/components/diagnose/DiagnoseContext.tsx +26 -8
  26. package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
  27. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
  28. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
  29. package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
  30. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
  31. package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
  32. package/src/components/diagnose/InvestigationView.tsx +493 -529
  33. package/src/components/diagnose/investigationCase.test.tsx +267 -129
  34. package/src/components/diagnose/investigationCase.ts +122 -77
  35. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  47. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  48. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +81 -0
  49. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +285 -0
  50. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  51. package/src/components/diagnose/investigationEvidence/builder.ts +427 -0
  52. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  53. package/src/components/diagnose/investigationEvidence/identity.test.ts +632 -0
  54. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  55. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  56. package/src/components/diagnose/investigationEvidence/observations.ts +978 -0
  57. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  58. package/src/components/diagnose/investigationEvidence/projection.test.ts +2736 -0
  59. package/src/components/diagnose/investigationEvidence/types.ts +514 -0
  60. package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
  61. package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
  62. package/src/components/diagnose/investigationState.test.ts +316 -58
  63. package/src/components/diagnose/investigationState.ts +257 -44
  64. package/src/components/diagnose/investigationStory.test.ts +161 -0
  65. package/src/components/diagnose/investigationStory.ts +207 -0
  66. package/src/components/diagnose/parts.test.tsx +9 -6
  67. package/src/components/diagnose/parts.tsx +977 -193
  68. package/src/components/diagnose/storyParts.test.tsx +426 -0
  69. package/src/components/diagnose/target.test.ts +19 -0
  70. package/src/components/diagnose/target.ts +3 -2
  71. package/src/components/diagnose/toolCallLabel.test.ts +51 -0
  72. package/src/components/diagnose/toolCallLabel.ts +135 -0
  73. package/src/components/diagnose/useDisclosureReveal.ts +10 -11
  74. package/src/components/gitops/GitOpsView.tsx +7 -0
  75. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  76. package/src/components/helm/HelmCompareRoute.tsx +18 -4
  77. package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
  78. package/src/components/helm/InstallWizard.tsx +8 -3
  79. package/src/components/home/MCPSetupDialog.tsx +15 -9
  80. package/src/components/portforward/PortForwardManager.tsx +6 -13
  81. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  82. package/src/components/resource/PrometheusCharts.tsx +4 -3
  83. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  84. package/src/components/resource/PrometheusChartsGrid.tsx +75 -35
  85. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  86. package/src/components/resource/RightsizingStrip.tsx +15 -3
  87. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  88. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  89. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  90. package/src/components/resource/WorkloadMetricsSection.tsx +480 -0
  91. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  92. package/src/components/resource/workloadMetricValues.ts +30 -0
  93. package/src/components/resources/PodFilePreview.tsx +3 -3
  94. package/src/components/resources/ResourcesView.tsx +44 -6
  95. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  96. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  97. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  98. package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
  99. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  100. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  101. package/src/components/settings/SettingsDialog.tsx +98 -16
  102. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  103. package/src/components/traffic/TrafficGraph.tsx +6 -6
  104. package/src/components/traffic/TrafficView.tsx +10 -9
  105. package/src/components/traffic/trafficFilters.test.ts +28 -1
  106. package/src/components/traffic/trafficFilters.ts +31 -0
  107. package/src/components/ui/CommandPalette.tsx +10 -6
  108. package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
  109. package/src/components/ui/Disclosure.tsx +47 -0
  110. package/src/components/ui/Markdown.tsx +23 -11
  111. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
  112. package/src/components/ui/UpdateNotification.tsx +18 -2
  113. package/src/components/workload/WorkloadView.tsx +19 -8
  114. package/src/context/ConnectionContext.tsx +48 -3
  115. package/src/context/DiagnoseCustomization.tsx +1 -0
  116. package/src/index.css +36 -6
  117. package/src/utils/navigation.ts +1 -1
  118. package/src/utils/topology-namespace.test.ts +64 -0
  119. package/src/utils/topology-namespace.ts +18 -0
  120. package/src/components/diagnose/investigationEvidence.test.ts +0 -6617
  121. package/src/components/diagnose/investigationEvidence.ts +0 -5572
  122. package/src/components/gitops/GitOpsActions.tsx +0 -1
  123. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  124. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  125. package/src/components/gitops/SyncCountdown.tsx +0 -1
  126. package/src/components/gitops/index.ts +0 -4
  127. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  128. package/src/components/logs/JsonLogLine.tsx +0 -1
  129. package/src/components/logs/LogCore.tsx +0 -2
  130. package/src/components/logs/useLogBuffer.ts +0 -2
  131. package/src/components/logs/useLogSearch.ts +0 -1
  132. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  133. package/src/components/resources/drawer-components.tsx +0 -1
  134. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  191. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  198. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  199. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  200. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  201. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  202. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  203. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  204. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  205. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  206. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  207. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  208. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  209. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  210. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  211. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  212. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  213. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  214. package/src/components/resources/renderers/index.ts +0 -94
  215. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  216. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  217. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  218. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  219. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  220. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  221. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  222. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  223. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  224. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  225. package/src/components/resources/resource-utils-argo.ts +0 -2
  226. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  227. package/src/components/resources/resource-utils-eso.ts +0 -2
  228. package/src/components/resources/resource-utils-flux.ts +0 -2
  229. package/src/components/resources/resource-utils-istio.ts +0 -2
  230. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  231. package/src/components/resources/resource-utils-knative.ts +0 -2
  232. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  233. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  234. package/src/components/resources/resource-utils-traefik.ts +0 -1
  235. package/src/components/resources/resource-utils-trivy.ts +0 -2
  236. package/src/components/resources/resource-utils-velero.ts +0 -2
  237. package/src/components/shared/EditableYamlView.tsx +0 -24
  238. package/src/components/timeline/DiffViewer.tsx +0 -1
  239. package/src/components/traffic/index.ts +0 -3
  240. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  241. package/src/components/ui/MetricsChart.tsx +0 -1
  242. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -33,6 +33,8 @@ export interface InvestigationCaseItem {
33
33
  index: number;
34
34
  role: DiagnosisEvidenceRole;
35
35
  claim: string;
36
+ /** What the agent says this result does not cover. */
37
+ gap?: string;
36
38
  subject?: DiagnosisEvidenceSubject;
37
39
  source: InvestigationEvidenceSource;
38
40
  placement: InvestigationCasePlacement;
@@ -51,65 +53,6 @@ export interface InvestigationCaseResolution {
51
53
  ruledOut: InvestigationCaseRuledOut[];
52
54
  }
53
55
 
54
- /**
55
- * Notes the agent put on cards in earlier assessments stay on those cards
56
- * when a later turn takes over the pane's case, unless the later turn
57
- * addressed the same card; a follow-up about one chart must not strip the
58
- * initial assessment's reading from everything else. Ordering and the
59
- * ruled-out list follow the live turn alone, and source-placed notes stay
60
- * listed under their own assessment. `earlier` is newest first.
61
- */
62
- export function mergeInvestigationCases(
63
- live: InvestigationCaseResolution | undefined,
64
- earlier: readonly (InvestigationCaseResolution | undefined)[],
65
- ): InvestigationCaseResolution | undefined {
66
- const liveItems = live?.items ?? [];
67
- const covered = new Set(
68
- liveItems.flatMap((item) => (item.groupId ? [item.groupId] : [])),
69
- );
70
- const carried: InvestigationCaseItem[] = [];
71
- for (const resolution of earlier) {
72
- // The newest assessment that spoke about a group wins it outright, and it
73
- // wins with everything it said: a card note and a pinned revision note are
74
- // two readings of one group, not rivals. Coverage is therefore taken after
75
- // the whole resolution, not as each of its items is carried.
76
- const takenHere = new Set<string>();
77
- for (const item of resolution?.items ?? []) {
78
- if (item.placement === "source" || !item.groupId) continue;
79
- if (covered.has(item.groupId)) continue;
80
- takenHere.add(item.groupId);
81
- carried.push(item);
82
- }
83
- for (const groupId of takenHere) covered.add(groupId);
84
- }
85
- if (carried.length === 0) return live;
86
- return { items: [...liveItems, ...carried], ruledOut: live?.ruledOut ?? [] };
87
- }
88
-
89
- /**
90
- * The subset of one assessment's items that survived into the case the pane
91
- * actually renders. Matching is by value, not object identity: the merge is
92
- * fed freshly resolved copies of every turn, so an identity test would report
93
- * that an assessment's own note had vanished the moment any other turn was
94
- * re-resolved.
95
- */
96
- export function investigationCaseItemsStillRendered(
97
- assessmentItems: readonly InvestigationCaseItem[] | undefined,
98
- renderedItems: readonly InvestigationCaseItem[] | undefined,
99
- ): InvestigationCaseItem[] {
100
- if (!assessmentItems?.length || !renderedItems?.length) return [];
101
- const key = (item: InvestigationCaseItem) =>
102
- [
103
- item.index,
104
- item.source.id,
105
- item.placement,
106
- item.groupId ?? "",
107
- item.observation ? investigationCaseObservationKey(item.observation) : "",
108
- ].join("\u0000");
109
- const rendered = new Set(renderedItems.map(key));
110
- return assessmentItems.filter((item) => rendered.has(key(item)));
111
- }
112
-
113
56
  /**
114
57
  * One half of a Go↔TS contract: `evidenceRoles` in internal/ai/parse.go and the
115
58
  * DiagnosisEvidenceRole union in api/diagnose.ts must list exactly these roles.
@@ -134,10 +77,12 @@ export function investigationCaseObservationKey(
134
77
 
135
78
  /**
136
79
  * Resolves the agent's case for one assessment. Refs are re-validated against
137
- * the assessment turn exactly like root-cause refs; an item whose ref does not
138
- * resolve is dropped on its own. Placement follows the observation the
139
- * (ref, subject) pair names: exactly one match pins the claim to that
140
- * observation, anything else falls back to the source row.
80
+ * the run up to and including the assessment turn, exactly like root-cause
81
+ * refs — a revised assessment may rest on a result read in an earlier turn,
82
+ * and the observation it resolves to is that earlier read, never a later one.
83
+ * An item whose ref does not resolve is dropped on its own. Placement follows
84
+ * the observation the (ref, subject) pair names: exactly one match pins the
85
+ * claim to that observation, anything else falls back to the source row.
141
86
  */
142
87
  export function resolveInvestigationCase(
143
88
  projection: InvestigationEvidenceProjection,
@@ -149,17 +94,17 @@ export function resolveInvestigationCase(
149
94
  ): InvestigationCaseResolution {
150
95
  const evidence = diagnosis?.evidence ?? [];
151
96
  if (evidence.length === 0) return { items: [], ruledOut: [] };
97
+ const targetPods = new Set(projection.targetPods);
152
98
  const byRef = new Map<string, InvestigationEvidenceSource[]>();
153
99
  for (const source of projection.evidenceRefSources) {
154
- if (source.turnIndex !== assessmentTurnIndex || !source.evidenceRef)
155
- continue;
100
+ if (source.turnIndex > assessmentTurnIndex || !source.evidenceRef) continue;
156
101
  const matches = byRef.get(source.evidenceRef) ?? [];
157
102
  matches.push(source);
158
103
  byRef.set(source.evidenceRef, matches);
159
104
  }
160
105
  const citableSourceIds = new Set(
161
106
  projection.citableSources
162
- .filter((source) => source.turnIndex === assessmentTurnIndex)
107
+ .filter((source) => source.turnIndex <= assessmentTurnIndex)
163
108
  .map((source) => source.id),
164
109
  );
165
110
  const items: InvestigationCaseItem[] = [];
@@ -188,6 +133,9 @@ export function resolveInvestigationCase(
188
133
  index,
189
134
  role: entry.role,
190
135
  claim: typeof entry.claim === "string" ? entry.claim.trim() : "",
136
+ ...(typeof entry.gap === "string" && entry.gap.trim()
137
+ ? { gap: entry.gap.trim() }
138
+ : {}),
191
139
  ...(subject ? { subject } : {}),
192
140
  source,
193
141
  placement: "source",
@@ -200,7 +148,12 @@ export function resolveInvestigationCase(
200
148
  (observation) =>
201
149
  observation.source.id === source.id &&
202
150
  (!subject ||
203
- observationMatchesSubject(group, observation, subject)),
151
+ observationMatchesSubject(
152
+ group,
153
+ observation,
154
+ subject,
155
+ targetPods,
156
+ )),
204
157
  )
205
158
  .map((observation) => ({ group, observation })),
206
159
  );
@@ -241,11 +194,11 @@ export function resolveInvestigationCase(
241
194
  function validCaseSubject(
242
195
  value: DiagnosisEvidenceSubject | undefined,
243
196
  ): DiagnosisEvidenceSubject | undefined {
244
- if (!value || !nonEmptyString(value.kind) || !nonEmptyString(value.name))
245
- return undefined;
197
+ if (!value || !nonEmptyString(value.kind)) return undefined;
246
198
  const optional = (field: unknown) =>
247
199
  field === undefined || typeof field === "string";
248
200
  if (
201
+ !optional(value.name) ||
249
202
  !optional(value.group) ||
250
203
  !optional(value.namespace) ||
251
204
  !optional(value.container) ||
@@ -268,6 +221,8 @@ interface ObservationSubjectIdentity {
268
221
  name: string;
269
222
  container?: string;
270
223
  stream?: "current" | "previous";
224
+ /** A listing names a kind (and scope) but no single object. */
225
+ listing?: boolean;
271
226
  }
272
227
 
273
228
  /**
@@ -319,17 +274,44 @@ function observationSubjectIdentity(
319
274
  };
320
275
  }
321
276
  const args = investigationSourceArgs(observation.source);
322
- if (!args || !nonEmptyString(args.kind) || !nonEmptyString(args.name))
323
- return undefined;
277
+ // A listing whose call names no kind (list_namespaces) is still a listing
278
+ // of the one kind its rows carry.
279
+ if (data.type === "inventory" && !nonEmptyString(args?.kind)) {
280
+ const kinds = new Set(data.resources.map((resource) => resource.kind));
281
+ if (kinds.size !== 1) return undefined;
282
+ return {
283
+ kind: [...kinds][0],
284
+ namespace: nonEmptyString(args?.namespace) ? args.namespace : undefined,
285
+ name: "",
286
+ listing: true,
287
+ };
288
+ }
289
+ if (!args || !nonEmptyString(args.kind)) return undefined;
324
290
  return {
325
291
  kind: args.kind,
326
292
  group: nonEmptyString(args.group) ? args.group : undefined,
327
293
  namespace: nonEmptyString(args.namespace) ? args.namespace : undefined,
328
- name: args.name,
294
+ name: nonEmptyString(args.name) ? args.name : "",
295
+ ...(nonEmptyString(args.name) ? {} : { listing: true }),
329
296
  };
330
297
  }
331
298
 
332
- function sameKind(left: string, right: string): boolean {
299
+ // No CRD lives in a built-in API group: the core group under its three
300
+ // spellings, the four legacy unsuffixed groups, or any group under k8s.io.
301
+ const LEGACY_BUILT_IN_GROUPS = new Set([
302
+ "",
303
+ "core",
304
+ "v1",
305
+ "apps",
306
+ "batch",
307
+ "autoscaling",
308
+ "policy",
309
+ ]);
310
+ function isBuiltInGroup(group: string): boolean {
311
+ return LEGACY_BUILT_IN_GROUPS.has(group) || group.endsWith(".k8s.io");
312
+ }
313
+
314
+ export function sameKind(left: string, right: string): boolean {
333
315
  return pluralToKind(left).toLowerCase() === pluralToKind(right).toLowerCase();
334
316
  }
335
317
 
@@ -342,6 +324,7 @@ function observationMatchesSubject(
342
324
  group: InvestigationEvidenceGroup,
343
325
  observation: InvestigationEvidenceObservation,
344
326
  subject: DiagnosisEvidenceSubject,
327
+ targetPods: ReadonlySet<string>,
345
328
  ): boolean {
346
329
  if (subject.observation !== undefined) {
347
330
  // A diagnose bundle captures several vitals charts for one resource, so
@@ -349,7 +332,10 @@ function observationMatchesSubject(
349
332
  // whose identity ends in that category. An agent-run query is one chart,
350
333
  // so a qualifier on it carries no meaning.
351
334
  const [kind, qualifier] = subject.observation.toLowerCase().split(":", 2);
352
- if (kind !== observation.data.type) return false;
335
+ // A listing is resources too: "resource" names an inventory card as well.
336
+ const inventoryAsResource =
337
+ kind === "resource" && observation.data.type === "inventory";
338
+ if (kind !== observation.data.type && !inventoryAsResource) return false;
353
339
  if (
354
340
  qualifier !== undefined &&
355
341
  observation.data.type === "metrics" &&
@@ -359,6 +345,10 @@ function observationMatchesSubject(
359
345
  return false;
360
346
  }
361
347
  }
348
+ // One rules result holds many rules and none states a resource; the rule's
349
+ // name is the handle that tells them apart.
350
+ if (observation.data.type === "alerts" && subject.name !== undefined)
351
+ return observation.data.rule.name === subject.name;
362
352
  const identities = observationIdentities(observation);
363
353
  // An observation that states no resource of its own (an agent-run query
364
354
  // whose selectors do not name the target exactly, a topology summary) can
@@ -366,7 +356,7 @@ function observationMatchesSubject(
366
356
  // the rest.
367
357
  if (identities.length === 0) return subject.observation !== undefined;
368
358
  return identities.some((identity) =>
369
- identityMatchesSubject(identity, observation, subject),
359
+ identityMatchesSubject(identity, observation, subject, targetPods),
370
360
  );
371
361
  }
372
362
 
@@ -397,17 +387,72 @@ function observationIdentities(
397
387
  return identities;
398
388
  }
399
389
 
390
+ // Pod names are DNS subdomains (letters, digits, hyphens, dots), so a name
391
+ // is a whole word between characters that cannot be part of one; "api"
392
+ // inside "api-other" names another Pod.
393
+ function namesPod(text: string, name: string): boolean {
394
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
395
+ return new RegExp(`(^|[^A-Za-z0-9.-])${escaped}(?![A-Za-z0-9.-])`).test(text);
396
+ }
397
+
400
398
  function identityMatchesSubject(
401
399
  identity: ObservationSubjectIdentity,
402
400
  observation: InvestigationEvidenceObservation,
403
401
  subject: DiagnosisEvidenceSubject,
402
+ targetPods: ReadonlySet<string>,
404
403
  ): boolean {
405
- if (!sameKind(identity.kind, subject.kind) || identity.name !== subject.name)
404
+ // "The events of namespace X" names a namespace-scoped read whose
405
+ // observation inherits the resource its call was asked about; the agent
406
+ // naming the namespace instead is not wrong, so a stated evidence kind plus
407
+ // a matching namespace is enough for that shape.
408
+ if (
409
+ subject.observation !== undefined &&
410
+ sameKind(subject.kind, "Namespace") &&
411
+ identity.namespace !== undefined &&
412
+ identity.namespace === subject.name
413
+ )
414
+ return true;
415
+ // A workload read collects its pods' events, so "the events of Pod X" names
416
+ // the events observation a diagnose bundle or workload call inherited from
417
+ // the workload; the ref already fixes the call, uniqueness fixes the card.
418
+ if (
419
+ subject.observation !== undefined &&
420
+ observation.data.type === "events" &&
421
+ sameKind(subject.kind, "Pod") &&
422
+ !sameKind(identity.kind, "Pod") &&
423
+ (identity.namespace === undefined ||
424
+ subject.namespace === undefined ||
425
+ identity.namespace === subject.namespace) &&
426
+ // The Pod has to be the target's own on events the target's read produced,
427
+ // or named whole by an event: a Pod nothing here establishes is not this card.
428
+ (subject.name === undefined ||
429
+ (targetPods.has(subject.name) && observation.relevance !== "broader") ||
430
+ observation.data.events.some((event) =>
431
+ namesPod(event.message, subject.name!),
432
+ ))
433
+ )
434
+ return true;
435
+ if (!sameKind(identity.kind, subject.kind)) return false;
436
+ // A subject with no name (a listing cited for what it does not contain) is
437
+ // a wildcard that uniqueness still gates. A listing has no name of its own; the agent naming the entry it means
438
+ // ("ConfigMap kube-root-ca.crt" in the ConfigMaps of a namespace) still
439
+ // points at that listing, whether or not the listing holds the entry: an
440
+ // absent entry is often the point, and the card says which it is.
441
+ if (
442
+ !identity.listing &&
443
+ subject.name !== undefined &&
444
+ identity.name !== subject.name
445
+ )
406
446
  return false;
447
+ // A built-in group on a core kind ("apps" on a Pod) is the agent
448
+ // misremembering the API, not naming a different object: no CRD lives in a
449
+ // built-in group, so nothing else could be meant. A vendor group on a core
450
+ // kind (a Knative Service) still names a different object.
407
451
  if (
408
452
  subject.group !== undefined &&
409
453
  identity.group !== undefined &&
410
- subject.group.toLowerCase() !== identity.group.toLowerCase()
454
+ subject.group.toLowerCase() !== identity.group.toLowerCase() &&
455
+ !(identity.group === "" && isBuiltInGroup(subject.group.toLowerCase()))
411
456
  ) {
412
457
  return false;
413
458
  }
@@ -0,0 +1,60 @@
1
+ import { type IssueRecentChange } from "@skyhook-io/k8s-ui";
2
+ import {
3
+ ProjectionBuilder,
4
+ addChanges,
5
+ addNarrowHint,
6
+ changesSubjectFromArgs,
7
+ invalidPayload,
8
+ scopeFromArgs,
9
+ sourceArgsRelevance,
10
+ } from "../observations";
11
+ import { nonEmptyString, parseJSON, recentChange, record } from "../parse";
12
+ import { type InvestigationEvidenceSource } from "../types";
13
+
14
+ export function adaptChanges(
15
+ builder: ProjectionBuilder,
16
+ source: InvestigationEvidenceSource,
17
+ payload: unknown,
18
+ ): void {
19
+ const value = record(payload);
20
+ // The producer serializes an empty result as a nil slice, which is null.
21
+ const changesRaw = value?.changes === null ? [] : value?.changes;
22
+ if (!value || !Array.isArray(changesRaw)) {
23
+ invalidPayload(builder, source, "Recent changes");
24
+ return;
25
+ }
26
+ const changes = changesRaw
27
+ .map(recentChange)
28
+ .filter((item): item is IssueRecentChange => Boolean(item));
29
+ if (changes.length !== changesRaw.length) {
30
+ invalidPayload(builder, source, "Recent changes");
31
+ return;
32
+ }
33
+ addNarrowHint(builder, source, value);
34
+ let sourceErrors = 0;
35
+ if (Array.isArray(value.sourcesErrored)) {
36
+ for (const sourceError of value.sourcesErrored) {
37
+ if (nonEmptyString(sourceError)) {
38
+ sourceErrors += 1;
39
+ builder.limit(source, "Recent changes", sourceError, "error");
40
+ }
41
+ }
42
+ } else if (value.sourcesErrored !== undefined) {
43
+ invalidPayload(builder, source, "Recent changes source coverage");
44
+ }
45
+ // Reads of one resource's history are revisions of one card however the
46
+ // window or cap differs; the window is stated in the title instead.
47
+ const args = record(source.args ? parseJSON(source.args) : undefined);
48
+ addChanges(
49
+ builder,
50
+ source,
51
+ changes,
52
+ `changes:${scopeFromArgs(source)}`,
53
+ undefined,
54
+ !nonEmptyString(value.narrowHint) && sourceErrors === 0,
55
+ false,
56
+ sourceArgsRelevance(builder, source),
57
+ changesSubjectFromArgs(source),
58
+ nonEmptyString(args?.since) ? args.since : undefined,
59
+ );
60
+ }