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:
@@ -0,0 +1,58 @@
|
||||
// Headless guard for the run-context header builder (chart-viewer.js buildHeader).
|
||||
// Property protected: buildHeader maps an AURA_TRACES.meta object to header chips
|
||||
// WITHOUT a DOM — a run shows name/tap/broker/window/commit chips; a family adds a
|
||||
// members chip; absent meta yields []. Loads the real viewer module and drives the
|
||||
// exported pure buildHeader; mount() (browser-only) is never called.
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const fail = (msg) => { console.error("FAIL: " + msg); process.exit(1); };
|
||||
const has = (chips, label, needle, msg) => {
|
||||
const c = chips.find((c) => c.label === label);
|
||||
if (!c) fail(msg + " (no '" + label + "' chip)");
|
||||
if (needle != null && String(c.value).indexOf(needle) < 0) fail(msg + " (got " + c.value + ")");
|
||||
};
|
||||
|
||||
// No `document` global -> chart-viewer.js's browser mount is skipped; require gives
|
||||
// only the pure export (mirrors chart_viewer_build.mjs stubbing window before require).
|
||||
global.window = {};
|
||||
const { buildHeader } = require(join(here, "..", "assets", "chart-viewer.js"));
|
||||
|
||||
// run meta -> name / tap / broker / window / commit chips; no members chip
|
||||
const run = {
|
||||
kind: "run", name: "eur-demo", commit: "abc1234def567",
|
||||
window: [1704067200000000000, 1735689600000000000], broker: "sim-optimal(pip_size=0.0001)",
|
||||
seed: 0, taps: ["equity", "exposure"], members: null, params: [["len", "20"]],
|
||||
};
|
||||
const rc = buildHeader(run);
|
||||
has(rc, "run", "eur-demo", "run name chip");
|
||||
has(rc, "tap", "equity", "tap chip");
|
||||
has(rc, "broker", "sim-optimal", "broker chip");
|
||||
has(rc, "window", "2024", "window chip (formatted day)");
|
||||
has(rc, "commit", "abc1234", "commit chip (shortened)");
|
||||
if (rc.find((c) => c.label === "members")) fail("a single run must not carry a members chip");
|
||||
|
||||
// family meta -> adds a members chip, name label is 'family', and renders its
|
||||
// SPANNING window. The window models a walk-forward family's full OOS coverage
|
||||
// (built Rust-side as (min member.from, max member.to)); the rendered chip must
|
||||
// show that full span's endpoints, not a single member's window.
|
||||
const fam = {
|
||||
kind: "family", name: "ger40-5y", commit: "deadbeef",
|
||||
window: [1577836800000000000, 1735689600000000000], // 2020-01-01 -> 2025-01-01
|
||||
broker: "sim-optimal(pip_size=1)", seed: 0, taps: ["equity"], members: 4, params: [],
|
||||
};
|
||||
const fc = buildHeader(fam);
|
||||
has(fc, "family", "ger40-5y", "family name chip");
|
||||
has(fc, "members", "4", "family members chip");
|
||||
has(fc, "window", "2020", "family window chip (span start)");
|
||||
has(fc, "window", "2025", "family window chip (span end)");
|
||||
|
||||
// absent meta -> []
|
||||
if (buildHeader(undefined).length !== 0) fail("absent meta must yield no chips");
|
||||
|
||||
console.log("OK — buildHeader run/family/empty chips.");
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Integration guard for the `aura chart` run-context header builder.
|
||||
//!
|
||||
//! `chart-viewer.js`'s `buildHeader` maps `AURA_TRACES.meta` to header chips in
|
||||
//! JavaScript, so the property "buildHeader yields the right run/family chips"
|
||||
//! cannot be checked from Rust. This shells out to `node`, running
|
||||
//! `tests/chart_viewer_header.mjs`, which loads the real viewer module and drives
|
||||
//! the exported pure `buildHeader`. `node` is REQUIRED: if absent this test FAILS.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn chart_viewer_builds_run_context_header() {
|
||||
let script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("chart_viewer_header.mjs");
|
||||
assert!(script.exists(), "guard script missing at {}", script.display());
|
||||
|
||||
let out = match Command::new("node").arg(&script).output() {
|
||||
Ok(out) => out,
|
||||
Err(e) => panic!(
|
||||
"node is required for the chart-viewer header guard but could not be run \
|
||||
({e}); install Node.js or ensure `node` is on PATH"
|
||||
),
|
||||
};
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"chart-viewer header guard failed (exit {:?}).\n--- node stdout ---\n{stdout}\n--- node stderr ---\n{stderr}",
|
||||
out.status.code()
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user