Skip to content

Render cited Prometheus range queries as chart cards with change markers - #1684

Merged
nadaverell merged 12 commits into
mainfrom
feat/findings-chart-card
Sep 9, 2026
Merged

nadaverell merged 12 commits into
mainfrom
feat/findings-chart-card

Conversation

@nadaverell

@nadaverell nadaverell commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

When the investigating agent runs a Prometheus range query, Findings now renders it as a chart card with the query underneath, and draws the changes Radar recorded for that workload in the same window as vertical markers on the chart. The prompt tells the agent when Prometheus is actually connected, so it queries instead of guessing. Track B of the Findings next layer, stacked on #1678 (Track 0), which provides internal/prometheus.Availability and the selectors inventory on query_prometheus results.

A chart only claims to be about the investigation target when every selector in the query is scoped to it; anything broader shows only when the assessment cites it. Markers say "a change was recorded here", never "this caused that".

What changed

Findings (web/)

  • New metrics evidence kind and a query_prometheus adapter. Range results become a chart, instant results a compact value table, producer-truncated results a coverage limitation carrying the cardinality summary (no chart).
  • Scope comes from the producer's selectors inventory, never from the expression text. target only when every selector carries the target namespace (namespace="shop" or the anchored namespace=~"shop") and a pod or workload matcher naming the target: pod="<name>-…", deployment|statefulset|daemonset|workload="<name>" matching the target kind, or the exact regex forms pod=~"<name>-.*" / <label>=~"<name>" with the name's dot escaped (an unescaped api.v2 or an api-?.* also selects other workloads, so those stay unclaimed). A Pod target is named only by its own exact name, since its <name>- prefix selects other pods. Namespace-only selectors are producer-related; a bare selector, a sibling workload, an alternation, empty selectors or selectorsUnknown are broader.
  • metrics joins the focused kinds in partitionInvestigationEvidence, so a cited broader chart enters the main list (one observation per query_prometheus result, so today's legacy citation selects it). Uncited target-scoped charts sit in "More evidence about this workload"; uncited broader charts are withheld and counted: "N broader metric results are not shown; they appear here when the assessment cites them."
  • Change markers (investigationMetrics.ts): only same-turn changes observations with relevance target or producer-related, only changes whose resource is the chart's subject or one of the relatives producers named in that turn (consumed ConfigMaps/Secrets, the owner, and the pods/ReplicaSets the diagnosis rows identified). Absolute IssueRecentChange.timestamp only, inside the chart window, deduplicated on apiVersion+kind+namespace+name+timestamp. A change without an apiVersion (Helm history rows, older stores) matches on kind, namespace and name against the subject and its relatives; one with an apiVersion must match the group exactly. Label names the changed resource; hover tooltip says "Change recorded". No crash markers, no relative changeContext.when.
  • Axis label is the raw expression, truncated with the full text in a tooltip and repeated in full under the chart when truncated. The unit is what every metric in the query states through its name (from the selector inventory): _bytes stays bytes through aggregations, a rate() of bytes is bytes/s, _seconds stays seconds; a division, disagreeing metrics or a bare number leave the axis unitless, and unitless axes scale with k/M/G.
  • Series are named by every label when the pod/instance/node label is missing or shared (an aggregation by (container) reads container=api, not series-0), derived once from the whole result so names stay distinct across chart and table, with a legend under multi-series charts. A series with a single finite sample cannot draw a line, so it is listed with its time and value under the chart; a series whose samples are all gaps (Prometheus serializes NaN and infinities as null) is named as returning no finite values.
  • Card actions are the existing View source and the existing open-resource button (subject = target when scope is target).

@skyhook-io/k8s-ui AreaChart (additive)

  • annotations?: ChartAnnotation[] and domain?: {start, end}. Vertical dashed line plus a label pill per annotation; labels stack into rows to avoid collisions, are hidden past three rows or when the rendered chart is narrower than 420px, and the text moves into the hover tooltip in both cases.
  • Y axis handles negative minima (zero line drawn, area fills toward zero). A count unit gets an integer axis on a 1/2/5 step (0 and 1 for an all-zero series, 0/5/10/15/20 for a maximum of 18). Existing callers are unchanged: with no annotations, no domain and non-negative data the geometry is identical.
  • layout="auto" (opt-in; Findings uses it) switches to a compact layout below the 420px label-hiding width: two Y ticks, first and last X ticks, larger text and a taller plot, so a narrow pane shows a readable chart instead of a strip. layout="compact" forces it for tests.
  • ChartAnnotation exported from charts/types.ts and the charts index; formatMetricValue gains a seconds unit and a sign for negative values; seriesDisplayLabels is the one place the chart tooltip and SeriesLegend derive series names from, and both accept an optional seriesLabels override for callers that chart a subset of a result.

Prompt nudge (Go)

  • RunManager.MetricsAvailability hook, wired in internal/server to prometheus.Availability. Probed under a 2 s timeout only for investigation turns (initial and follow-up); explanation, apply and the scripted post-apply verification turn never probe, so verification stays on checking the confirmed fix.
  • Request.Metrics carries the result; prompt selection moved into turnPrompt, which appends the sentence after selection so follow-ups get it too. Only connected produces text: "Prometheus is connected at ; for resource, restart, throttling or latency questions run one query_prometheus range query over the failure window and cite it." Other states say nothing. Credentials embedded in a configured URL are stripped before the address reaches the model-visible prompt.

Testing

  • make tsc: pass
  • cd web && npm test: 927 passed, 2 failed in src/api/client.authRedirect.test.ts (5 s timeouts). The same file fails the same way on main at b54b311 in a clean checkout; unrelated to this diff.
  • cd packages/k8s-ui && npm test: pass (3560 tests, 1 skipped)
  • go test ./...: pass
  • make build: pass
  • git diff --check: clean
  • New tests: adapter (range, instant, truncated, empty result, revision merge, 13 scope cases including a braced target numerator over a bare cluster denominator, empty selectors, unknown selectors, pod regex alternation, optional-dash regex, dotted names escaped and unescaped, Pod targets, anchored and alternated namespace regexes), series display labels, sparse and null-only series listing, legend, unit inference (aggregations, rate and increase wrappers, mixed metrics, ratios, bare numbers, slashes inside matcher values), unitless value formatting, marker derivation (relatives, sibling exclusion, other-turn and broader exclusion, non-target chart), pane placement (uncited target → workload, uncited broader → withheld with note, cited broader → main, markers rendered and subject linked), AreaChart.test.tsx (annotations in/out of domain, row stacking and hidden labels, truncation, explicit domain wider than samples, negative values and zero line, null gaps, sparse samples, empty, compact layout, count axes) and axis.test.ts, Go (nudge text per turn kind and connection state, credential stripping; RunManager probes only investigation turns with a deadline, not explanation, apply or verification).
  • Visual test on a fixture run (a cited target-scoped range query, an uncited cluster-wide query, and two in-window changes, against kind-radar-kyverno-demo): verified the chart card in the main list with both markers labelled, the hover tooltip reading "Change recorded · ConfigMap demo-api-config" next to the sample value, the withheld-metrics note, light and dark themes, a 720px viewport, a forced 380px-wide card showing marker lines without labels, and a forced 390px card in the docked pane rendering the compact layout (two Y ticks, edge X ticks, 225px-tall plot). No console errors.

Scope and tradeoffs

  • Uncited broader charts are deliberately withheld; the note tells the reader they exist and how they get shown.
  • A pod=~"<name>-.*" prefix is accepted as naming a workload target per the plan; a sibling whose name starts with the same prefix would match that regex in Prometheus too, so this mirrors what the query actually selects.
  • Relatives for markers come only from producer rows in the same turn; a ReplicaSet or Pod that no producer named in that turn does not qualify, even if its name starts with the workload's.
  • No "same range" link: the resource charts are relative-time and curated, so a saved window cannot be reopened there.
  • Diagnose-origin metrics (Track D1) use the same payload and renderer; their placement into main waits for Track C per D-5.

Note

Medium Risk
Touches investigation evidence partitioning, model-visible prompts, and new chart rendering paths; scope rules and credential stripping reduce mis-attribution and secret leakage risk.

Overview
Investigation Findings now surfaces query_prometheus results as first-class metrics evidence: range queries render as chart cards (with instant queries as value tables and oversized results as limitations), scoped using the producer selectors inventory so only fully target-scoped charts show uncited; broader queries stay hidden until cited, with a separate withheld-metrics count.

Charts can show same-turn change markers (subject and producer-named relatives inside the query window) via extended AreaChart support (annotations, domain, compact layout="auto", count axes, negative values). The Go RunManager probes Prometheus on read-only investigation turns only and appends a prompt nudge when connected, with URL credential stripping before text reaches the model.

Reviewed by Cursor Bugbot for commit 54842ba. Bugbot is set up for automated code reviews on this repo. Configure here.

@nadaverell
nadaverell requested a review from hisco as a code owner September 8, 2026 06:44
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Render Prometheus findings as charts with change markers

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Render Prometheus range and instant results as scoped Findings evidence cards.
• Mark same-turn related resource changes without implying causation.
• Probe Prometheus before read-only turns so agents gather cited metrics.
Diagram

sequenceDiagram
    participant RM as Run Manager
    participant Prom as Prometheus
    participant Agent as AI Agent
    participant Projector as Evidence Projector
    participant Pane as Findings Pane
    participant Chart as Area Chart
    RM->>Prom: Probe availability
    Prom-->>RM: Connection state
    RM->>Agent: Metrics-aware prompt
    Agent->>Prom: Range or instant query
    Prom-->>Agent: Series and selectors
    Agent->>Projector: Cited tool result
    Projector->>Projector: Validate and classify scope
    Projector->>Pane: Typed metrics evidence
    Pane->>Pane: Apply citation visibility
    Pane->>Chart: Range series and markers
Loading
High-Level Assessment

The approach is appropriate: producer-supplied selector inventories establish scope more safely than parsing PromQL text, typed evidence adaptation preserves honest empty and truncated states, and the existing shared AreaChart avoids introducing another visualization stack. Rendering raw tool payloads or inferring scope from expressions was reasonably dismissed because either would weaken consistency and evidence trust.

Files changed (20) +2225 / -44

Enhancement (14) +1057 / -40
diagnoser.goAdd Prometheus availability to investigation prompts +45/-11

Add Prometheus availability to investigation prompts

• Adds a bounded metrics availability contract to diagnosis requests. Centralizes turn prompt selection and appends the Prometheus query nudge only to read-only turns with confirmed connectivity.

internal/ai/diagnoser.go

runs.goProbe metrics before read-only investigation turns +21/-0

Probe metrics before read-only investigation turns

• Adds an injectable Prometheus availability probe with a two-second timeout. Probe results are attached only to ordinary investigation requests.

internal/ai/runs.go

server.goWire Prometheus availability into AI runs +7/-0

Wire Prometheus availability into AI runs

• Connects the RunManager probe to the internal Prometheus availability service and maps its state into the AI request contract.

internal/server/server.go

AreaChart.tsxExtend area charts with change annotations +152/-19

Extend area charts with change annotations

• Adds explicit time domains, responsive stacked annotation labels, marker tooltips, and negative-value axes with zero baselines. It also uses unambiguous shared series labels.

packages/k8s-ui/src/components/charts/AreaChart.tsx

SeriesLegend.tsxUse collision-safe series labels in legends +2/-2

Use collision-safe series labels in legends

• Replaces local pod and instance naming with the shared display-label resolver so aggregated or duplicate series remain distinguishable.

packages/k8s-ui/src/components/charts/SeriesLegend.tsx

annotations.tsAdd deterministic chart annotation layout +59/-0

Add deterministic chart annotation layout

• Introduces annotation truncation and row-placement logic. Overlapping labels use up to three rows while markers remain visible when labels cannot fit.

packages/k8s-ui/src/components/charts/annotations.ts

colors.tsGenerate distinct display names for metric series +23/-0

Generate distinct display names for metric series

• Adds shared series naming that prefers pod, instance, or node labels when unique and otherwise exposes all labels.

packages/k8s-ui/src/components/charts/colors.ts

format.tsFormat negative and duration metric values +8/-0

Format negative and duration metric values

• Adds sign-preserving recursive formatting and human-readable microsecond through hour formatting for metrics measured in seconds.

packages/k8s-ui/src/components/charts/format.ts

index.tsExport annotation and series-label APIs +2/-1

Export annotation and series-label APIs

• Publishes ChartAnnotation and seriesDisplayLabels through the charts package entry point.

packages/k8s-ui/src/components/charts/index.ts

types.tsDefine change annotation data for charts +12/-0

Define change annotation data for charts

• Adds a typed timestamped chart annotation that explicitly records changes without asserting causation.

packages/k8s-ui/src/components/charts/types.ts

InvestigationEvidencePane.tsxRender Prometheus evidence as Findings cards +235/-5

Render Prometheus evidence as Findings cards

• Adds full-width metrics cards with charts, value tables, sparse-series handling, query metadata, and change annotations. Uncited broader metrics are withheld and counted while cited results can enter primary evidence.

web/src/components/diagnose/InvestigationEvidencePane.tsx

investigationEvidence.tsAdapt Prometheus results into typed evidence +301/-2

Adapt Prometheus results into typed evidence

• Introduces the metrics evidence model and query_prometheus adapter. It validates series and selectors, derives conservative relevance and units, and converts oversized results into limitations.

web/src/components/diagnose/investigationEvidence.ts

investigationMetrics.tsDerive metric presentation and trusted change markers +188/-0

Derive metric presentation and trusted change markers

• Adds conservative metric-unit inference and range-domain parsing. Correlates charts only with same-turn changes to the target or explicitly established related resources.

web/src/components/diagnose/investigationMetrics.ts

investigationSourceFocus.tsExpose PromQL in evidence source focus +2/-0

Expose PromQL in evidence source focus

• Uses the original Prometheus query as the source excerpt for metrics evidence navigation.

web/src/components/diagnose/investigationSourceFocus.ts

Tests (6) +1168 / -4
metrics_nudge_test.goTest metrics-aware prompts and turn probing +149/-0

Test metrics-aware prompts and turn probing

• Verifies connected prompts, silent failure behavior, and exclusion from apply or explanation turns. Also confirms RunManager probes are deadline-bound and optional.

internal/ai/metrics_nudge_test.go

AreaChart.test.tsxTest chart annotations, domains, and negative values +108/-0

Test chart annotations, domains, and negative values

• Covers annotation filtering and stacking, explicit time domains, negative axes, null gaps, sparse samples, and empty input.

packages/k8s-ui/src/components/charts/AreaChart.test.tsx

colors.test.tsTest series display label selection +27/-0

Test series display label selection

• Covers preferred resource labels, duplicate-label disambiguation, full label rendering, and positional fallbacks.

packages/k8s-ui/src/components/charts/colors.test.ts

InvestigationEvidencePane.test.tsxTest Findings metric cards and visibility rules +194/-2

Test Findings metric cards and visibility rules

• Exercises range charts, instant and sparse values, broader-result withholding, citation promotion, labels, and related change markers.

web/src/components/diagnose/InvestigationEvidencePane.test.tsx

investigationEvidence.test.tsTest Prometheus evidence adaptation and scope +411/-2

Test Prometheus evidence adaptation and scope

• Validates range, instant, empty, truncated, malformed, and revision states. Extensively tests conservative scope classification from producer-supplied selectors.

web/src/components/diagnose/investigationEvidence.test.ts

investigationMetrics.test.tsTest metric units, domains, and change correlation +279/-0

Test metric units, domains, and change correlation

• Covers conservative unit inference, valid range domains, same-turn producer-relative markers, deduplication, and exclusion of unrelated changes.

web/src/components/diagnose/investigationMetrics.test.ts

@qodo-code-review

qodo-code-review Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Backend credentials reach the AI provider ✓ Resolved 🐞 Bug ⛨ Security
Description
The server copies AvailabilityState.Address directly into MetricsAvailability, which
metricsNudge embeds in the model-visible diagnosis prompt. A manually configured Prometheus URL
with userinfo is returned without redaction, so its credentials are sent whenever a read-only
investigation successfully probes that backend.
Code

internal/server/server.go[R264-267]

+				return ai.MetricsAvailability{
+					Connected: state.State == prometheuspkg.AvailabilityConnected,
+					Address:   state.Address,
+				}
Relevance

●●● Strong

Security findings involving untrusted values reaching logs or downstream systems are consistently
accepted.

PR-#524
PR-#1210

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The newly added wiring passes the availability address into the request, and the added prompt code
formats that address for the agent. The availability implementation preserves a manually configured
URL verbatim apart from trailing-slash removal.

internal/server/server.go[262-268]
internal/ai/runs.go[705-709]
internal/ai/diagnoser.go[107-115]
internal/prometheus/availability.go[73-79]
internal/prometheus/client.go[165-173]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Prevent configured Prometheus URL credentials from entering model-visible prompts.

## Issue Context
Availability returns the raw manually configured URL after trimming only its trailing slash. The new server wiring carries that address into `MetricsAvailability`, and the diagnosis prompt interpolates it verbatim when the probe reports a connection.

## Fix Focus Areas
- internal/server/server.go[262-268]
- internal/ai/diagnoser.go[107-115]
- internal/prometheus/availability.go[73-79]
- internal/prometheus/client.go[165-173]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Sibling metrics appear as target data ✓ Resolved 🐞 Bug ≡ Correctness
Description
selectorNamesTarget accepts any exact pod beginning with the workload name, the same prefix regex
even for a Pod target, and generic workload matchers without ownership or kind proof. When a sibling
pod or different workload shares that prefix or name, metricsScope returns target, so its
uncited chart is shown in the workload collection and can receive the target's change markers.
Code

web/src/components/diagnose/investigationEvidence.ts[R3323-3327]

+    if (op === "=~") return regexNamesExactly(value, target.name, "-.*");
+    if (op === "=") {
+      return target.kind.toLowerCase() === "pod"
+        ? value === target.name
+        : value.startsWith(`${target.name}-`);
Relevance

●● Moderate

Potential selector-attribution false positives are substantive, but no closely matching precedent
establishes team disposition.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The classifier treats <target>-... as sufficient ownership for exact pod selectors, applies
<target>-.* to every target kind, and places no kind restriction on generic workload labels.
metricsScope promotes a selector when any matcher passes that check, while the pane withholds only
results classified as broader; target-classified results therefore become visible without a
citation.

web/src/components/diagnose/investigationEvidence.ts[3294-3300]
web/src/components/diagnose/investigationEvidence.ts[3317-3336]
web/src/components/diagnose/investigationEvidence.ts[3345-3365]
web/src/components/diagnose/InvestigationEvidencePane.tsx[151-193]
web/src/components/diagnose/investigationMetrics.ts[143-154]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Metrics selectors are classified as target-scoped using pod-name prefixes or generic workload names, even though those names do not prove that the selected series belong to the diagnosed resource. This can present a sibling resource's metrics as target evidence.

## Issue Context
An exact pod matcher for a workload currently needs only the `<target>-` prefix, and Pod targets also accept `<pod>-.*`. The generic `workload` label has no associated kind requirement. Keep these cases producer-related unless the producer supplies exact resolved pod ownership; Pod targets should require an exact pod match, and workload label matching should verify the target kind.

## Fix Focus Areas
- web/src/components/diagnose/investigationEvidence.ts[3294-3300]
- web/src/components/diagnose/investigationEvidence.ts[3317-3336]
- web/src/components/diagnose/investigationEvidence.ts[3345-3365]
- web/src/components/diagnose/investigationEvidence.test.ts[3274-3427]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. A redundant comment adds code noise ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
AreaChart adds the comment Zero line when the axis extends below zero immediately before a
zeroLineY !== null condition that renders a <line>. When negative-axis rendering changes, this
duplicate prose must be reviewed alongside JSX that already states the same behavior.
Code

packages/k8s-ui/src/components/charts/AreaChart.tsx[287]

+        {/* Zero line when the axis extends below zero */}
Relevance

●●● Strong

This is a trivial maintainability nit matching the repository’s stated comment rule.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 3036542 prohibits comments that only translate immediately following code into
prose; the cited comment describes the visible condition and <line> directly beneath it.

Rule 3036542: Avoid explanatory comments that restate obvious code behavior
packages/k8s-ui/src/components/charts/AreaChart.tsx[287-298]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The zero-line comment merely restates the immediately following condition and JSX without explaining intent or a hidden constraint.

## Issue Context
The rendering code is self-describing, so removing the comment preserves understanding while avoiding duplicate maintenance.

## Fix Focus Areas
- packages/k8s-ui/src/components/charts/AreaChart.tsx[287-287]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Operators cannot distinguish split series ✓ Resolved 🐞 Bug ≡ Correctness
Description
MetricsBody partitions the result into charted and sparse arrays before each renderer
independently calls seriesDisplayLabels. If two series share a pod but differ by container and one
has only one sample, each subset sees a unique pod name and drops the container labels that
distinguish the two measurements.
Code

web/src/components/diagnose/InvestigationEvidencePane.tsx[R2113-2114]

+  const charted = data.series.filter((item) => finiteSamples(item).length >= 2);
+  const sparse = data.series.filter((item) => finiteSamples(item).length < 2);
Relevance

●●● Strong

Splitting duplicate series before shared label derivation creates a concrete operator-facing
correctness regression.

PR-#753

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The labeling helper only expands to all labels when duplicates coexist in the array it receives. The
new metrics body separates charted and sparse series first, passes those subsets to different
components, and both components derive labels from their own subset, allowing formerly duplicate pod
labels to appear identical.

packages/k8s-ui/src/components/charts/colors.ts[31-46]
packages/k8s-ui/src/components/charts/AreaChart.tsx[202-207]
web/src/components/diagnose/InvestigationEvidencePane.tsx[2031-2044]
web/src/components/diagnose/InvestigationEvidencePane.tsx[2111-2164]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Series display labels are recalculated independently after the complete Prometheus result is split into charted and sparse subsets. Labels that were duplicates in the original result can consequently lose their distinguishing dimensions.

## Issue Context
Compute display labels from the complete series collection before partitioning, then preserve those labels in both the chart tooltip or legend and the sparse value table. Add coverage for two series sharing a pod but differing by container, with one charted and one sparse.

## Fix Focus Areas
- web/src/components/diagnose/InvestigationEvidencePane.tsx[2022-2063]
- web/src/components/diagnose/InvestigationEvidencePane.tsx[2111-2164]
- packages/k8s-ui/src/components/charts/AreaChart.tsx[202-207]
- packages/k8s-ui/src/components/charts/colors.ts[31-46]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Null-only series claim one sample ✓ Resolved 🐞 Bug ≡ Correctness
Description
MetricsBody puts every series with fewer than two finite values into sparse, but its
accompanying text states that each such series has a single sample. Prometheus deliberately
serializes non-finite results as null gaps, so a series containing only null values is listed with a
blank time and “no value” while the card incorrectly claims that one measurement exists.
Code

web/src/components/diagnose/InvestigationEvidencePane.tsx[R2113-2114]

+  const charted = data.series.filter((item) => finiteSamples(item).length >= 2);
+  const sparse = data.series.filter((item) => finiteSamples(item).length < 2);
Relevance

●●● Strong

The condition includes zero-sample series while the rendered text unconditionally claims a single
sample.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The Prometheus transport explicitly converts NaN and infinities to null. The new UI defines finite
samples by numeric values, classifies both zero and one finite sample as sparse, and then
unconditionally describes every sparse series as having a single sample.

pkg/prom/types.go[50-69]
web/src/components/diagnose/InvestigationEvidencePane.tsx[2018-2020]
web/src/components/diagnose/InvestigationEvidencePane.tsx[2111-2114]
web/src/components/diagnose/InvestigationEvidencePane.tsx[2153-2164]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The sparse-series path combines zero-finite-sample and one-finite-sample series but describes all of them as having one sample. Null-only Prometheus series therefore receive a false measurement count.

## Issue Context
Prometheus values such as NaN and infinity are intentionally serialized as null gaps. Partition sparse series by finite sample count and give zero-value series an explicit no-finite-samples message, or omit them with an honest limitation.

## Fix Focus Areas
- web/src/components/diagnose/InvestigationEvidencePane.tsx[2018-2020]
- web/src/components/diagnose/InvestigationEvidencePane.tsx[2111-2165]
- web/src/components/diagnose/InvestigationEvidencePane.test.tsx[2262-2311]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (2)
6. A chart dot uses an unapproved color ✗ Dismissed 📘 Rule violation ⚙ Maintainability
Description
AreaChart renders the annotation tooltip dot with bg-accent instead of one of the approved theme
background utilities. Every tooltip shown near a change marker reaches this class, leaving the new
element outside the prescribed background-token set.
Code

packages/k8s-ui/src/components/charts/AreaChart.tsx[512]

+                <span className="w-2 h-2 rounded-full shrink-0 bg-accent" />
Relevance

●●● Strong

Explicit theme-token compliance findings are likely accepted, especially for newly introduced UI
elements.

PR-#1585

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 3036653 permits only the listed theme background utilities, while the added tooltip
marker uses bg-accent.

Rule 3036653: Use theme background tokens instead of hardcoded utility color classes
packages/k8s-ui/src/components/charts/AreaChart.tsx[512-512]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The annotation tooltip dot uses `bg-accent`, but modified frontend elements must use an approved theme background utility.

## Issue Context
Preserve the marker's intended appearance while routing its background through the permitted token set.

## Fix Focus Areas
- packages/k8s-ui/src/components/charts/AreaChart.tsx[512-512]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Target charts lose their change markers ✓ Resolved 🐞 Bug ≡ Correctness
Description
metricsScope accepts a namespace matcher only when its operator is =, even though an exact
=~"shop" matcher selects only the target namespace. A range query with that valid matcher and a
target pod or workload selector is classified as broader, so it receives no subject and cannot
obtain change markers.
Code

web/src/components/diagnose/investigationEvidence.ts[R3355-3360]

+    const inNamespace = selector.matchers.some(
+      (matcher) =>
+        matcher.label === "namespace" &&
+        matcher.op === "=" &&
+        matcher.value === target.namespace,
+    );
Relevance

●●● Strong

Exact anchored regex namespace matchers are valid and should preserve target attribution for marker
generation.

PR-#1585

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The producer preserves PromQL matcher operators in its selector inventory, while the new classifier
accepts only exact equality for the namespace. Prometheus documents that regex label matchers are
fully anchored, and target attribution is the prerequisite for marker generation.

internal/mcp/tools_prometheus.go[100-105]
internal/mcp/tools_prometheus.go[146-148]
web/src/components/diagnose/investigationEvidence.ts[3354-3365]
web/src/components/diagnose/investigationEvidence.ts[3468-3476]
web/src/components/diagnose/investigationMetrics.ts[147-154]
🌐 Prometheus states that regex label matches are fully anchored, so env=~&quot;foo&quot; is treated as env=~&quot;^foo$&quot;.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Treat exact, safely escaped namespace regex matchers as target namespace scope.

## Issue Context
Prometheus fully anchors regex label matchers, so `namespace=~"shop"` is equivalent to an exact namespace match. The current scope classifier marks this valid target-scoped query as broader, which prevents assigning `subject` and consequently suppresses chart change markers.

## Fix Focus Areas
- web/src/components/diagnose/investigationEvidence.ts[3354-3365]
- web/src/components/diagnose/investigationMetrics.ts[147-154]
- web/src/components/diagnose/investigationEvidence.test.ts[3274-3427]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 41 rules
✅ Web pages:
  +2 more
✅ Cross-repo context — repo relationships
  Explored: repo: skyhook-dev/radar-hub-web (sha: e5bc6761)
Review mode: 🧠 Deep: This is a broad, behavior-heavy cross-cutting change spanning Prometheus query scoping, authorization and bounded backend calls, evidence classification, chart rendering, and change-marker logic, with many independent edit sites and substantial opportunity for subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can commit Qodo's fix in one click with committable suggestions (GitHub & GitLab)

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread packages/k8s-ui/src/components/charts/AreaChart.tsx
Comment thread packages/k8s-ui/src/components/charts/AreaChart.tsx Outdated
Comment thread web/src/components/diagnose/investigationEvidence.ts Outdated
Comment thread web/src/components/diagnose/InvestigationEvidencePane.tsx Outdated
Comment thread web/src/components/diagnose/InvestigationEvidencePane.tsx Outdated
Comment thread web/src/components/diagnose/investigationEvidence.ts
Comment thread internal/server/server.go

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/diagnose/investigationEvidence.ts Outdated
Comment thread internal/ai/runs.go
@nadaverell
nadaverell changed the base branch from chore/metrics-seams to main September 8, 2026 07:11
@nadaverell

Copy link
Copy Markdown
Contributor Author

Retargeted to main so build and test CI run (workflows only trigger for PRs against main). This branch stacks on #1678 and should merge after it; the diff shrinks to this PR's own changes once #1678 lands.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/diagnose/investigationMetrics.ts

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/diagnose/investigationMetrics.ts Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/components/charts/axis.ts

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/diagnose/investigationEvidence.ts
@nadaverell

Copy link
Copy Markdown
Contributor Author

Review round (Cursor Bugbot, head 9ea7553):

# Source Issue Verdict Reasoning Action
1 Bugbot Blank titles for unnamed metric selectors Fix A bare matcher block yields one empty metric name and the title became "" Empty names are dropped before deciding the title; test added

Findings gains a metrics kind: query_prometheus results become a chart card (range), a value table (instant) or a truncation limitation. Scope is decided from the producer's selector inventory only: target when every selector carries the target namespace and names the target's pods or workload, producer-related for namespace-only selectors, broader for anything else. Uncited broader charts are withheld and counted.

AreaChart accepts annotations and an explicit domain, handles negative minima, and stacks marker labels; markers come only from same-turn change observations on the subject or its producer-established relatives.

The RunManager probes Prometheus before read-only investigation turns and the prompt tells the agent it is connected, so it queries instead of guessing.
A Pod target is named only by its exact pod name, an anchored namespace regex counts as the target namespace, and credentials in a configured Prometheus URL never reach the prompt. The post-apply verification turn no longer probes for metrics. Series names are derived once from the whole result so a chart subset and its sparse table stay distinguishable, and a series with only null samples is reported as returning no finite values instead of a single sample.
An aggregation keeps the unit its metrics state, a rate turns bytes into bytes per second, and a division or disagreeing metrics leave the axis unitless. Unitless axes scale with k, M and G instead of a runaway k suffix. Prettier pass on the files this branch touched.
count, count_over_time, absent, changes, resets and timestamp return a number that is not the metric's quantity, so a count of byte series must not render as bytes.
The grouping clause sits between the aggregation name and its parenthesis, so the counting-function check must accept that form.
Opt-in layout='auto' switches to a compact layout below the label-hiding width: two Y ticks, first and last X ticks, larger text and a taller plot, so a narrow Findings pane no longer shows a strip with unreadable ticks. Existing callers keep the full layout. A count axis ends on a whole 1/2/5 step so its ticks are integers; an all-zero count series shows 0 and 1.
Compact text is larger, so a label like 177.7 MiB overran the fixed margin and was clipped by the SVG.
Helm history rows and older stores omit apiVersion, so a Helm upgrade of the workload never drew a marker. Without an apiVersion a change now matches on kind, namespace and name against the subject and its producer-established relatives; with one, the exact group match stands.
A query over one metric is titled with that metric and carries
Prometheus in the subtitle; the generic title stays only when the
selectors name several metrics or are unknown. The title is Radar's
reading of the query, never the agent's.
The metrics nudge names the Prometheus address so the agent knows where the
backend is, and that prompt is model-visible and leaves the machine. The
redaction removed userinfo only. A Prometheus behind an auth proxy is commonly
configured with the credential in the query string instead — `?token=…` — so
the address was reduced to scheme, host and path, and the fragment goes with it
for the same reason. The test covered `user:password@host` alone, which is what
hid the rest.

Found by Codex in the merge sign-off review.
@nadaverell
nadaverell force-pushed the feat/findings-chart-card branch from 01fbd78 to bd0d5c1 Compare September 9, 2026 18:45

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bd0d5c1. Configure here.

Comment thread web/src/components/diagnose/InvestigationEvidencePane.tsx Outdated
The pane heads two lines with these counts — "results about other resources"
and "broader metric results" — and a withheld chart was incrementing both, so
one withheld result was announced twice and the two numbers stopped summing to
what was actually held back.

The counters are now disjoint: a metrics group goes to the metrics count and
everything else to the broader one, which is what each line already claims.

Introduced resolving this branch's conflict with #1681, where the two counters
first met. Found by Cursor Bugbot.
@nadaverell

Copy link
Copy Markdown
Contributor Author

Merge sign-off review on the rebased branch (Claude + Codex). One fix, one deferral, no other blockers.

Fixed — Withheld metrics counted twice (54842baf). The pane heads two lines with hiddenBroader and hiddenMetrics, and a withheld chart incremented both, so one held-back result was announced twice and the counts stopped summing. Introduced while resolving this branch's conflict with #1681, where the two counters first met. Now disjoint, with a case pinning it.

Deferred, and the reason is the merge order — Codex's only blocker is that pod-name prefixes do not establish ownership: investigationEvidence.ts:4616 matches value.startsWith(name + "-"), so api-.* also matches api-worker's pods and another workload's metrics can be labelled target evidence and take the target's change markers. Real here. It is retired in #1694, which replaces prefix matching with controller ownership throughout — that branch has no occurrence of the pattern, and its comments state that a prefix "a sibling such as api-worker would also satisfy" is downgraded to producer-related rather than target. No release happens between these merges.

Rebase check. The conflict region needed hand resolution over #1681: 12 hunks were additions on both sides, 7 were real overlaps. Two would have gone wrong silently — #1681 refactored the focused-kinds list into FOCUSED_EVIDENCE_TYPES, so "metrics" had to be added at the constant rather than in the hunk (otherwise a cited chart quietly stops being promotable), and the two withheld-counters each needed keeping. Both were re-checked after the remaining ten commits replayed.

Verification: tsc, 989 web tests, 3,603 k8s-ui tests, both Go modules, go build. All nine CI checks green. Visual testing not repeated on this branch — the chart card was captured during its own review, and the rebase changed partitioning and counters rather than rendering.

@nadaverell
nadaverell merged commit 3ec1d9a into main Sep 9, 2026
9 checks passed
nadaverell added a commit that referenced this pull request Sep 9, 2026
## Summary

`diagnose` returns a `metrics` field for workload targets — CPU, memory
and restarts over the exact pods the bundle resolved (#1679, on `main`).
Findings did not render it. This PR projects each category as a chart
card in the shape the Prometheus chart cards already use (#1684), so the
chart, the agent and the ledger hold the same numbers over the same
window and the same pods, and the changes Radar recorded for that
workload in the same turn are marked on it.

## What changed

**`addDiagnoseMetrics`** (`investigationEvidence.ts`) — one `metrics`
observation per category with `origin: "diagnose"`, `mode: "range"`, the
producer's own `query` and `unit`, a category label (`CPU usage`,
`Memory working set`, `Restarts`), the diagnosed resource as `subject`,
and `pods` / `partial` from the field.

- **Relevance follows the bundle** (`relevanceForResource`), never an
unconditional target — a neighbour's vitals stay `broader` with the
neighbour as subject.
- **Identity is `metrics:diagnose:<subject>:<category>`**, keyed by the
diagnosed resource rather than its display scope, so a re-diagnose
revises the same three cards instead of stacking new ones.
- **Summary line** reads `N pods · 60m window · 1m2s step`; `first 50 of
55 pods … · partial pod set` when the producer capped the set; `No
samples in the window` for an empty series.
- **Failure is distinguished from absence.** `metrics.error` becomes a
"Workload metrics" limitation while the categories that did answer still
chart. An *absent* field emits nothing at all — Prometheus not
configured, or the read not permitted, is not a collection failure and
must not be reported as one.
- **Validation enforces the documented contract** before anything
renders: a known category (own-property test, so inherited keys like
`__proto__` cannot pass), a non-empty unit, non-negative integer counts,
and a window whose end is after its start. Anything else is an
invalid-payload limitation, never a card.

**`metricsChangeMarkers`** (`investigationMetrics.ts`) — no markers on a
`broader` chart. The turn's non-broader relatives and change
observations all belong to the target, so drawing them on a neighbour's
chart would attribute the target's changes to it. That path is reachable
only now that a diagnose chart can carry a neighbour as its subject.

**`partitionInvestigationEvidence`** (`InvestigationEvidencePane.tsx`) —
inside "More evidence about this workload", cards that carry facts sort
ahead of receipt-kind cards ("No classified workload issues", "No
matching warning events"), which keep their order among themselves. A
healthy workload no longer opens on four empty tiles before its vitals.

Placement: these observations sit in "More evidence about this workload"
rather than the main collection.

## Testing

- `make tsc` ✓ · `web` 87 files / 1037 tests ✓ · `git diff --check`
clean. No Go delta.
- Adapter tests: three series → three cards with subject, unit, label,
window and pod count; a re-diagnose merging into the same cards; a
producer error becoming a limitation while the surviving series still
chart; an unreachable Prometheus as a limitation with no card; an absent
field emitting nothing; a partial pod set noted in the summary; a
neighbour's diagnose landing as `broader` with the neighbour as subject;
six malformed envelopes plus an out-of-contract category and a missing
unit rejected.
- Pane tests: a diagnose-origin chart in the workload collection with
the same-turn Deployment change marked and the resource link present;
the neighbour guard on the marker function; the limitation row for a
producer error; and the receipt ordering — vitals first, receipts last
and contiguous.
- Visual test (light, dark, narrow) against a saved investigation with
the `metrics` field present: three cards titled per category showing `1
pod · 60m window · 1m2s step`, axis label and unit, three recorded
changes marked with "3 changes recorded in this window are marked on the
chart", the earlier diagnose folded under "Previous observations · 1",
and the "Open current Deployment" link.

## Scope and tradeoffs

- Restarts render with unit `count`, which the shared formatter treats
as unitless (values like `1.00`). A `count` case in `formatMetricValue`
is a `k8s-ui` follow-up.
- Marker labels for several changes on the same resource within minutes
overlap in the chart — that is the marker rendering shared with #1684.
- Recorded changes carrying no `apiVersion` (synthesized Helm rows,
older stores) never match a chart subject and so are not marked. That
rule is #1684's; it is restated here because a chart of a workload's own
vitals is where its absence is most visible.

## Note on history

This is #1690 re-based onto `main`. It was approved and marked merged,
but its base was the integration branch `feat/findings-next-layer`, so
its content never reached `main`. The commits are the same three; the
Prettier-only commit was dropped and the formatting applied directly.
Rebase conflicts were confined to `InvestigationEvidencePane` and its
test — the counters #1684 made disjoint (`hiddenBroader` /
`hiddenMetrics`) are preserved alongside the new receipt-ordering sort.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes evidence projection and chart annotation rules for diagnose
output; validation is strict but malformed or partial metrics could
affect what users see in investigations.
> 
> **Overview**
> Findings now surfaces the **`metrics` payload from `diagnose`** as
workload chart cards (CPU, memory, restarts), aligned with existing
Prometheus-style metrics evidence instead of ignoring that field.
> 
> **`addDiagnoseMetrics`** turns each valid category into a range chart
with `origin: "diagnose"`, the producer query/unit, the diagnosed
resource as **subject**, and relevance that follows the bundle
(neighbour diagnoses stay **broader**). Re-diagnoses merge on stable
identity; **`metrics.error`** becomes a "Workload metrics" limitation
while partial series still render; a missing field stays silent;
malformed payloads are rejected with limitations, not cards.
> 
> **`metricsChangeMarkers`** skips **broader** charts so same-turn
target changes are not drawn on a neighbour’s vitals.
**`partitionInvestigationEvidence`** sorts the workload section so fact
cards (including vitals) appear before contiguous **receipt** tiles.
> 
> Coverage is mostly new adapter and pane tests for projection, merging,
errors, ordering, change markers, and neighbour guardrails.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3bf9c12. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@nadaverell
nadaverell deleted the feat/findings-chart-card branch September 17, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant