Reports
A verdict, summary metrics, grouped findings and next steps – rendered compact or detailed from your own data.
Building blocks
| Part | Rust | Node (ReportInput) |
|---|---|---|
| Title and verdict | Report::new(title, Verdict) |
title, verdict |
| Summary metrics, optional trend and verdict | Metric::new(key, value).with_trend(Trend, delta).with_verdict(Verdict) |
metrics: [{ key, value, verdict, tone, trend, delta }] |
| Finding groups | FindingGroup::new(title).add_finding(…) |
groups: [{ title, findings, totalCount, advisory }] |
| Findings | Finding::new(Tone, message) with rule ID, location, remedy, confidence, badge |
{ message, tone, ruleId, location, remedy, confidence, badge } |
| Next steps | NextStep::new(text).with_command(cmd) |
nextSteps: [{ text, command }] |
| Scope notes | Report::add_scope_note(ScopeNote::new(heading, items)) |
scopeNotes: [{ heading, items }] |
| Status notes | Report::add_vocabulary(Vocabulary) |
vocabulary: [{ kind, value }] |
| Compact samples per group | report.max_compact_samples = 5 |
maxCompactSamples |
| Top issue rules threshold | report.top_issues_threshold = Some(50) |
topIssuesThreshold |
| Overflow hint | Report::with_details_hint(Some(hint)) |
detailsHint |
Verdicts: passed, warning, failed, action-required, skipped, info.
Example
use runemark::{
ColorMode, Console, Finding, FindingGroup, Location, Metric, NextStep, Report, Tone, Trend,
Verdict,
};
let report = Report::new("Site audit v1.2.0", Verdict::Warning)
.add_metric(Metric::new("Warnings", "4").with_trend(Trend::Negative, "+2"))
.add_group(
FindingGroup::new("Accessibility violations").add_finding(
Finding::new(Tone::Warning, "Image missing alt attribute")
.with_rule_id("a11y/img-alt")
.with_location(Location::file_line_col("src/pages/index.astro", 42, 10))
.with_remedy("Add alt=\"…\" to the <img> tag"),
),
)
.add_next_step(NextStep::new("Run the auto-fixer").with_command("audit --fix"));
print!("{}", report.render(Console::stdout(ColorMode::Auto)));const { renderReport } = require('@casoon/runemark')
process.stdout.write(renderReport({
schemaVersion: 1,
title: 'Site audit v1.2.0',
verdict: 'warning',
metrics: [{ key: 'Warnings', value: '4', trend: 'negative', delta: '+2' }],
groups: [{
title: 'Accessibility violations',
findings: [{
message: 'Image missing alt attribute',
tone: 'warning',
ruleId: 'a11y/img-alt',
location: { kind: 'file', value: 'src/pages/index.astro', line: 42, column: 10 },
remedy: 'Add alt="…" to the <img> tag',
}],
}],
nextSteps: [{ text: 'Run the auto-fixer', command: 'audit --fix' }],
}, { width: process.stdout.columns }))The rendered result, in colour, is in the showcase.
Compact and detailed
The compact form is a decision view: verdict, metrics and a limited number of sample findings per group. The detailed form lists every provided finding with its rule ID, location and remedy.
- Rust:
report.with_detail_level(DetailLevel::Detailed) - Node:
detailLevel: 'detailed'
The compact form shows three findings per group. Report::max_compact_samples (a public field,
there is no builder) and maxCompactSamples in Node change that number.
When a group has more findings than the compact form shows, or its totalCount is larger than the
findings you pass, the report says so instead of implying a complete list:
... 23 more finding(s) (use --details to view all). The detailed form lists every finding you
passed; if totalCount is larger, it says ... N finding(s) not included in the report data.
The parenthetical in the compact line is the details hint. Rust sets it with
Report::with_details_hint(Some("see the full log".to_string())), and with_details_hint(None)
drops it. Node uses detailsHint: 'see the full log', or detailsHint: false to drop it. Change
it when your CLI’s flag is not called --details.
use runemark::{FindingGroup, Report, Verdict};
let report = Report::new("Site audit", Verdict::Warning)
.with_details_hint(Some("run with --verbose".to_string()))
.add_group(FindingGroup::new("Accessibility").with_total_count(40));
Top issue rules
Once a report carries 20 findings or more across its groups (counting each group’s totalCount),
a “Top issue rules” block lists the five most frequent rule IDs with their counts, ahead of the
groups:
Top issue rules:
16x a11y/contrast
9x a11y/img-alt
Only findings you pass with a rule ID are counted, so with a bounded sample the numbers describe
the sample. The block is left out when no finding has a rule ID. Change the threshold with
report.top_issues_threshold = Some(50) in Rust (None turns the block off), or
topIssuesThreshold: 50 in Node (false turns it off).
use runemark::{Report, Verdict};
let mut report = Report::new("Site audit", Verdict::Warning);
report.top_issues_threshold = None;
Notes, badges and confidence
Beyond metrics and findings, a report can carry short notes that qualify how far to trust it. They are plain data you pass in; runemark decides where they appear.
- Status notes (
Vocabulary) are shown on one line under the verdict, separated by•:DryRunrenders[DRY-RUN],Baseline("main")rendersBaseline: main,Truncated(100)rendersTruncated at 100, andEmptyState("No fixes needed")renders the text as given. - Scope notes (
ScopeNote) say what the run did not cover. Each renders asHeading: item, itemunder the verdict. - Badges (
Badge) are a short label in front of a finding, rendered as[QUICK WIN].Badge::quick_win(),gained()andlost()are presets;Badge::new(label, tone)makes any other. Node has no presets: pass{ label: 'QUICK WIN', tone: 'success' }. - Confidence (
Confidence::High,Medium,Low) renders as(high confidence)after the rule ID. - Advisory groups (
FindingGroup::with_advisory(true),advisory: true) mark a group as informational; its heading gets an[advisory]suffix.
use runemark::{
Badge, Confidence, Finding, FindingGroup, Report, ScopeNote, Tone, Verdict, Vocabulary,
};
let report = Report::new("Site audit", Verdict::Warning)
.add_vocabulary(Vocabulary::DryRun)
.add_vocabulary(Vocabulary::Baseline("main".to_string()))
.add_scope_note(ScopeNote::new(
"Scope warnings",
vec!["History partial".to_string(), "1 file skipped".to_string()],
))
.add_group(
FindingGroup::new("Accessibility")
.with_advisory(true)
.add_finding(
Finding::new(Tone::Warning, "Image missing alt attribute")
.with_rule_id("a11y/img-alt")
.with_badge(Badge::quick_win())
.with_confidence(Confidence::High),
),
);const { renderReport } = require('@casoon/runemark')
process.stdout.write(renderReport({
schemaVersion: 1,
title: 'Site audit',
verdict: 'warning',
vocabulary: [{ kind: 'dry-run' }, { kind: 'baseline', value: 'main' }],
scopeNotes: [{ heading: 'Scope warnings', items: ['History partial', '1 file skipped'] }],
groups: [{
title: 'Accessibility',
advisory: true,
findings: [{
message: 'Image missing alt attribute',
tone: 'warning',
ruleId: 'a11y/img-alt',
badge: { label: 'QUICK WIN', tone: 'success' },
confidence: 'high',
}],
}],
}))In Node, vocabulary is a tagged union on kind: 'dry-run' takes no value; 'empty-state',
'baseline' and 'truncated' require one (an integer for 'truncated'). Without colour, both
samples render as:
[WARN] Site audit
[DRY-RUN] • Baseline: main
Scope warnings: History partial, 1 file skipped
* Accessibility (1) [advisory]
- [QUICK WIN] Image missing alt attribute [a11y/img-alt] (high confidence)
Locations
Locations are files (with optional line and column), URLs, selectors or artifacts. In terminals
that support OSC 8, http and https URLs become clickable links. File locations become
file:// links too, with the line and column as a #line:col fragment. The link target is the
absolute path: runemark canonicalises the path you passed (or joins it to the working directory
when it cannot), so the escape sequence carries the local directory layout even though the visible
text is the path as given. Selectors and artifacts are never links.
Links follow the colour decision and are off in plain or redirected output. Turn them off
explicitly with Console::with_hyperlinks(false) in Rust or hyperlinks: false in Node. See
Security for how targets are validated.
Metrics as outcomes
Metric::with_verdict renders the verdict’s symbol in front of the metric, and supplies its
tone where none was set.
use runemark::{Metric, Verdict};
let metric = Metric::new("Tests", "1.2s").with_verdict(Verdict::Failed);
In Node the field is verdict on the metric, with the same accepted strings as the report’s:
const { renderReport } = require('@casoon/runemark')
process.stdout.write(renderReport({
schemaVersion: 1,
title: 'Build',
verdict: 'failed',
metrics: [{ key: 'Tests', value: '1.2s', verdict: 'failed' }],
}))
Use it whenever a metric answers “did this pass” rather than only “how many”. A tone alone
disappears with colour: piped, redirected, or under NO_COLOR, a failing metric reads exactly
like a passing one. The symbol survives all three, and follows the console’s theme — ✖ where
symbols are drawn, [FAIL] where they are not, which is the same condition.
An explicit with_tone still wins, in whichever order the two are called. The symbol counts
towards the width that decides whether metrics stack, since an ASCII [WARN] is six columns.
Writing to a stream
Every renderer has a writer-first method next to the String one, so output can go straight to
stdout, stderr or a file without building an intermediate string. They take the console and any
std::io::Write, and return std::io::Result<()>:
| Item | String |
Writer |
|---|---|---|
Report |
render(console) |
write_to(console, &mut writer) |
Report with width |
render_with_options(console, options) |
write_with_options(console, options, &mut writer) |
ErrorBlock, DiffBlock, Location, Badge |
render(console) |
write_to(console, &mut writer) |
Verdict |
render(console, message) |
write_to(console, message, &mut writer) |
Console |
paint(tone, value) |
write_paint(tone, value, &mut writer) |
use runemark::{ColorMode, Console, Report, Verdict};
fn print_report(report: &Report) -> std::io::Result<()> {
let console = Console::stdout(ColorMode::Auto);
report.write_to(console, &mut std::io::stdout().lock())
}
let report = Report::new("Site audit", Verdict::Passed);
print_report(&report).unwrap();
The output is identical to the String form. Node has no writer-first API: rendering returns
strings and your code writes them (RunemarkConsole.write* accepts any object with a write(text)
method).