upstart_reporter — HTML broadsheet report

Purpose

Renders one self-contained Plotly HTML “broadsheet” (newspaper front page) from REAL pipeline data: overlaid time-series figures, the pipeline’s reduction tables, and an agent-written narrative. The HTML successor to star_reporter (LaTeX, kept for other run-specs); replaced the checkin PDF flow (built 2026-06-28).

Role in the system

  • Sits beside star_reporter at the presentation end of the pipeline: run → log → analyze → report. Driven by the log-driver skill (on-demand log selection, agent-written summary, second-agent verify).
  • Calls the three orchestrator stages directly — orch.analyze(orch.run_or_load_logs(orch.build_run_contexts())) — instead of orch.run(), so the matplotlib-PNG (plotter) and LaTeX stages never execute and no side-effect artifacts land.
  • Never recomputes a metric. Every trace is the pipeline’s own series via orch.data_summary.signal_analysis(item.view).metric_time_series(key, use_norm=True) (data_analyzer); tables are analysis.table_frames verbatim; “Measured highlights” lines are analysis.conclusions.
  • Imports from the homes (analysis.orchestrator, utils.infra). The former validation/signal_measurement.py facade is retired (2026-07-01, file gone); this module never routed through it.
  • Python emits no prose: the narrative slots are filled by a summary markdown the driving agent writes (checkin discipline).

Inputs / Outputs

  • In: a run-spec stem (the comparison.overlay block defines which metrics plot), optionally an agent-written summary md (# → headline, > → dek, ## → section), optionally an explicit out path.
  • Out: one report.html (Plotly via CDN, loaded once on the hero figure). Default path is derived from the orchestrator: orch.report_dir / orch.run_stamp / "report.html"logs/logs_<date>/<safe_stem>/reports/run_PMHHMM/report.html.

Key methods / functions

  • build(stem, summary, summary_md, out) — the entry point: pack + write — analysis/upstart_reporter.py:433
  • Broadsheet.html / Broadsheet.write — assemble the page from the design-token Template; write it — :85 / :103
  • _pack — drive load+analyze and pack a Broadsheet; raises if the overlay block defines no metrics — :376
  • _metrics / _variant — overlay keys (+ secondary_keys) from the run-spec; per-variant series through the pipeline — :350 / :366
  • decimate_for_display — envelope-preserving min/max display decimation (figures only) — :129
  • summary_from_md — parse the agent summary md into Summary:396
  • demo — self-checking smoke on real logs (checkin_Jun25_26): page well-formed, summary parse, decimation budget + spike survival — :446

CLI: python -m analysis.upstart_reporter <stem> [summary.md]; no args runs demo().

Footguns

The data seam is Variant(label, t, series) — fill it only from the pipeline

The reporter was thrown out twice for the same mistake: fabricating that shape with a synthetic _synth(), then dodging the Write(analysis/**/*.py) deny with a scratch file + staged-mv. The sanctioned door was Edit (no Edit deny exists on analysis/), and the only legitimate filler is _variant reading the orchestrator’s own overlay extraction. (notes/upstart_reporter_footguns.md)

Figures are decimated; tables and metrics are not

decimate_for_display keeps each bucket’s min and max sample plus the endpoints (budget MAX_PLOT_POINTS = 2000 per trace, :126) — plain striding aliases away exactly the spikes a check-in exists to show. Added 2026-07-04: full-resolution traces made the Jun21–23 six-run page 34 MiB against the Cloudflare Pages 25 MiB file cap. Series at or under 2 * max_points pass through untouched. Smoke: _heredocs/test_decimation.py + the demo asserts.

"Measured highlights" is silent for comparison reports — by design

analysis.conclusions is empty for an A/B run: the orchestrator’s conclusion lines only emit for frames with a single value column (single-run tables), and comparison frames are pivoted with variants as columns. The reduction tables carry the numbers; _highlights renders nothing when there are no lines. (notes/upstart_reporter_footguns.md)

Ask the orchestrator where the report goes

The output path is derived (orch.report_dir / orch.run_stamp), never hardcoded — that keeps the HTML co-located with the run’s figures/npz by construction. The stem is lower-cased by safe_filename on the way in.

Legend-click trace toggling is free

Plotly hides a trace on legend click by default — the “click a line to remove it” spec needs zero custom JS. Horizontal legend configured in _layout (:113).

Pseudocode

build(stem, summary_md):
  orch     = Orchestrator(stem)
  analysis = orch.analyze(orch.run_or_load_logs(orch.build_run_contexts()))  # load+analyze ONLY
  metrics  = overlay keys from the run-spec comparison block
  variants = per item: signal_analysis(item.view).metric_time_series(key, use_norm=True)
  tables   = [(title, frame) for frame in analysis.table_frames]             # pipeline reductions
  summary  = summary_from_md(summary_md)      # '# ' headline, '> ' dek, '## ' sections
  each figure: decimate_for_display(t, y) -> one go.Scatter per variant, shared axis
  write orch.report_dir / orch.run_stamp / "report.html"

orchestrator · data_analyzer · star_reporter · plotter · logger · terminology