Sverka
Reference

Run reporting (@sverka/reporter)

`@sverka/reporter` turns an engine `RunEvent` stream into human output: a vitest-style text renderer for the terminal, a self-contained HTML run report, and an

@sverka/reporter turns an engine RunEvent stream into human output: a vitest-style text renderer for the terminal, a self-contained HTML run report, and an interactive Ink TUI — plus the pure plumbing they share (event reducer, findings collector, policy gate, DAG layout). It is what sverka run uses for --format text|html and --tui (Specs 43–45, 51).

engine RunEvents ──→ EventReducer (pure) ──→ UIState
                                               │
SARIF artifacts ──→ collectFindings ──→ FindingRow[]
                                               │
findings + policy ──→ evaluateGate ──→ verdict + exit code
                                               │
            ┌──────────────┬───────────────────┴──┐
            ▼              ▼                      ▼
     TextRenderer    HtmlRenderer            InkRenderer
     (stdout)        (--output, default     (opt-in TUI)
                      .sverka/report.html)

HtmlRenderer is the explicit --format html report — it is not the per-run .sverka/runs/<runId>/report.html: that one is written best-effort by generateSarifHtml (@sverka/sarif-viewer-web, see SARIF pipeline) alongside report.json on every run, relocatable with --report.

The Renderer contract

Every format implements one interface — events stream in during the run, findings and the verdict arrive after it:

interface Renderer {
  onEvent(event: RunEvent): void; // per engine event
  onFindings(findings: readonly Finding[]): void; // after --evaluate
  onVerdict(result: PolicyResult): void; // after policy eval
  flush(): void; // final write — file renderers emit here
}

The reducer (pure)

reduceEvent(state, event) folds a RunEvent into an immutable UIState; createInitialState() starts empty. UIState holds runId/planId, run status, durationMs, a Map<stepId, StepUIState> (state, duration, error, retry attempt), and a DiagnosticEntry[] log. Because it's pure and total — it only consumes existing event types — the same state drives text, HTML, and TUI without per-format bookkeeping.

Collecting findings

collectFindings scans per-step SARIF artifacts and attributes each finding to the step that produced it:

import { collectFindings } from "@sverka/reporter";

const rows = await collectFindings({
  artifactDir: ".sverka/artifacts",
  runId, // scan only <artifactDir>/<runId>/ — preferred, exact scope
});
// rows: FindingRow[] = { finding, stepId }[]

Artifacts live at <artifactDir>/<runId>/<stepId>/<name>.sarif; the runId scope is the identity boundary (a sinceMs fallback exists for legacy, non-run-keyed trees). Non-SARIF files are ignored; unparseable SARIF throws ReporterError(COLLECTION_FAILED).

The policy gate

evaluateGate wraps evaluatePolicy (@sverka/verification) for CLI use — it supplies DEFAULT_POLICY when none is given and maps the verdict to an exit code:

import { evaluateGate } from "@sverka/reporter";

const { result, exitCode } = evaluateGate({
  findings,
  baselineFingerprints, // optional — onlyNew filtering is per-rule
});
// exitCode: 0 pass, 1 fail

Text output (default)

createTextRenderer({ writer, color?, stepOutputLines? }) prints vitest-style status lines as events arrive — ○ pending, ▶ running, ✓ succeeded, ✗ failed, ⊘ skipped — with durations and the tail of each step's captured stdout/stderr (default last 20 lines, sanitized of terminal escapes). After the run: a findings summary by severity and the policy verdict. color should come from stdout.isTTY (the CLI honors NO_COLOR/TERM=dumb).

HTML report

createHtmlRenderer({ outputPath, graph?, context? }) buffers events and writes one self-contained HTML file on flush() — inline CSS/JS, no CDN, works offline and under CSP. Per Spec 51 the report carries:

  • Context header — ReportContext assembled by the caller (title, generated-at, command line, meta rows like branch/cwd, clickable links like repo/commit/CI-run URLs). The renderer escapes values and only renders http(s) links — it never derives URLs itself.
  • Step views — a segmented Gantt | DAG | Tree | List switch, all pre-rendered at generation time: an SVG waterfall showing real parallelism, a status-colored static SVG of the dependency graph, a nested tree, and per-step rows with expandable stdout/stderr tails (byte-capped at 64 KiB).
  • Findings table — severity filter, sortable columns, search; and every step element is clickable to filter findings to that step.
  • Verdict banner — pass/fail with the policy summary.
  • Layout toggle — Stacked | Split for the steps+findings area.

The DAG view positions nodes with layoutDag — dagre (Sugiyama) under the hood, direction: "LR" (default) or "TB":

import { layoutDag } from "@sverka/reporter";

const { nodes, edges } = layoutDag(graph, { direction: "TB" });
// DagNode: { id, label, x, y, layer } · DagEdge: { source, target, label? }

Interactive TUI

createInkRenderer(options?) renders the run live — a DAG tree of per-step glyphs, a filterable findings list, and a verdict footer. The pure half is exported for reuse: stepGlyph(state) maps StepState → glyph+color, buildStepTree(graph, state) flattens the graph into deterministic StepTreeRow[], and filterFindings/searchFindings drive the FINDING_FILTERS bar (all|critical|high|medium|low|new|error).

import { createInkRenderer } from "@sverka/reporter";

const renderer = createInkRenderer({
  graph, // optional — flat list when absent
  baselineFingerprints, // enables the [new] filter
});
// renderer implements Renderer + a promise that resolves when the
// user quits (q / Ctrl+C) — the CLI awaits it before exiting

Keybindings: j/k scroll, / search, f cycle filter, d step detail, q quit. interactive/debug/stdout/stdin options are testability seams — normally left unset.

CLI wiring

Flag / formatEffect
--format text (default)TextRenderer → stdout
--format jsonsverka.run/v1 payload → stdout + .sverka/runs/<id>/report.json
--format html / --output <path>HtmlRenderer → file (implies --evaluate)
--report <path>Relocate the per-run HTML report
--format sarif / webserializeSarif / generateSarifHtml (@sverka/verification, @sverka/sarif-viewer-web)
--evaluatecollectFindings + evaluateGate after the run
--tuiInkRenderer (opt-in; never auto-selected — text is always default)
--step-output-lines NText renderer tail length (0 disables)

Every run writes .sverka/runs/<runId>/report.json (schema sverka.run/v1, append-only) and report.html; human output ends with the report path. --watch re-runs reuse the same renderer pipeline.

Errors

ReporterError carries code: "COLLECTION_FAILED" | "RENDER_ERROR" with cause — collection failures are SARIF/IO problems, render errors are emission problems (e.g. unwritable outputPath).

API summary

ExportKind
reduceEvent, createInitialStatePure event → UIState reducer
collectFindingsSARIF artifacts → FindingRow[]
evaluateGateFindings + policy → verdict + exit code
createTextRendererVitest-style stdout renderer
layoutDagDefinitionGraph → positioned nodes
createHtmlRendererSelf-contained HTML report
stepGlyph, buildStepTree, filterFindings, searchFindings, FINDING_FILTERSPure TUI model
createInkRendererInteractive terminal renderer
ReporterError, ReporterErrorCodeErrors

Types: Renderer, UIState, StepUIState, StepState, DiagnosticEntry, FindingRow, FindingsCollectorOptions, PolicyGateOptions, PolicyGateResult, TextRendererOptions, TextWriter, DagNode, DagEdge, DagLayoutResult, DagLayoutOptions, HtmlRendererOptions, ReportContext, FindingFilter, StepGlyph, StepTreeRow, InkRenderer, InkRendererOptions.

See also

  • CLI — sverka run flags end to end
  • SARIF pipeline — standalone SARIF viewers (sverka view) the report formats share normalization with
  • Policy, Findings — what the gate and collector wrap

On this page