Skip to content

Commit e0f6d8d

Browse files
authored
Findings: a card for every tool result, and citations that reach them (#1802)
## Summary Every tool result the agent can cite now has a card in the Findings pane, and the card carries what the citation is about. Before this, a Helm release rendered without its hooks, history or values; a permissions check without its rules; and `top_resources`, `get_cluster_audit`, `get_cluster_upgrade_readiness`, `list_helm_releases`, `list_packages`, `search` and `discover_metrics` had no card at all, so a claim about them sat under "Notes without a card". Live runs also showed the agent citing the new kinds with the one word the prompt gives it, `resource`, and the resolver rejecting it; those claims never reached their cards either. ## What changed **Cards for the remaining tools** (`investigationEvidence/adapters/`) - `ranking`: `top_resources` rows with cpu and memory against limits, sorted as the tool sorted them, the target's rows marked; `metricsAvailable: false` becomes a limit rather than a card. - `posture`: `get_cluster_audit` and `get_cluster_upgrade_readiness` keep only findings about the target or objects it manages (`managedBy`), rendered as configuration checks with severity, check and remediation, never as an outage; a scan with nothing about the target files a receipt carrying the verdict. - Inventory reuse: `list_helm_releases` (release, status, chart, health issue), `list_packages` (version, sources), `search` (the matched field and snippet on the row). A listing's identity now includes the tool, so a cluster-wide Helm list and a cluster-wide package list are two cards, not one with a revision. - `discover_metrics`: a receipt naming what was searched and how much came back in the last hour; truncation becomes a limit. - Helm card: hooks (kind, name, event), revision history (newest first, ten), values set by the user. `get_helm_release` now returns `history` beside the detail it already loaded. Permissions card: effective rules under the bindings, up to the tool's cap. **Placement** (`investigationCase.ts`, prompt in `internal/ai/diagnoser.go`, `docs/mcp.md`) - `observation: "resource"` names every resource-shaped kind (resource, listing, ranking, posture, Helm release, permissions, neighborhood, dns, network). The stream-shaped kinds keep their own word so one word still picks one observation of a diagnose bundle. - A call that yielded a single observation is not held to the word at all; what the subject names still has to match. This is what places a receipt the agent called `metrics` or `resource`. - A listing binds when the subject names an entry it holds even if the kinds differ: a package row carries Radar's own kind and the agent names the object that declares it (a Flux HelmRelease in flux-system), so packages are matched by name. **The excerpt slot** (`investigationEvidence/excerpt.ts`, `StoryExcerpt.tsx`) - What a placed story card shows inline (log lines, the named inventory rows, a ranking's target rows, posture findings about the target, the metrics chart on cause and symptom cards) is one decision in one place. Behaviour-preserving for the kinds that had inline content. - On Captured results cards, the rows a listing's citations name lead the list with a faint highlight, so the entry a claim is about is in view without scrolling a long inventory. **Running label** is a pure function with tests (`investigationRunningLabel`). ## Testing - `make tsc`, eslint on the diagnose tree, `npm test` (vitest over `src`, 1365 tests), `go test ./internal/ai ./internal/mcp`, `go build ./...`, web build. - New tests: adapters for every new card, the excerpt slot, resolver placement for resource-shaped citations and the sole-observation rule, listing identity per tool, package rows named by the declaring kind, cited rows leading the inventory body. - Visual test: ran. Before/after captures of every changed card type on three saved runs (podinfo and crashloop on the kind gitops demo, a live crash loop on the GKE nonprod cluster for ranking, upgrade readiness, Helm listing and metric discovery), baseline built from `refactor/findings-structure`. Review packet linked from the PR conversation. - Not verified live: posture findings that name the workload, and a ranking with the target's row marked (neither cluster produced one); both unit-tested. ## Follow-ups - A cause claim on a `get_pod_logs` read that names the container stays unplaced when the read was made without a container argument (identity container undefined). Pre-existing. - The focus contract (PR 3 of the coverage plan) is untouched; it needs the mockup approval. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes investigation evidence projection, story placement, and agent verdict contracts; incorrect binding could misplace cards but does not touch auth or cluster mutation paths. > > **Overview** > **Findings now turns almost every citable MCP read into a real card**, with citations that can reach them via `observation: "resource"` (listings, rankings, posture, Helm, permissions, neighborhood, etc.) instead of leaving those claims under “Notes without a card.” > > New **evidence adapters** add **ranking** (`top_resources`), **posture** (cluster audit / upgrade readiness filtered to the workload), **inventory-style** cards for Helm releases, packages, and search hits, plus **metric-discovery receipts**; **Helm** and **permissions** cards gain hooks, revision history (newest ten), user values, and effective RBAC rules. **`get_helm_release`** now includes a capped **`history`** in the default MCP response (full history still via `include=history`). > > **Citation placement** in `investigationCase` treats resource-shaped observations broadly, skips the observation word for sole **receipt** results, and improves **package/listing** subject matching (declaring-object kinds, namespace rules). **Story placement** moves inline slices (log lines, cited inventory rows, target ranking/posture rows, cause/symptom charts) into **`storyExcerpt`** / **`StoryExcerpt`**, with cited rows leading long inventory lists in Captured results. > > **Docs** (`docs/mcp.md`) and the **diagnoser prompt** (`verdictContract` / `storyGuidance`) are updated so agents know which tools are placeable and how to cite them. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 67da046. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8fba01a commit e0f6d8d

35 files changed

Lines changed: 3137 additions & 179 deletions

‎docs/mcp.md‎

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

‎internal/ai/diagnoser.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -876,15 +876,15 @@ const verdictContract = "VERDICT BLOCK: " +
876876
"inconclusive=true when you could not determine the cause: root_cause empty, the blocking question first in unresolved, verify or investigate steps offered. healthy and inconclusive are mutually exclusive. " +
877877
"root_cause: the paragraph under the headline, two or three plain sentences for a reader who will not open the analysis: what failed, the mechanism, and the one result that shows it, with since-when if a result gives it. No [[radar:...]] markers here or in summary. GitHub-flavored markdown; backticks only for an identifier the reader needs exactly, not for every name. Claim nothing the ledger does not cover; empty when healthy or inconclusive. " +
878878
"root_cause_evidence_refs: at most 3 refs of the checks that establish WHY, copied exactly from their [[radar:evidence-ref=ev_...]] markers; empty when root_cause is empty. " +
879-
"evidence: at most 8 results from this investigation that returned data (a call that errored or was denied cannot be cited; say in the story what it would have shown), each with its ref copied exactly, a role (cause: establishes why; symptom: what it looks like; context: checked and worth seeing; benign: looks adverse but is not a live problem; demoted: related but less relevant; rules_out: this result contradicts a hypothesis), an optional one-sentence claim (required for benign, demoted and rules_out, 200 characters max), and a gap: one clause naming what this result does not cover that a reader might assume it does (the other restarts, the other pods, earlier history), written to stand on its own under the card, a fragment rather than a sentence: 'live spec only, not the Git manifest', never 'Shows the live spec, not the manifest'; omitted when nothing is assumed. A gap is a limit of the evidence, never a licence to claim past it. Refs may repeat with different subjects. When a result covers several resources, pods, containers or log streams, subject names the one you mean (kind and name required; group is the API group and is empty for core kinds such as Pod, ConfigMap, Secret, Service, Event and Node; for a listing, name the entry you mean, or give kind and namespace with no name when the point is what the listing does not contain; namespace, container, stream and observation as needed, observation being the evidence kind, exactly one of: resource, logs, events, changes, metrics, metrics:cpu / metrics:memory / metrics:restarts for a diagnose bundle's charts, alerts with name as the rule's name, or issue; any other word leaves the item unplaced). The diagnose bundle is never about one thing: every citation of it needs a subject. " +
879+
"evidence: at most 8 results from this investigation that returned data (a call that errored or was denied cannot be cited; say in the story what it would have shown), each with its ref copied exactly, a role (cause: establishes why; symptom: what it looks like; context: checked and worth seeing; benign: looks adverse but is not a live problem; demoted: related but less relevant; rules_out: this result contradicts a hypothesis), an optional one-sentence claim (required for benign, demoted and rules_out, 200 characters max), and a gap: one clause naming what this result does not cover that a reader might assume it does (the other restarts, the other pods, earlier history), written to stand on its own under the card, a fragment rather than a sentence: 'live spec only, not the Git manifest', never 'Shows the live spec, not the manifest'; omitted when nothing is assumed. A gap is a limit of the evidence, never a licence to claim past it. Refs may repeat with different subjects. When a result covers several resources, pods, containers or log streams, subject names the one you mean (kind and name required; group is the API group and is empty for core kinds such as Pod, ConfigMap, Secret, Service, Event and Node; for a listing, name the entry you mean, or give kind and namespace with no name when the point is what the listing does not contain; namespace, container, stream and observation as needed, observation being the evidence kind, exactly one of: resource (anything a call returned about a resource: a single resource, a listing, a ranking, a posture card, a Helm release, a permissions check, a neighborhood), logs, events, changes, metrics, metrics:cpu / metrics:memory / metrics:restarts for a diagnose bundle's charts, alerts with name as the rule's name, or issue; any other word leaves the item unplaced). The diagnose bundle is never about one thing: every citation of it needs a subject. " +
880880
"ruled_out: at most 5 hypotheses, each pointing at the 0-based evidence item whose result contradicts it, meaning the hypothesis predicts something that result shows to be false (a container that ran does not rule out the wrong image; an empty window does not rule out an earlier change); a hypothesis nothing contradicts is untested and belongs in unresolved, not here. " +
881881
"steps: up to 6 actions someone can take, empty on a healthy verdict, each self-contained and copy-pasteable in GitHub-flavored markdown with inline code for commands (a fenced bash block only for real multi-line scripts, fence lines on their own). kind: mitigate changes the cluster to restore service, verify settles an unresolved item, investigate gathers more; these are purposes, not an order. precondition: the condition under which the step is right, empty when there is none. A warning or a thing not to do belongs in the story, not in steps. " +
882882
"recommended_index: the one mitigate step Apply should perform, 1-based, safe, targeted, reversible, with no precondition and depending on nothing you inferred or left untested (a cluster type read off a node name is an inference; an intent you could not read is untested); when a step depends on such a thing it carries it as its precondition, the discriminating verify step comes first, and recommended_index is 0. recommended_reason: one clause on why it is the safe pick and, when the fix restores a behaviour the evidence cannot confirm is the wanted one (a default config, an earlier template), a few words saying so; empty when 0. " +
883883
"revises_assessment: false on an initial or verification turn. confidence: 0..1. "
884884

885885
const storyGuidance = "STORY: prose an on-call engineer reads top to bottom, after the verdict block. Its job is the why: how the evidence leads to the headline, what else was checked, and what the evidence does not cover. It need not repeat the summary; open with whatever the reader must see first, knowing they will look for the result behind the headline. Length and shape are yours: as short as the case allows, never a bullet list restating results. An on-call reader gives it about a minute, so most cases fit in 150-250 words and only a case with several moving parts needs more; a healthy story stops once present health, the limit of any adverse signal, and what would reopen the question are clear. A healthy story says how long the target has been in this state only when a cited result shows it (a pod's start time, a rollout's completion, an event's age); restart or rollout age alone does not establish continuous health, so otherwise say it is healthy at the time of this check and that how long is unknown. Radar's audit findings are posture, not part of the story unless one bears on the cause. " +
886886
"PLACING A CARD: put [[radar:evidence=N]] alone on a line, where N is the 0-based index into evidence (the index, never the ev_ reference: [[radar:evidence-ref=...]] is the ledger's marker on a tool result and must not appear in the story, the summary or root_cause), and Radar renders that result's card there; [[radar:evidence=N|compact]] renders its header and your role label only, not your note; the same marker inside a sentence places the card under that paragraph if nothing else places it, and otherwise is a reference back to the card; prefer the own-line form, which lets you choose where the card lands. A marker inside a code span, code block or blockquote is literal text; the story holds no ```json fence of its own. Two or three cards is typical; 6 is the ceiling, not a target. " +
887-
"Only results Radar renders as cards can be placed: diagnose, get_resource, list_resources or list_namespaces with at least one item, get_events, get_pod_logs, get_workload_logs, get_changes, get_neighborhood, get_topology, get_helm_release, get_subject_permissions, get_prometheus_rules, query_prometheus and issues; a search, a top_resources ranking, an empty listing, list_helm_releases, list_packages, get_dashboard, discover_metrics, get_cluster_audit and get_cluster_upgrade_readiness can be cited in evidence but have no card, so never place them. WHEN TO PLACE: your judgement. Place a card where seeing the result changes how the sentence lands: the log line that names the error, the spec field that is wrong, the chart that shows when. Not every claim needs a card. Every result about this resource that Radar captured is listed under Captured results regardless, and every read is in Activity, so cite a result in evidence without placing it when a card would interrupt the reading, or place it compact when the reader needs the fact but not the detail. Close with what would change the answer, or one clause saying nothing open would, unless that is already plain. "
887+
"Only results Radar renders as cards can be placed: diagnose, get_resource, list_resources, list_namespaces, list_helm_releases, list_packages or search with at least one row, get_events, get_pod_logs, get_workload_logs, get_changes, get_neighborhood, get_topology, get_helm_release (with its hooks, history and values), get_subject_permissions (with its effective rules), get_prometheus_rules, query_prometheus, discover_metrics, issues, top_resources (a ranking; the workload's own row is marked), and get_cluster_audit or get_cluster_upgrade_readiness (only the findings about this resource or its relations become a card; a scan with none becomes a receipt); an empty listing or get_dashboard can be cited in evidence but has no card, so never place it. WHEN TO PLACE: your judgement. Place a card where seeing the result changes how the sentence lands: the log line that names the error, the spec field that is wrong, the chart that shows when. Not every claim needs a card. Every result about this resource that Radar captured is listed under Captured results regardless, and every read is in Activity, so cite a result in evidence without placing it when a card would interrupt the reading, or place it compact when the reader needs the fact but not the detail. Close with what would change the answer, or one clause saying nothing open would, unless that is already plain. "
888888

889889
// followUpInstruction keeps Findings stable across ordinary questions.
890890
// Saved runs show agents restating the root cause on most answers; without an

‎internal/mcp/tools_helm.go‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"log"
7+
"sort"
78
"strings"
89

910
"github.com/modelcontextprotocol/go-sdk/mcp"
@@ -131,6 +132,9 @@ func handleGetHelmRelease(ctx context.Context, req *mcp.CallToolRequest, input g
131132
result["managedByFluxHelmRelease"] = detail.ManagedByFluxHelmRelease
132133
}
133134

135+
if len(detail.History) > 0 {
136+
result["history"] = newestHelmRevisions(detail.History, defaultHelmHistoryLimit)
137+
}
134138
if len(detail.Hooks) > 0 {
135139
result["hooks"] = detail.Hooks
136140
}
@@ -333,6 +337,21 @@ func cloneHelmValue(value any) any {
333337
}
334338
}
335339

340+
// A release's stored history can run to hundreds of revisions; the detail
341+
// read carries the newest few so the Findings card can show the trail, and
342+
// include=history carries all of it.
343+
const defaultHelmHistoryLimit = 10
344+
345+
func newestHelmRevisions(history []helm.HelmRevision, limit int) []helm.HelmRevision {
346+
if len(history) <= limit {
347+
return history
348+
}
349+
sorted := make([]helm.HelmRevision, len(history))
350+
copy(sorted, history)
351+
sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Revision > sorted[j].Revision })
352+
return sorted[:limit]
353+
}
354+
336355
// parseIncludes parses a comma-separated include string into a set.
337356
func parseIncludes(s string) map[string]bool {
338357
result := make(map[string]bool)

‎internal/mcp/tools_helm_test.go‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,18 @@ func TestRedactedHelmValuesRedactsSecretsAndKeepsReferences(t *testing.T) {
8484
t.Fatalf("input mutated: %#v", input["password"])
8585
}
8686
}
87+
88+
func TestNewestHelmRevisionsKeepsTheNewestAndTheOrderOfShortHistories(t *testing.T) {
89+
short := []helm.HelmRevision{{Revision: 1}, {Revision: 2}}
90+
if got := newestHelmRevisions(short, 10); len(got) != 2 || got[0].Revision != 1 {
91+
t.Fatalf("short history changed: %+v", got)
92+
}
93+
long := make([]helm.HelmRevision, 0, 12)
94+
for i := 1; i <= 12; i++ {
95+
long = append(long, helm.HelmRevision{Revision: i})
96+
}
97+
got := newestHelmRevisions(long, 10)
98+
if len(got) != 10 || got[0].Revision != 12 || got[9].Revision != 3 {
99+
t.Fatalf("long history = %d entries, first %d, last %d; want 10, 12, 3", len(got), got[0].Revision, got[len(got)-1].Revision)
100+
}
101+
}

‎web/src/components/diagnose/EvidenceCard.tsx‎

Lines changed: 31 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { StoryExcerpt } from "./StoryExcerpt";
2+
import { storyExcerpt } from "./investigationEvidence/excerpt";
13
import {
24
type InvestigationCaseItem,
35
investigationCaseObservationKey,
@@ -10,13 +12,7 @@ import {
1012
useId,
1113
} from "react";
1214
import { clsx } from "clsx";
13-
import {
14-
Collapse,
15-
CollapseChevron,
16-
TerminalBlock,
17-
TerminalBlockLabel,
18-
stripAnsi,
19-
} from "@skyhook-io/k8s-ui";
15+
import { Collapse, CollapseChevron } from "@skyhook-io/k8s-ui";
2016
import { parseLogLine } from "../../utils/log-format";
2117
import { evidenceDisplaySnapshot } from "./investigationEvidencePresentation";
2218
import {
@@ -41,13 +37,10 @@ import {
4137
uniquePrimarySources,
4238
phaseLabel,
4339
} from "./investigationEvidence/cardParts";
44-
import { VISIBLE_LOG_EVIDENCE_LINES } from "./investigationEvidence/bodies/streams";
4540
import {
46-
InventoryRow,
4741
listingScopeNamespace,
4842
namedInventoryRows,
4943
} from "./investigationEvidence/bodies/inventory";
50-
import { MetricsBody } from "./investigationEvidence/bodies/metrics";
5144
import {
5245
EvidenceBody,
5346
evidenceHasDetails,
@@ -187,41 +180,28 @@ export function EvidenceCard({
187180
observation.data,
188181
observation.summary,
189182
);
190-
const storyLogLines =
191-
storyCard && observation.data.type === "logs"
192-
? (observation.data.logs?.lines ?? [])
193-
.slice(-VISIBLE_LOG_EVIDENCE_LINES)
194-
.map((line) => stripAnsi(line))
195-
: [];
196-
// A story log card shows the newest two selected lines always — the ones a
197-
// story cites from a tail. Expanding appends the earlier lines below them,
198-
// under a divider, so nothing above them moves.
199-
const storyLogHead = storyLogLines.slice(-2);
200-
const storyLogRest = storyLogLines.slice(0, -2);
201-
// A story log card carries its cited lines inline, so the only thing left to
202-
// expand is the earlier lines; the record keeps the full logs body.
203-
const inlineLogs = storyCard && observation.data.type === "logs";
204-
// A chart placed as the cause or the symptom is the card: it shows inline,
205-
// and the fold has nothing left to add.
206-
const inlineMetrics =
207-
storyCard &&
208-
!compact &&
209-
observation.data.type === "metrics" &&
210-
cardItems.some((item) => item.role === "cause" || item.role === "symptom");
211-
// Each row a citation names is the part of the listing the sentence rests
212-
// on; an entry the listing does not hold is often the point, so the card
213-
// says so rather than showing another row.
214-
const storyInventoryRows =
215-
storyCard && !compact && observation.data.type === "inventory"
183+
const excerpt = storyCard
184+
? storyExcerpt(observation, cardItems, {
185+
compact,
186+
scopeNamespace: listingScopeNamespace(observation.source),
187+
})
188+
: undefined;
189+
const citedRows =
190+
observation.data.type === "inventory"
216191
? namedInventoryRows(
217192
observation.data.resources,
218193
cardItems,
219194
listingScopeNamespace(observation.source),
220-
)
221-
: [];
195+
).flatMap((named) => (named.row ? [named.row] : []))
196+
: undefined;
197+
// A card whose content shows inline has nothing left behind the fold but
198+
// the earlier lines or the history.
199+
const inlineLogs = excerpt?.kind === "lines";
200+
const inlineMetrics = excerpt?.kind === "chart";
201+
const storyLogRest = excerpt?.kind === "lines" ? excerpt.rest : [];
222202
const hasExpandableBody =
223203
(hasEvidenceDetails && !inlineLogs && !inlineMetrics) || meaningfulHistory;
224-
const canExpand = !compact && (hasExpandableBody || storyLogLines.length > 2);
204+
const canExpand = !compact && (hasExpandableBody || storyLogRest.length > 0);
225205
const revealHistory = investigationEvidenceShouldRevealHistory(
226206
group,
227207
revealSourceId,
@@ -434,80 +414,17 @@ export function EvidenceCard({
434414
))}
435415
</div>
436416
) : null}
437-
{storyLogHead.length > 0 ? (
438-
<div
439-
data-story-log-lines
440-
className={
441-
prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2"
442-
}
443-
>
444-
<TerminalBlock
445-
footer={
446-
storyLogRest.length > 0 ? (
447-
<Collapse open={open && canExpand}>
448-
<TerminalBlockLabel divider>
449-
Earlier lines
450-
</TerminalBlockLabel>
451-
<pre className="overflow-x-auto whitespace-pre-wrap break-words px-3 pb-2.5 font-mono text-xs leading-relaxed text-[var(--terminal-text)]">
452-
{storyLogRest.join("\n")}
453-
</pre>
454-
</Collapse>
455-
) : null
456-
}
457-
>
458-
{storyLogHead.join("\n")}
459-
</TerminalBlock>
460-
</div>
461-
) : null}
462-
{storyInventoryRows.length > 0 ? (
463-
<div
464-
data-story-inventory-rows
465-
className={
466-
prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2"
467-
}
468-
>
469-
<div className="rounded-md border border-theme-border">
470-
{storyInventoryRows.map((entry, index) =>
471-
entry.row ? (
472-
<InventoryRow
473-
key={entry.key}
474-
resource={entry.row}
475-
divider={index > 0}
476-
/>
477-
) : (
478-
<p
479-
key={entry.key}
480-
data-story-inventory-absent={
481-
entry.matches === 0 ? "" : undefined
482-
}
483-
className={clsx(
484-
"px-2.5 py-1.5 font-mono text-xs text-theme-text-secondary",
485-
index > 0 && "border-t border-theme-border/60",
486-
)}
487-
>
488-
{entry.matches === 0
489-
? `Not in this listing: ${entry.label}`
490-
: `${entry.label}: ${entry.matches} entries match in this listing`}
491-
</p>
492-
),
493-
)}
494-
</div>
495-
</div>
496-
) : null}
497-
{inlineMetrics && observation.data.type === "metrics" ? (
498-
<div
499-
data-story-metrics
500-
className={
501-
prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2"
502-
}
503-
>
504-
<MetricsBody
505-
data={observation.data}
506-
changeCoverage={metricsMarkersBySource?.get(
507-
observation.source.id,
508-
)}
509-
/>
510-
</div>
417+
{excerpt ? (
418+
<StoryExcerpt
419+
excerpt={excerpt}
420+
data={observation.data}
421+
prominence={prominence}
422+
open={open}
423+
canExpand={canExpand}
424+
changeCoverage={metricsMarkersBySource?.get(
425+
observation.source.id,
426+
)}
427+
/>
511428
) : null}
512429
</>
513430
) : cardItems.some((item) => item.claim || item.role) ? (
@@ -550,6 +467,7 @@ export function EvidenceCard({
550467
changeCoverage={metricsMarkersBySource?.get(
551468
observation.source.id,
552469
)}
470+
citedRows={citedRows}
553471
/>
554472
) : null}
555473
{meaningfulHistory ? (

0 commit comments

Comments
 (0)