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
- 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.
- 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?
- Cross-source precedence — summary always wins, or first-authored wins? Chronological order across notes is available from artefact
created_at.
- 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?
Context
A scout report can carry charts.
SignalReport.charts(products/signals/backend/models.py) holds a validated list ofReportChart(products/signals/backend/report_charts.py) —chart_id+title+query+ optionalcaption/size, wherequeryis anInsightVizNode,DataVisualizationNode, orSavedInsightNode. The scout writes them throughemit_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.LemonMarkdownrecognizes the target and defers to arenderChartRefprop (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 astrailingCharts.A scout can also append a note artefact to a report —
edit_report(append_note=…)→append_report_note(scout_report/persistence.py) → aNoteArtefact(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 throughLemonMarkdownwith norenderChartRef, so achart:reference in a note is silently reduced to its label text.resolveChartPlacementsonly ever parses the summary, so a chart attached alongside a note falls totrailingChartsand 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
chartsset bychart_id. A scout attaches the chart and the note in a singleedit_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
ReportChartvalidation and the batch caps (MAX_REPORT_CHARTS,MAX_REPORT_CHARTS_QUERY_CHARS) unchanged, and needs no schema or migration.The cost is that
chartson 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 inNoteArtefact.content) is in Open questions.Scope
Placement
resolveChartPlacementstakes 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.resolveChartPlacementsalready encodes "only the first reference to an id places it" within a source; this is the cross-source version of the same rule.trailingChartsbecomes "placed by neither the summary nor any note", so a chart drawn in a note stops being duplicated under the prose.inboxReportDetailLogic(reportCharts,chartsById,chartPlacements,trailingCharts). Note bodies come fromreportArtefacts, which polls — the placement inputs need the same identity-stable keyingchartIdsKeyalready does, or every poll recomputes placement and remounts charts.Rendering
NoteBodypassesrenderChartRefand renders<ReportChart chartId={…} />.ReportChartalready resolves the chart from the logic by id, so no new plumbing.text-xscolumn, andinferChartSize/SIZE_HEIGHTSare tuned for the report body. A chart in a note needs its own cap so alargechart doesn't blow out a log row.Scout-facing instructions
_REPORT_CHARTSinscout_harness/prompt.pycurrently says "Place it from the summary". It needs the note case: thatappend_note+chartsin oneedit_reportcall is the idiom, that a note's reference resolves against the report's charts, and that replacingchartslater orphans a reference in an older note.scout-edit-reportinproducts/signals/mcp/tools.yamldescribeschartsas report-level andappend_noteas separate; the descriptions should say they compose.Renderers that can't draw charts
products/desktop/packages/ui/src/features/inbox/components/detail/ArtefactLogList.tsx,CollapsibleNote) draws no charts — verify achart:reference degrades to its plain label there rather than showing raw markdown or a dead link.slack_formatting.pyneeds no change. If that ever changes, the existingchart:reduction has to run over note text too.Tests
chartsreplacement reads as its label.NoteBodywith and without a resolvable chart.Open questions
charts: list[ReportChart]onNoteArtefact.content— self-contained, immune to a laterchartsreplacement, 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.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?created_at.