Report

The report is a self-contained HTML view of every fit, sim, graph, and yagni run on the local machine. No server, no database, no asset hosting — a single file you can email or commit, fully functional offline.

What you'll understand after this:

- When the report opens automatically vs. manually.

- What the HTML report contains (the top-level tabs and their subtabs).

- How the static HTML is generated and how data flows in.

- Where the report renderer's source lives.


When it opens

Two triggers, both opt-in:

The launcher's decideReportOpen (packages/cli/src/open-report.ts) returns shouldOpen: true only when all of these hold:

The HTML file is always written. If any guard skips the browser launch, the user can navigate to it manually.


What it shows

Five top-level tabs (Overview, Fitness, Simulation, Code Graph, YAGNI). The Fitness and Simulation tabs each carry three subtabs (Sessions, Catalog, Recipes). The Code Graph (graph) tab carries four subtabs (Sessions, Catalog, Recipes, Explore). The YAGNI tab carries two subtabs (Sessions, Detectors). Browser panel modules live under packages/dashboard/src/client/; the top-of-page tool-tab switcher is registered through tool-tabs-registrations.ts and rendered by tool-tabs.ts.

Overview

The default landing panel. Shows:

Source: packages/dashboard/src/client/overview.ts.

Sessions list (per-tool Overview subtab)

A list of every past run, sorted reverse-chronological. Click into one to see its full detail — every check that ran, every finding, every directive applied, every check that was skipped or errored.

Per-run detail expands into a tree: check → file → finding. Each finding shows the rule id, severity, line, and (when present) the suggestion text.

Source: packages/dashboard/src/client/sessions.ts. Rendered inside each per-tool tab's Overview subtab; the tab switcher is in tool-tabs.ts.

Catalog (per-tool Catalog subtab)

Every check that was registered for the current project, with per-check stats:

Filterable by tag, by source pack, by pass-rate. Useful for spotting the noisiest checks (high failure rate) and the dormant ones (haven't run in weeks — maybe a recipe drift).

Source: packages/dashboard/src/client/checks.ts.

Recipes (per-tool Recipes subtab)

The configured recipes, with per-recipe stats. Same shape as the catalog but a level up: how often each recipe has run, its pass rate, its average duration.

Source: packages/dashboard/src/client/recipes.ts.

Code Paths panel

The Code Paths panel is the dashboard's graph-tool surface. It's powered by the catalog produced by opensip graph. The pipeline that builds the underlying catalog is documented in 40-graph/01-stages-and-catalog.md.

Like the Fitness and Simulation tabs, the Code Paths tab carries subtabs:

The Explore subtab is language-agnostic — it consumes the shared Catalog

shape and works against TypeScript, Python, Rust, Go, and Java catalogs alike.

Per-edge confidence is carried on GraphCallEdge and is available to views;

today it's read but not surfaced as a UI badge, so reachability views on

tree-sitter catalogs look the same as TypeScript ones even though the underlying

edges are lower-fidelity. See the per-rule fidelity table in

02-rules-and-gating.md for what this

means in practice.

The Explore subtab has three views (each with the same row-click → universal Function Card flow):

dagre/cose/breadthfirst layouts), with SCC cycle highlighting folded in.

columns cover body length (lines, the default sort), inbound callers,

parameter count (width), and kind/package/file. It carries an in-table

name filter plus a Test-only toggle that narrows to production

functions reached only from tests. Paginated at 10 rows per page — every

function in the catalog (after filters) is reachable by paging.

The Universal Function Card is the cross-cutting drill-down: every clickable function name in any view opens the same overlay with name + location, body length, kind, params, return type, callers grouped by package, callees (resolved + external), an "Open in editor" deep link (vscode:// or cursor:// — opt in via dashboard.editor in opensip-cli.config.yml; falls back to "Copy path" when unset), and a "Trace from entry" BFS.

Filter chips apply across the Explore views: package multi-select, kind multi-select, and a production/test toggle (default: production-only).

Source: packages/dashboard/src/code-paths.ts and the per-view browser modules under packages/dashboard/src/client/ (view-graph.ts, view-coupling.ts, view-distribution.ts).

Tool tabs

The report supports fit, sim, graph, and yagni runs. The top-of-page tab switcher filters the panels by tool. Fit and sim use the shared Sessions/Catalog/Recipes shape, graph uses Code Graph with catalog exploration, and YAGNI uses Sessions/Detectors. Source: tool-tabs.ts and tool-tabs-registrations.ts.


How it's generated

Static HTML. The generator (packages/dashboard/src/generator.ts) assembles:

The output is one self-contained latest.html. No CDN, no external script tags, no fetch calls, no asset directory. You can save the file and open it in three weeks on a plane.

Why static, no server? A few reasons:

The cost: dynamic features (filtering, sorting, expand-collapse) are JS in the browser, against the embedded JSON. That works fine up to ~thousands of sessions; beyond that, the page is slow to load. Past a certain scale the right answer is a real backend; for the typical opensip-cli project (dozens of sessions per week), static HTML is plenty.


Extending the report renderer

The @opensip-cli/dashboard package exposes three contributor-facing seams. New

data, new ranked views, and new session-aware deep-link tabs each go

through one of them — none requires forking the generator or

sprinkling globals.

DashboardInput — the input contract

generateDashboardHtml({ … }) accepts a single options object; the

shape is the DashboardInput interface re-exported from

@opensip-cli/dashboard. Today it carries sessions,

checkCatalog, recipeCatalog, graph catalog/rule/recipe data,

simulation scenario/recipe data, YAGNI detector data, and

editorProtocol. Future tool-shaped data — alarm history,

dependency graphs, simulation traces — extends the interface as new

optional fields. Don't grow positional parameters; add a new

optional field to DashboardInput and surface it in the generator's

top-of-page <script> block via the existing

serializeOptionalBlob(id, value, kind) helper (in

packages/dashboard/src/generator.ts).

defineRankedView — the rank-and-render skeleton

The ranked Functions view in Code Paths is built on a

rank-and-render skeleton: walk indexes.byBodyHash.values(), apply

chip filters and an optional view-specific predicate, compute a

numeric metric, sort descending, and hand the result to

renderFunctionRows. That skeleton lives in

client/view-template.ts;

the view file is declarative config (id, label, help, metric,

optional predicate / preamble / searchByName / filterToggle,

columns, headingText, emptyMessage). The Functions view uses this

skeleton with sortable columns for the common size, caller, width, and

test-reachability questions.

A new ranked view that fits this shape is one config and one

registration in code-paths.ts.

Bespoke views (Graph, Coupling) have different shapes and

keep their own emitters.

registerTabActivator — session-aware tab navigation

The Overview tab's row-click handler routes by session.tool. For

tabs that need session-aware behavior (jumping to a specific row,

selecting a subtab, scrolling into view), register an activator

into the shared tabActivators map at module init:

// inside dashboardCodePathsJs() or any future tab's emitter
if (typeof registerTabActivator === 'function') {
  registerTabActivator('graph', openCodePathsSession);
}

The Overview row click then calls activateTabForSession(session);

if a matching activator exists, it runs and the default top-level

tab switch is suppressed. code-paths.ts is the worked example.

New session-aware tabs (fit, sim detail panels, etc.) plug in

the same way — the registry decouples Overview from "tab X happens

to be loaded into this page".

The registry helpers (registerTabActivator,

activateTabForSession) are declared in

tab-activators.ts and

are available wherever any tab JS runs.


Where it lives

<project>/opensip-cli/.runtime/reports/latest.html

Single rolling file. Each generation overwrites the previous file — the dashboard is "show me the most recent state of the project", not a per-run archive. Per-run history lives in the SQLite session store (.runtime/datastore.sqlite, read via SessionRepo); the Sessions panel inlines the most recent 20 sessions (new SessionRepo(datastore).list({ limit: 20 }) in packages/cli/src/report-compose.ts) so historical runs are browsable inside the HTML up to that bound. Older sessions stay in the store until you run sessions purge.

The HTML file is fully self-contained — no asset directory, no CDN, no fetches. Email a stakeholder the file and they can open it on their machine without opensip-cli installed. Useful for: post-incident reports, security review handoffs, compliance audits.

The runtime dir is gitignored. If you want to archive a specific snapshot, copy latest.html somewhere else before re-running.


What the dashboard isn't

A few common mis-expectations, listed once:


Where the example lands

For acme-api after the nightly CI run:

In CI, --open is suppressed (no TTY), so no browser opens — but the HTML file is still written. Teams that want a per-run archive copy latest.html to a build-artifact path with a run-scoped filename before the next pipeline run overwrites it.


What's next