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
+74
View File
@@ -1377,3 +1377,77 @@ fn chart_family_with_tap_overlays_the_named_tap_per_member() {
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#102, single-run run-context reaches the served page): `aura chart
/// <run>` injects the run's `RunManifest` context into `window.AURA_TRACES.meta`
/// at the binary boundary — `kind:"run"`, the chart name, the manifest broker and
/// data window, the charted taps, and `members:null` (a single run carries no
/// member count). The render-layer unit (`render_chart_html_injects_run_context`)
/// proves the struct serializes, but only an end-to-end run pins that the
/// *built-and-read-back* manifest threads through `build_chart_data` into the
/// served HTML; a regression dropping the `meta` build (charting bare series with a
/// `ChartMeta::default()`) would leave every render/unit test green while serving a
/// header-less page. Asserts on the stable manifest fields (the synthetic window is
/// `[1,7]`, the broker is the FX sim-optimal label, kind/taps/members are fixed),
/// NEVER on the volatile `commit` (the dirty git HEAD), so it stays deterministic.
#[test]
fn chart_run_serves_manifest_run_context_in_meta() {
let cwd = temp_cwd("chart-run-meta");
let traced = Command::new(BIN).args(["run", "--trace", "demo"]).current_dir(&cwd).output().expect("spawn run --trace");
assert!(traced.status.success(), "run --trace exit: {:?}", traced.status);
let chart = Command::new(BIN).args(["chart", "demo"]).current_dir(&cwd).output().expect("spawn chart");
assert!(chart.status.success(), "chart exit: {:?}", chart.status);
let html = String::from_utf8(chart.stdout).expect("utf-8 html");
// The run-context meta is injected, tagged as a single run named `demo`.
assert!(html.contains("\"meta\":{"), "run-context meta not injected into the served page: {html:?}");
assert!(html.contains("\"kind\":\"run\""), "served meta must be kind run");
assert!(html.contains("\"name\":\"demo\""), "served meta must carry the run name");
// The manifest's broker + data window thread through to the served page.
assert!(html.contains("\"broker\":\"sim-optimal(pip_size=0.0001)\""), "manifest broker missing from served meta");
assert!(html.contains("\"window\":[1,7]"), "manifest window missing from served meta");
// The charted taps + the single-run sentinel.
assert!(html.contains("\"taps\":[\"equity\",\"exposure\"]"), "charted taps missing from served meta");
assert!(html.contains("\"members\":null"), "a single run must carry no member count");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#102, family run-context + SPANNING window at the served page): `aura
/// chart <family>` injects family-shaped `meta` — `kind:"family"`, a `members`
/// count, the one compared `taps`, and crucially a `window` that is the SPAN across
/// all members, NOT a single member's window. The built-in sweep grid has 4
/// members, each over the synthetic `[1,7]` window, but the family's recorded
/// timestamps union to a wider `[1,18]` span; the served `window` must report that
/// span (the ledger's families-comparison invariant: only the span labels a
/// walk-forward family's true OOS coverage, and for sweep/MC it is still the union
/// of member windows). A regression that took the first member's window, or dropped
/// the family `members` count, would still pass the family-overlay series test
/// (which only checks series labels); this pins the header context. Deterministic:
/// asserts the fixed kind/members/taps/span, never the volatile commit.
#[test]
fn chart_family_serves_member_count_and_spanning_window_in_meta() {
let cwd = temp_cwd("chart-family-meta");
let swept = Command::new(BIN).args(["sweep", "--trace", "fam"]).current_dir(&cwd).output().expect("spawn sweep --trace");
assert!(swept.status.success(), "sweep --trace exit: {:?}", swept.status);
let chart = Command::new(BIN).args(["chart", "fam"]).current_dir(&cwd).output().expect("spawn chart fam");
assert!(chart.status.success(), "chart family exit: {:?}", chart.status);
let html = String::from_utf8(chart.stdout).expect("utf-8 html");
assert!(html.contains("\"meta\":{"), "family run-context meta not injected: {html:?}");
assert!(html.contains("\"kind\":\"family\""), "served meta must be kind family");
assert!(html.contains("\"name\":\"fam\""), "served meta must carry the family name");
// The 4-point built-in grid -> a member count of 4 (the single-run sentinel is gone).
assert!(html.contains("\"members\":4"), "family meta must carry the member count, got: {html}");
assert!(!html.contains("\"members\":null"), "a family must not carry the single-run null sentinel");
// The default compared tap (equity), one entry — not the per-member labels.
assert!(html.contains("\"taps\":[\"equity\"]"), "family meta must carry the single compared tap");
// The SPANNING window across members ([1,18]), wider than any single member's [1,7].
assert!(html.contains("\"window\":[1,18]"), "family meta must carry the SPAN across members, not one member's window; got: {html}");
let _ = std::fs::remove_dir_all(&cwd);
}