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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user