feat(chart): decimate served pages + run-context header (visual World cut 2)

Hardens the families-comparison chart page (#107) so it is openable and legible on
real multi-year data. Two halves, both confined to the CLI render path (engine +
registry untouched, C14):

#108 — serve-time min-max decimation. A pure `decimate(ChartData, buckets)`
transform partitions the shared union-ts spine into <=~2000 buckets and emits each
series' per-bucket min+max (nulls preserved), bounding the served page to a few
thousand points regardless of the underlying M1 point count. Applied in emit_chart
on both the single-run and family paths; a no-op under budget (every existing
fixture). Full recorded data stays on disk; only the page is thinned. For a
walk-forward family the null-fill blow-up collapses for free (an all-null bucket
stays null). Deterministic (C1).

#102 — a run-context header. A `ChartMeta` (name/commit/window/broker/seed/taps,
+ member count for a family, + bound params for a single run) is built from the
RunManifest in build_chart_data / build_comparison_chart_data (which gain a `name`
arg), injected into window.AURA_TRACES.meta, and rendered by a new pure buildHeader
in chart-viewer.js (headless .mjs-guarded). The family window is the SPAN across
members (min from, max to), not the first member's window — the only reading that
labels a disjoint walk-forward family's true OOS coverage (it collapses to the
shared window for sweep/MC). Reuses the existing render_value for param display
(no new Scalar Display, keeping C7's tag-only param plane). The earlier
"manifest is NOT carried into the page" render pin is flipped to a positive one.

Verified: cargo test --workspace = 446 passed / 0 failed; cargo clippy --workspace
--all-targets -D warnings clean. New coverage: 5 decimate unit tests, single-run +
family meta wiring (incl. the span-window invariant), a buildHeader headless guard,
and two end-to-end cli_run tests pinning the served-page meta at the binary boundary.

closes #108
closes #102
This commit is contained in:
2026-06-22 12:37:09 +02:00
parent 12bfa50a4f
commit 476342d7b1
6 changed files with 531 additions and 22 deletions
+61 -10
View File
@@ -65,6 +65,7 @@ const GRAPH_HEAD: &str = r#"<header>
const CHART_HEAD: &str = r#"<header>
<b>aura chart</b>
<span class="sub">timestamp-aligned trace series</span>
<span id="ctx" class="sub"></span>
<button id="xmode-toggle" type="button">x: collapsed (gaps off)</button>
</header>
<div id="stage"></div>
@@ -132,14 +133,49 @@ pub struct Series {
pub points: Vec<Option<f64>>,
}
/// The serve-ready chart payload: the shared timestamp axis and the per-(tap,column)
/// series. These are exactly the fields `chart-viewer.js` reads; injected verbatim
/// as `window.AURA_TRACES` (plus the `mode`). The run's manifest stays on disk in the
/// trace store's `index.json`; it is not carried into the served page, which renders
/// only the series, so nothing here is dead injected payload.
/// Run-context for the page header (#102): serialized into `window.AURA_TRACES.meta`
/// and rendered client-side by `chart-viewer.js`'s pure `buildHeader`. A single run
/// carries its manifest fields + bound params; a family carries its shared identity
/// (commit/broker — one frozen artifact, one broker profile) plus the SPANNING
/// window across its members (see the `window` field) + member count — per-member
/// identity is the series label (`member.key`), not repeated here.
#[derive(Default, serde::Serialize)]
pub struct ChartMeta {
/// "run" | "family".
pub kind: String,
/// The chart name (run name or family id).
pub name: String,
/// Git commit of the run's frozen artifact (C18); `buildHeader` shows the short form.
pub commit: String,
/// Inclusive data window `(from, to)` epoch-ns. For a single run, the run's
/// manifest window. For a family, the SPAN across all members —
/// `(min member.from, max member.to)` — NOT the first member's window: a
/// walk-forward family's members are disjoint OOS windows, so only the span
/// labels the family's true coverage (and for sweep/MC, whose members share a
/// window, the span collapses to it).
pub window: (i64, i64),
/// Broker profile label, e.g. "sim-optimal(pip_size=1)".
pub broker: String,
/// RNG seed (0 for a seed-free synthetic run).
pub seed: u64,
/// The charted taps (single run: all charted taps or the `--tap` pick; family:
/// the one compared tap).
pub taps: Vec<String>,
/// Family member count; `None` for a single run.
pub members: Option<usize>,
/// Bound params (single run only; empty for a family, where params are the
/// per-member series labels). Stringified `name -> display` for the header.
pub params: Vec<(String, String)>,
}
/// The serve-ready chart payload: the shared timestamp axis, the per-(tap,column)
/// series, and the run-context `meta`. These are exactly the fields `chart-viewer.js`
/// reads; injected verbatim as `window.AURA_TRACES` (plus the `mode`). `meta` carries
/// the manifest context for the header (#102); the full manifest stays on disk.
pub struct ChartData {
pub xs: Vec<i64>,
pub series: Vec<Series>,
pub meta: ChartMeta,
}
/// Assemble the self-contained uPlot chart page for one run's aligned traces.
@@ -155,6 +191,7 @@ pub fn render_chart_html(data: &ChartData, mode: ChartMode) -> String {
"mode": mode_str,
"xs": data.xs,
"series": data.series,
"meta": data.meta,
})
.to_string();
@@ -219,6 +256,17 @@ mod tests {
Series { name: "equity".into(), y_scale_id: "y_0".into(), points: vec![Some(0.0), Some(0.001), Some(0.004)] },
Series { name: "exposure".into(), y_scale_id: "y_1".into(), points: vec![Some(1.0), Some(1.0), Some(-1.0)] },
],
meta: ChartMeta {
kind: "run".into(),
name: "sample".into(),
commit: "abc1234def".into(),
window: (1, 3),
broker: "sim-optimal(pip_size=1)".into(),
seed: 0,
taps: vec!["equity".into(), "exposure".into()],
members: None,
params: vec![],
},
}
}
@@ -252,13 +300,16 @@ mod tests {
assert!(!html.contains("hover · [+] expand · body drill"), "graph breadcrumb leaked onto the chart page");
}
/// `window.AURA_TRACES` carries only the fields `chart-viewer.js` reads
/// (`mode`, `xs`, `series`) — no `manifest`/`taps` injected-but-unread payload.
/// `window.AURA_TRACES.meta` now carries the run-context the header reads
/// (#102): the run's commit / window / broker / taps, threaded from the
/// manifest (this flipped the earlier "manifest is NOT carried" pin).
#[test]
fn render_chart_html_injects_only_what_the_viewer_reads() {
fn render_chart_html_injects_run_context() {
let html = render_chart_html(&sample_chart_data(), ChartMode::Overlay);
assert!(!html.contains("\"manifest\""), "unread manifest carried into the served chart");
assert!(!html.contains("\"taps\""), "unread taps list carried into the served chart");
assert!(html.contains("\"meta\""), "run-context meta not injected");
assert!(html.contains("abc1234def"), "manifest commit not injected");
assert!(html.contains("sim-optimal(pip_size=1)"), "manifest broker not injected");
assert!(html.contains("\"kind\":\"run\""), "meta kind not injected");
}
#[test]