Skip to content

Signals: render report charts inside report note artefacts #86843

Description

@andrewm4894

Context

A scout report can carry charts. SignalReport.charts (products/signals/backend/models.py) holds a validated list of ReportChart (products/signals/backend/report_charts.py) — chart_id + title + query + optional caption/size, where query is an InsightVizNode, DataVisualizationNode, or SavedInsightNode. The scout writes them through emit_report(charts=…) / edit_report(charts=…) (scout_harness/tools/report.py).

Placement is a markdown convention, not a bespoke block type: a link with a chart: target in the report's summary[Daily signups](chart:signups-drop) — draws that chart at that point in the prose. LemonMarkdown recognizes the target and defers to a renderChartRef prop (frontend/src/lib/lemon-ui/LemonMarkdown/LemonMarkdown.tsx); resolveChartPlacements (products/signals/frontend/inbox/utils/chartPlacement.ts) decides which references are placeable; anything unplaced renders after the prose as trailingCharts.

A scout can also append a note artefact to a report — edit_report(append_note=…)append_report_note (scout_report/persistence.py) → a NoteArtefact (artefact_schemas.py) on the report's Activity log. This is how a scout adds evidence to a report it already emitted, without rewriting the summary.

Notes cannot draw charts. NoteBody (products/signals/frontend/inbox/components/detail/ArtefactLogList.tsx) renders the note through LemonMarkdown with no renderChartRef, so a chart: reference in a note is silently reduced to its label text. resolveChartPlacements only ever parses the summary, so a chart attached alongside a note falls to trailingCharts and renders under the summary — detached from the note that explains it, and above prose written before the chart existed.

So a scout returning to a report with new evidence has to choose between putting the finding in a note where the chart won't draw, or rewriting the summary it may not own the framing of.

What this issue asks for

A chart: reference inside a report note draws its chart in the note, the same way one in the summary draws it in the prose.

Design: reference the report's charts, don't give notes their own

The default should be that a note references the report's existing charts set by chart_id. A scout attaches the chart and the note in a single edit_report(append_note=…, charts=[…]) call, which already works today — only the rendering and placement halves are missing.

This keeps one chart set per report, reuses ReportChart validation and the batch caps (MAX_REPORT_CHARTS, MAX_REPORT_CHARTS_QUERY_CHARS) unchanged, and needs no schema or migration.

The cost is that charts on an edit replaces the whole set (_build_edit_charts), so a later edit that drops a chart leaves an older note referencing an id that no longer exists. That degrades to the note's plain label — the same fallback Slack and the desktop inbox already get — which is acceptable, but it has to be a stated property rather than a surprise. The alternative (embedding charts in NoteArtefact.content) is in Open questions.

Scope

Placement

  • resolveChartPlacements takes one markdown source today. Extend placement to resolve references across the summary and each note body, returning per-source placements so a note knows which of its own references draw.
  • Precedence has to be explicit and stable: a chart placed in the summary should not also draw in a note, and where two notes reference the same id only one should draw. resolveChartPlacements already encodes "only the first reference to an id places it" within a source; this is the cross-source version of the same rule.
  • trailingCharts becomes "placed by neither the summary nor any note", so a chart drawn in a note stops being duplicated under the prose.
  • Selectors live in inboxReportDetailLogic (reportCharts, chartsById, chartPlacements, trailingCharts). Note bodies come from reportArtefacts, which polls — the placement inputs need the same identity-stable keying chartIdsKey already does, or every poll recomputes placement and remounts charts.

Rendering

  • NoteBody passes renderChartRef and renders <ReportChart chartId={…} />. ReportChart already resolves the chart from the logic by id, so no new plumbing.
  • Size is the real work: the Activity log is a narrow text-xs column, and inferChartSize/SIZE_HEIGHTS are tuned for the report body. A chart in a note needs its own cap so a large chart doesn't blow out a log row.
  • Each rendered chart runs its query on open. Since the same chart id can now be referenced from two places, confirm the precedence rule above actually prevents a second mount rather than just hiding one.

Scout-facing instructions

  • _REPORT_CHARTS in scout_harness/prompt.py currently says "Place it from the summary". It needs the note case: that append_note + charts in one edit_report call is the idiom, that a note's reference resolves against the report's charts, and that replacing charts later orphans a reference in an older note.
  • scout-edit-report in products/signals/mcp/tools.yaml describes charts as report-level and append_note as separate; the descriptions should say they compose.

Renderers that can't draw charts

  • Desktop inbox (products/desktop/packages/ui/src/features/inbox/components/detail/ArtefactLogList.tsx, CollapsibleNote) draws no charts — verify a chart: reference degrades to its plain label there rather than showing raw markdown or a dead link.
  • Notes don't reach Slack today, so slack_formatting.py needs no change. If that ever changes, the existing chart: reduction has to run over note text too.

Tests

  • Placement: a reference in a note draws there; the same id in summary and note draws once; a reference to an id with no chart reads as its label; an orphaned reference after a charts replacement reads as its label.
  • Rendering: NoteBody with and without a resolvable chart.

Open questions

  1. Reference vs embed. The alternative is charts: list[ReportChart] on NoteArtefact.content — self-contained, immune to a later charts replacement, and it matches a note being an append-only log entry. It costs a second validation surface, a second set of batch caps, chart payloads inside artefact content, and two places a reader's browser can pick up a query to run. Worth deciding before any code.
  2. The Activity section is defaultCollapsed (ReportActivitySection.tsx). A chart placed only in a note is invisible until someone expands it. Is that fine (the note is supplementary by construction), or should a note-placed chart also surface somewhere the reader sees by default?
  3. Cross-source precedence — summary always wins, or first-authored wins? Chronological order across notes is available from artefact created_at.
  4. Human-authored notes. Note artefacts can be written through the generic artefact API, not just by scouts. Should a chart reference from a non-scout author render the same way, or is this scout-authored content only?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions