Status: spike / pre-ADR Author: design spike (plan 010), 2026-06-13 Next step: maintainer acceptance promotes a chosen embed strategy to an ADR under
docs/architecture/adr/000N-charts-in-html-report.md, and a small follow-up implementation plan wires it in.
The headline finding is stronger than the planning note assumed. PAL-X has a deterministic SVG chart renderer with golden-grade tests, but no code anywhere actually calls it to write a chart file, no report writer references a chart, and the CLI flags that were meant to drive it are dead. The expensive, hard part (byte-deterministic SVG) is done; the wiring is 100% missing — not merely "orphaned files nobody links to."
dotnet/src/Pal.Reporting/Charts/ScottPlotRenderer.cs exposes two entry points:
// ScottPlotRenderer.cs:18 — returns canonicalized SVG as a string (in-memory)
public static string Render(
string title,
IReadOnlyList<(DateTimeOffset ts, double value)> series,
double? warningThreshold = null,
double? criticalThreshold = null)
// ScottPlotRenderer.cs:36 — renders, then writes the SVG to a file
public static void RenderToFile(
string title,
IReadOnlyList<(DateTimeOffset ts, double value)> series,
string outputPath,
double? warningThreshold = null,
double? criticalThreshold = null)Render forces CultureInfo.InvariantCulture for the duration of the render
(ScottPlotRenderer.cs:24-33), draws a single scatter line plus optional dashed warning /
critical horizontal threshold lines from a frozen explicit-hex palette
(ScottPlotRenderer.cs:13-16), then pipes the raw SVG through
SvgCanonicalizer.Canonicalize (ScottPlotRenderer.cs:90-91). The plot size is fixed at
720×360 (ScottPlotRenderer.cs:9-10). RenderToFile writes with
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false) (ScottPlotRenderer.cs:44) —
UTF-8 without BOM, per the project convention.
SvgCanonicalizer (dotnet/src/Pal.Reporting/Charts/SvgCanonicalizer.cs) strips comments
and <metadata> blocks (which can embed a ScottPlot version stamp), then rewrites every
id="..." definition and url(#...) reference to a stable pal-N sequence numbered in
document order (SvgCanonicalizer.cs:24-65). This is what makes two renders of the same
data byte-identical despite SkiaSharp's otherwise-random element ids.
$ grep -rn "RenderToFile" dotnet
dotnet/src/Pal.Reporting/Charts/ScottPlotRenderer.cs:36: public static void RenderToFile(
The only hit is the definition itself. No production code and no test ever calls
RenderToFile. The only caller of Render is ScottPlotRendererTests
(dotnet/tests/Pal.Reporting.Tests/ScottPlotRendererTests.cs:21-76). So no chart SVG is
ever written to disk by any code path today — the <output>/charts/<report-name>-<chart-id>.svg
location described in CLAUDE.md is a planned convention, not a path any running code
produces.
AnalyzeSettings declares both flags:
// AnalyzeCommand.cs:74-80
[CommandOption("--include-charts")]
[Description("Emit chart SVG artifacts")]
public bool IncludeCharts { get; init; }
[CommandOption("--chart-limit <n>")]
[Description("Maximum charts to generate (default: 20)")]
public int ChartLimit { get; init; } = 20;But AnalyzeCommand.Execute (AnalyzeCommand.cs:115-243) never reads
settings.IncludeCharts or settings.ChartLimit. The execute body runs the analysis,
writes JSON/HTML/Markdown, and returns — there is no branch on the chart flags and no call
into ScottPlotRenderer. Confirmed by grep: across all of dotnet, IncludeCharts /
ChartLimit appear only at their declaration site (AnalyzeCommand.cs:74-80). The flags
are inert.
Drift note for reviewers. Plan 010's "Why this matters" framed charts as "written as standalone files that nothing links to." The live code is one step earlier than that: nothing writes the files in the first place. The drift check (
git diff --stat 208e140..HEADover the reporting project,AnalyzeCommand.cs, and the report schema) was clean — no in-scope source has changed since the plan was authored; the plan's "Current state" prose simply over-stated what existed. This doc records the verified reality.
$ grep -rniE 'chart|\.svg|<img|data:image' dotnet/src/Pal.Reporting/Html → no matches
$ grep -rniE 'charts/|<img|data:image' dotnet/src/Pal.Reporting → no matches
HtmlReportWriter (dotnet/src/Pal.Reporting/Html/HtmlReportWriter.cs) builds the report
as a single self-contained HTML string: header, summary table, a RenderFinding block per
finding with an evidence table (HtmlReportWriter.cs:103-114), a warnings list, and a
footer. It emits an <evidence-table> of Metric / Avg / Max / P95 / Condition
(HtmlReportWriter.cs:106-111) but no <img>, no inline <svg>, no charts/ link.
There is no <script> and the only style is one inline <style> block
(HtmlReportWriter.cs:129-153).
JsonReportWriter.MapFinding (dotnet/src/Pal.Reporting/Json/JsonReportWriter.cs:133-172)
emits evidence.metrics but no evidence.charts array, even though the schema defines
one (see below).
dotnet/schemas/pal.report.v1.json defines Evidence.charts and a ChartRef:
and a top-level Artifacts.chart_paths array (pal.report.v1.json:288-291). Both shapes
are present in the schema but written by no code path — the writers never populate
evidence.charts or artifacts.chart_paths. Notably, ChartRef.artifact_path presumes an
on-disk file, which is the link model (option C below), not an embed.
| Stage | CLI path | API path |
|---|---|---|
| Entry | AnalyzeCommand.Execute (AnalyzeCommand.cs:115) |
AnalysisWorker.GenerateAndStoreReportsAsync (Worker/AnalysisWorker.cs:217) |
| Analysis | new AnalysisRunner().Run(...) (AnalyzeCommand.cs:142) |
runner result passed in |
| JSON write | new JsonReportWriter().Write(writeInput) → file (AnalyzeCommand.cs:215) |
JsonReportWriter().WriteToStream(..., jsonMs) → bytes → storage (AnalysisWorker.cs:240-243) |
| HTML write | HtmlReportWriter.Write(writeInput, htmlPath) → file (AnalyzeCommand.cs:221) |
HtmlReportWriter.WriteToStream(..., htmlMs) → bytes → storage (AnalysisWorker.cs:246-249) |
| Markdown write | new MarkdownReportWriter().Write(...) (AnalyzeCommand.cs:227) |
MarkdownReportWriter().WriteToStream(...) (AnalysisWorker.cs:251-255) |
| Chart render | (none — IncludeCharts ignored, RenderToFile never called) |
(none — worker never references ScottPlotRenderer) |
| Chart → file | (none) | (none — reports persist to storage via WriteReportAsync; there is no charts/ dir) |
AnalysisWorker.GenerateAndStoreReportsAsync (dotnet/src/Pal.Api/Worker/AnalysisWorker.cs:217-256)
generates exactly three artifacts — JSON, HTML, Markdown — entirely in memory via the
WriteToStream overloads, then persists each byte[] through
_storage.WriteReportAsync(jobId, format, bytes, ct) and records it with
_analysisRepo.SaveReportAsync (AnalysisWorker.cs:242-255). It never references
ScottPlotRenderer, never writes a charts/ directory, and the user retrieves a report by
GET /analysis/{id}/report?format=html|json|markdown
(dotnet/src/Pal.Api/Endpoints/AnalysisEndpoints.cs:71-91), which streams the single stored
report blob. The Blazor UI links to that endpoint
(dotnet/src/Pal.Api/Components/Pages/JobDetail.razor:82-84). There is no on-disk
sibling directory the browser could resolve a relative charts/... URL against. This is
the single most important constraint for the embed-vs-link decision.
Make the HTML report glance-able: a stakeholder opening a PAL-X HTML report should see the trend line and threshold bands for each triggering metric, not just a table of numbers. Concretely, a successful implementation:
- Renders a chart per triggering metric of each finding (the warning/critical threshold lines that fired are drawn on the chart) and shows it next to that finding's evidence table in the HTML report.
- Works identically for both delivery paths: the CLI's on-disk HTML file and the API's
stored-blob HTML download. A user who downloads the HTML and opens it from their Downloads
folder — with no adjacent
charts/directory — still sees every chart. - Stays byte-deterministic under a fixed
--now, reusing the existingSvgCanonicalizerso the chart bytes are stable. - Costs the follow-up plan roughly: wire the renderer into the writer's per-finding loop, thread the series data + thresholds into the writer's input, and (one time, intentionally) regenerate the JSON golden fixtures.
A concrete target used to evaluate the options below: the cpu-pressure fixture, whose
high-cpu-sustained warning finding should render a CPU % Processor Time line with a
dashed warning line at the rule's threshold.
The chart SVG can reach the browser three ways. The deciding axis is the API download
path, which has no charts/ directory (see §1).
Drop the canonicalized SVG markup directly into the HTML at the finding's location:
<div class="finding sev-warning">
...evidence table...
<svg viewBox="0 0 720 360" ...> ...chart... </svg>
</div>Pros
- Fully self-contained: one HTML file, works for CLI-on-disk and API-download identically.
- No base64 inflation — the SVG bytes appear once, verbatim.
- SVG scales crisply and is themeable/inspectable in the DOM.
- The bytes already flow through
SvgCanonicalizer, so determinism is inherited.
Cons
- The SVG's own
id="pal-N"values (post-canonicalization) and anyurl(#pal-N)references live in the same DOM as the rest of the report. With multiple inlined charts, ids collide — every chart restarts its id counter atpal-0. The writer must namespace ids per chart (e.g. prefix with thechart_id) or wrap each chart in an<svg>with its own coordinate scope. This is the one real wrinkle. - The SVG ScottPlot emits is prefixed with an XML declaration
(
ScottPlotRendererTests.cs:61notes "ScottPlot prepends an XML declaration before<svg"); an inline embed must strip that leading<?xml ...?>since it is illegal mid-HTML-body. - Sanitization: ScottPlot output is trusted (we generate it), but if charts are inlined
the writer should still assert the fragment contains no
<script>/<foreignObject>and no event handlers, so a future untrusted input can't smuggle markup. Cheap to add as a guard.
Base64-encode the SVG and reference it from an <img>:
<img alt="CPU % Processor Time" src="data:image/svg+xml;base64,PHN2ZyB...">Pros
- Self-contained; works for both delivery paths.
- No id-collision risk — the SVG is an isolated document inside the
<img>, so itspal-Nids never touch the host DOM. This sidesteps Option A's only real wrinkle. - Simpler writer code: encode and emit one attribute; no fragment surgery.
Cons
- ~33% size inflation from base64. For the default
--chart-limit 20this is modest (each 720×360 line SVG is small), but it compounds on large reports. <img>-hosted SVG cannot inherit page CSS and is not DOM-inspectable — fine for a static chart, but less flexible than inline.- The data URI must be built from the canonicalized bytes for determinism; the XML declaration is legal inside a data URI so no stripping is needed.
Write each SVG to <output>/charts/... (the CLAUDE.md convention, matching
ChartRef.artifact_path in the schema) and link it.
Pros
- Smallest HTML; charts cached as separate files; matches the existing (unused)
ChartRef.artifact_path/Artifacts.chart_pathsschema slots. - The CLI already has an
<output>directory to write acharts/subdir into.
Cons
- Breaks on the API download path. The API stores the HTML as a single blob and serves
it via
GET /analysis/{id}/report?format=html(AnalysisEndpoints.cs:71-91); there is no adjacentcharts/directory, and no endpoint that would serve one. A user who downloads the HTML sees broken image icons. This is disqualifying for the API path (which is the primary product surface), and a report that renders differently depending on how it was produced is exactly the inconsistency to avoid. - Even for the CLI, a relative link breaks the moment the user emails or moves the
.htmlwithout thecharts/folder — the same fragility the legacy single-file report avoided.
Option B (data:image/svg+xml;base64 in <img>) is the recommended default.
It is the only option that is self-contained across both delivery paths and free of the
id-collision hazard, at the cost of a modest, bounded size increase that --chart-limit
already caps. Option A is a reasonable alternative if the size budget proves tight (base64
overhead matters) and the team is willing to namespace per-chart ids; the canonicalizer
would need a small extension to accept an id prefix. Option C is rejected because it
silently breaks the API download — PAL-X's primary surface. The schema's
ChartRef.artifact_path / Artifacts.chart_paths fields are link-shaped and should be
treated as vestigial under options A/B; the follow-up plan should decide whether to
repurpose ChartRef to carry an embed reference (e.g. chart_id + title, dropping
artifact_path) or leave the JSON report chart-free and embed only in HTML. (Needs
maintainer decision — see Open Questions.)
One chart per triggering metric of each finding — i.e. per EvidenceMetric on a
finding, the metric named by EvidenceMetric.CanonicalMetric is plotted. The finding's
HTML already loops over f.EvidenceMetrics to build the evidence table
(HtmlReportWriter.cs:107-111); the chart for each row is rendered from the same series.
This keeps the visual tightly coupled to the evidence that explains it, rather than a
disconnected top-level gallery.
Threshold lines come from the rule that fired: the warning/critical thresholds passed to
ScottPlotRenderer.Render(..., warningThreshold, criticalThreshold) should be the
numeric thresholds of the triggering condition, so the chart visually shows the breach.
Data-availability gap (needs implementation attention). The writer's current input,
JsonReportWriter.WriteInput, does not currently carry the per-metric sample series or the rule thresholds in a form the chart renderer can consume.EvidenceMetriccarriesStatisticsandTriggerDetails(HtmlReportWriter.cs:108-111), and the raw samples live oninput.Dataset.Serieskeyed byseries_id(JsonReportWriter.cs:174-185). The follow-up plan must joinEvidenceMetric.SeriesId→ the dataset'sTimeSeriessamples to build the(DateTimeOffset ts, double value)listRenderexpects, and surface the rule thresholds (available inTriggerDetails.ExpectedValue/TriggerDetail.expected_value,pal.report.v1.json:223). This is a wiring task, not a new data source — but it is the bulk of the implementation effort.
Inside each RenderFinding block (HtmlReportWriter.cs:93-124), after the evidence table
and before the recommendations. Each chart sits beside the metric row that produced it.
Respect --chart-limit (default 20, AnalyzeCommand.cs:80). The cap applies to the total
number of charts in the report, counted in finding order. When the cap is hit, remaining
findings render their evidence tables without charts (no truncation of the findings
themselves). For the API path, which has no CLI flag, a fixed default of 20 should apply
(or a config value); the worker would pass the same limit into the writer input.
Findings are already emitted in the canonical sort order the engine enforces
(severity desc → category asc → rule_id asc → finding_id asc, per CLAUDE.md). Charts
inherit that order. Each chart needs a stable chart_id; the natural, collision-free choice
is "{finding_id}-{series_id}" (or a short hash of it), which is deterministic given the
deterministic finding/series ids. This chart_id also becomes the per-chart id namespace
prefix that Option A would need, and the ChartRef.chart_id if the JSON report later
carries chart refs.
--include-charts is opt-in today (and inert). Recommendation: make HTML chart embedding
default-on once it is self-contained (Option B), because the whole value is
glance-ability and a self-contained embed has no orphaned-file downside. Keep a
--no-charts (or repurpose --include-charts to its inverse) escape hatch for users who
want the smaller HTML. (Needs maintainer decision — see Open Questions.) Whatever is
chosen, the API path needs an equivalent toggle or a fixed default since it has no CLI flags.
Determinism is a hard constraint, and the good news is the hardest part is already solved.
Every chart goes through SvgCanonicalizer.Canonicalize inside Render
(ScottPlotRenderer.cs:90-91), which strips version-stamped comments/metadata and rewrites
ids to a stable pal-N sequence. The renderer also pins InvariantCulture
(ScottPlotRenderer.cs:24-33) so decimal formatting can't drift by locale — exactly what
ScottPlotRendererTests.Render_NumberFormatting_UsesInvariantCulture asserts
(ScottPlotRendererTests.cs:30-47). And
ScottPlotRendererTests.Render_ProducesByteIdenticalOutput_OnTwoRenders
(ScottPlotRendererTests.cs:18-27) already proves two renders of the same series are
byte-identical. The embedded bytes must be these canonical bytes — the writer calls
Render (string) and embeds its result; it must never call into ScottPlot directly or
bypass the canonicalizer.
For Option A, the per-chart id prefix is a determinism input too: as long as the prefix
derives from the deterministic chart_id, the inlined ids stay stable. For Option B,
base64 of identical bytes is identical bytes — no extra concern.
The plan's Step 4 anticipates "golden HTML fixtures" in Pal.Reporting.Tests. Verified
reality:
Pal.Reporting.Testshas no golden/byte-identical report fixtures.HtmlReportWriterTestsuses substring assertions (Assert.Contains,HtmlReportWriterTests.cs:74-109) and a BOM check — not a byte-for-byte golden file. The only byte-identical assertion in that project is the SVG self-consistency test noted above.- The byte-level report golden fixtures live in
Pal.Cli.Tests, not the reporting project:GoldenFixtureTests.AssertMatchesGolden(dotnet/tests/Pal.Cli.Tests/GoldenFixtureTests.cs:212-255) compares a freshly written report againstfixtures/<name>/golden.pal-report.json. These goldens are JSON-only (one per fixture:healthy-server,cpu-pressure,disk-latency,memory-pressure,GoldenFixtureTests.cs:184-210). There is nogolden.pal-report.htmlanywhere in the repo. - The JSON comparison masks machine/OS/path-variant fields before comparing
(
MaskEngineFields,GoldenFixtureTests.cs:258-271):report_id,dataset_id,engine.version/host_os/runtime, andartifacts.json_report_path.
- If charts are embedded in HTML only (Option B, JSON unchanged): the existing JSON
goldens are unaffected — they assert nothing about HTML, and
MapFindingwould not gain achartsarray. No golden regeneration is needed at all. This is a meaningful simplification the plan didn't foresee, and it argues for keeping charts out of the JSON report (embed in HTML, leaveevidence.chartsempty). - If the JSON report also gains
evidence.charts(e.g. populatingChartRef): the fourgolden.pal-report.jsonfiles must be regenerated once, intentionally, and that diff reviewed as a deliberate change — not mistaken for drift. Flag it prominently in the follow-up PR. - If an HTML golden is introduced to lock chart embedding byte-for-byte (recommended, to
protect determinism), it is a new fixture, not a regeneration — add a
golden.pal-report.htmlper fixture and anAssertMatchesGolden-style HTML comparison. Because the embedded SVG bytes are canonical and the report is generated under a fixed--now/GeneratedAt, a byte-identical HTML golden is achievable; it would need the same masking treatment for any machine-variant fields the HTML surfaces.
Legacy PAL surfaced outlier-trimmed means (10% / 20% / 30%) so a reviewer could see the "typical" average under spiky data. PAL-X does not:
$ grep -niE 'outlier|excl_|trimmed' dotnet/schemas/pal.report.v1.json → no matches
The Statistics object today carries count/min/max/avg/median/p90/p95/p99/stddev/ trend_per_hour/missing_sample_count (pal.report.v1.json:265-281), mirrored by the
SeriesStatistics model (dotnet/src/Pal.Engine/Model/SeriesStatistics.cs:3-16) and
computed in SeriesStatisticsCalculator (dotnet/src/Pal.Engine/Statistics/ SeriesStatisticsCalculator.cs:37-43).
This is fully separable from the chart work and should be its own plan. Adding
avg_excl_10pct / avg_excl_20pct / avg_excl_30pct touches a wider blast radius than the
charts:
SeriesStatisticsmodel — three newdoubleproperties.SeriesStatisticsCalculator— compute trimmed means (sort, drop the top k%, average).pal.report.v1.jsonStatisticsdefinition — three new optional properties.- All three writers, which each render an evidence table today:
JsonReportWriter.MapStats(JsonReportWriter.cs:187-200),HtmlReportWriterevidence table (HtmlReportWriter.cs:106-111),MarkdownReportWriterevidence table (MarkdownReportWriter.cs:81-87).
- The four
golden.pal-report.jsonfixtures — regenerate once, intentionally (the new stats fields would appear in every series'statistics).
Because step 5 alone is a deliberate golden regeneration and steps 1–4 touch the engine, schema, and every writer, this add-on is best landed after the chart work, as a clean independent change. It is explicitly out of scope for the chart spike and listed here only so the chart plan does not accidentally entangle it.
A scoped outline for a follow-up implementation plan (not executed here).
Title: "Embed deterministic finding charts in the HTML report (Option B)"
Scope:
-
Thread chart inputs into the writer. Extend
JsonReportWriter.WriteInput(the shared input for all writers) — or pass a precomputed per-finding chart model — so the HTML writer can, for eachEvidenceMetric, obtain (a) the metric's sample series joined frominput.Dataset.SeriesviaSeriesId, and (b) the warning/critical thresholds from the triggering rule. This join is the core of the work. -
Render + embed in
HtmlReportWriter.RenderFinding. For each evidence metric, callScottPlotRenderer.Render(title, series, warningThreshold, criticalThreshold), base64-encode the canonical SVG, and emit<img class="chart" alt="{metric}" src="data:image/svg+xml;base64,...">after the evidence table. Assign each chart a deterministicchart_id = "{finding_id}-{series_id}". -
Honor the limit. Stop emitting charts once the running count reaches
--chart-limit(CLI) or a fixed default (API). Findings beyond the cap keep their tables. -
Wire both paths. CLI: read
settings.IncludeCharts/ default-on decision inAnalyzeCommand.Execute(currently ignored) and pass the limit through. API:AnalysisWorker.GenerateAndStoreReportsAsyncpasses the same limit/toggle into the writer input — nocharts/directory, no new endpoint, because the SVG is embedded. -
Lock determinism with a new HTML golden. Add a
golden.pal-report.htmlper existing fixture and anAssertMatchesGolden-style byte comparison (modeled onGoldenFixtureTests.AssertMatchesGolden,GoldenFixtureTests.cs:212-255), with masking for any machine-variant fields. If JSONevidence.chartsis left empty, the existing JSON goldens need no change. -
Decide the JSON-report chart contract. Either leave
evidence.charts/artifacts.chart_pathsempty (simplest, recommended) or populateChartRefwith embed metadata — and regenerate the JSON goldens if so.
What this plan does NOT include:
- The robust-stats add-on (§6) — separate plan.
- Interactive/JS charts, zoom/pan, or any change to
ScottPlotRenderer's rendering. - A
charts/directory or any new API endpoint (Option B needs neither). - Reviving Option C's link model.
Estimated effort: S–M. The renderer and canonicalizer are done; the work is the series/threshold join into the writer input and a one-time golden fixture addition.
Current evidence: --include-charts is opt-in and inert (AnalyzeCommand.cs:74-76,
never read in Execute). A self-contained embed (Option B) has no orphaned-file downside,
which is the usual reason to keep chart generation opt-in.
Proposed answer: default-on for HTML, with a --no-charts escape hatch and an
equivalent API toggle/default. The flag semantics need a deliberate choice (repurpose
--include-charts, or invert to --no-charts).
Needs maintainer decision.
Current evidence: The schema already has Evidence.charts / ChartRef
(pal.report.v1.json:195-236) and Artifacts.chart_paths (pal.report.v1.json:288-291),
but both are link-shaped (artifact_path) and written by nothing. Markdown viewers often
don't render embedded SVG at all.
Proposed answer: Embed in HTML only for the first cut; leave evidence.charts empty
so the JSON goldens stay untouched (§5). Repurposing ChartRef to an embed-friendly shape,
or emitting a PNG for Markdown, are follow-ups.
Needs maintainer decision on whether the JSON report should advertise charts at all.
Current evidence: MarkdownReportWriter emits a GFM evidence table
(MarkdownReportWriter.cs:81-87) and is delivered via --markdown, ?format=markdown, and
pal remote report --format markdown. GFM has no reliable inline-SVG support, and many
email clients strip SVG.
Proposed answer: Out of scope for the first chart cut (Markdown stays table-only). If
Markdown charts become a requirement, render PNG via ScottPlot's raster export and embed a
data:image/png;base64 URI — but PNG determinism (no canonicalizer for raster) needs its
own investigation before committing.
Needs maintainer decision if Markdown/email chart parity is a product goal.
Current evidence: --chart-limit defaults to 20 (AnalyzeCommand.cs:80). Each 720×360
line SVG is small, but base64 (Option B) adds ~33%, and the API stores the whole HTML as a
single blob (AnalysisWorker.cs:246-249) — SaveReportAsync records the byte length, so an
oversized report is observable but uncapped.
Proposed answer: Keep the 20-chart cap as the size guardrail for the first cut; if reports balloon, lower the default or switch large reports to Option A (inline, no base64 overhead). Add a soft byte-size warning if a report exceeds a threshold.
Needs maintainer decision on a hard size cap vs. the chart-count cap as the sole lever.
Current evidence: Both are self-contained and work across delivery paths. A is smaller
but risks pal-N id collisions across multiple inlined charts (the canonicalizer restarts
its counter per render, SvgCanonicalizer.cs:33-41) and needs the XML declaration stripped;
B is collision-free and simpler but ~33% larger.
Proposed answer: B for the first cut (simplicity + no id-collision risk); revisit A if
size becomes a problem, adding an id-prefix parameter to SvgCanonicalizer.
Needs maintainer decision if the size budget is tight from day one.
The following are explicitly out of scope for the chart-embedding work and should not be folded in without a separate ADR:
-
Interactive / JavaScript charts. No client-side charting library, no
<script>, no zoom/pan/tooltip/brush. The report stays a static, self-contained document (it has no<script>today,HtmlReportWriter.cs:30-88). Interactivity is a Phase 2 web-consumer concern, not a report-writer concern. -
Changing the ScottPlot rendering itself. Plot dimensions, palette, axis style, and the canonicalization rules (
ScottPlotRenderer.cs:9-91,SvgCanonicalizer.cs) are frozen and golden-tested; this work consumes the renderer's output, it does not modify the renderer. -
A
charts/directory or chart-serving endpoint. Option B (and A) embed bytes; there is deliberately no on-disk artifact and no new API route. Reviving Option C's link model is a separate decision with the API-download breakage it implies. -
The robust-stats trimmed-means add-on (§6). Independent change set touching the engine, schema, and all three writers plus a JSON golden regeneration — its own plan.
-
Per-series charts for non-triggering metrics / a full dataset gallery. Only triggering evidence metrics get charts in the first cut; a top-level "all counters" gallery is a larger UX decision deferred until the per-finding charts prove their value.
-
PNG/raster output and Markdown/email chart parity. SVG-only, HTML-only for the first cut (see Q8c).