Reports¶
One traceability matrix, four renderings: HTML to read, Markdown for pull requests and wikis, JSON as the canonical, diffable record, and a gap queue for whoever — or whatever — closes the gaps.
Producing a report¶
With the CLI:
$ rr report --model requirements/ --evidence bazel-testlogs \
--html report.html --json report.json --md report.md \
--queue-out gaps.json --title "Thermostat V&V"
--out FILE (repeatable) picks the format from the extension (.html, .htm,
.json, .md, .markdown); - writes to standard output. --scan adds the
source annotations of the workspace (see Source annotations), and
--current-build KEY=VALUE (repeatable) enables the
staleness check.
In Bazel, rr_report builds <name>.html, <name>.json and <name>.md as
ordinary outputs (Bazel rules).
The command prints a one-line summary to standard error and exits with:
Exit |
When |
|---|---|
|
The report was written and no |
|
An attribution issue is an error (a hard error such as |
|
The model is invalid, or an |
|
A test case is quarantined (unless |
--fail-on defaults to none: the report describes the state of the product,
and whether that state should fail a pipeline is a separate decision.
Each quarantined test case — one whose evidence names several ids, that two
entities claim, or whose test code two entities own — prints one
ATTRIBUTION ERROR: <code>: <detail> line to standard error, every entity it
names reads INVALID (Evidence and attribution), and rr report exits 3.
--on-attribution-error=warn keeps the exit status; it never changes a
verdict (the case still counts for nobody).
Every other attribution issue (same-path-multiple-owners, unscoped-evidence,
level-mismatch, suite-level-requirement, the lock findings, …) is a gap
in the report (Attribution issues are gaps lists them all). One at error severity also prints an
ATTRIBUTION ERROR: [<code>] <message> line and makes rr report exit 1;
the warnings are counted on one attribution: N warning(s) (...) line.
--strict escalates every attribution warning (not only the model’s) to an
error.
--sets-lock PATH reads that verification-set lock instead of
config.sets_lock; --no-lock reads none (the sets are then not pinned: an
unpinned-sets gap).
Attribution issues are gaps¶
Every attribution issue is a gap, warnings included, so --fail-on gaps
exits 1 on any of them even when every verdict is VERIFIED. Locking the sets
removes only the unpinned-sets gap. Each issue below is one gap, except
misdirected-evidence and unknown-id, which are gathered into one gap per
id. Configurable means config.rules can set the rule to warning or off
(off removes the issue and its gap; warning keeps the gap).
Issue |
Severity |
Configurable |
Do |
|---|---|---|---|
|
warning |
yes |
Resolve: rename one of the two tests reported under one case key. |
|
warning |
no |
Resolve: write the JUnit under a testlogs tree ( |
|
warning |
yes |
Resolve: move the |
|
warning |
yes |
Resolve: claim the target’s cases with |
|
warning |
yes |
Resolve: make the test’s tag name its owner, or drop the tag. |
|
warning |
yes |
Resolve ( |
|
warning |
no |
Resolve: tag the requirement, not the risk or test method. |
|
warning |
no |
Resolve: define the id in the model, or fix the tag. |
|
warning |
yes |
Resolve: record each case’s source file (the hooks do), or give the two tests different names. |
|
error |
no |
Resolve: record source files inside the workspace root, so the two files can be told apart. |
|
warning |
yes |
Resolve: make the claim’s |
|
warning |
no |
Lock: |
|
error |
yes |
Lock: |
|
error |
no |
Lock: |
|
error |
no |
Lock: fix |
|
warning |
yes |
Resolve (only with |
Besides these, --fail-on gaps fails on every other gap kind of
the gap queue: unpinned-sets (lock: set config.sets_lock
and run rr sets lock --write), the verdict gaps and the open notes.
The gates at a glance¶
Exit 3 is new in 0.3; 0, 1 and 2 mean what they meant in 0.2. The
commands that enforce one owner per test case
(One test case, one requirement), and when each one fails:
Command |
Fails with |
When |
In Bazel |
|---|---|---|---|
|
|
a model error: |
|
|
|
an invalid model / a quarantined case / an error-level attribution issue or a |
|
|
|
the published JSON breaks the partition or its counts |
|
|
|
the lock and the evidence disagree: a missing case, an unlocked member, an owner change, a stale entry |
|
|
|
a quarantine, a missing case, lock drift or an error-level issue |
— |
Lanes¶
A requirement’s set may span lanes — software tests in one pipeline, HITL
tests in another. --lane NAME stamps the report with its lane, and
--lane-targets FILE (labels, one per line, e.g. bazel query 'tests(//...)') lists the targets that lane runs. A not-run member of any
other target is labelled lane_hint: "out of lane" (expected elsewhere), and
the unverified / incomplete gaps such members alone cause stay out of
--queue-out (they are another lane’s work; the report still lists them).
Not-run members of in-lane targets stay ordinary not-run gaps. Verdicts
are identical with or without these flags: a set spanning both lanes reads
INCOMPLETE in each lane’s report, and only the combined report, over both
lanes’ evidence, can read VERIFIED.
Checking a published report¶
rr check-report report.json re-proves the one-owner partition from the JSON
alone, independently of the code that wrote it. It exits 1 if a case key is
an owned member (owned: true) of two entities — whether or not cases
lists it —, if an owned member is no row of cases owned by its entity, if
an owner is not a scalar id, if a quarantined case is owned, if the entities
holding a quarantined case are not exactly the ones its quarantine names (a
list of ids that follows from its code), if one of them does not read
INVALID, if an entity’s evidence is not exactly the view of its owned
members, if the counts (summary, sets, per-target counts, granularity)
disagree with the rows they count, or if the file names a key twice in one
object (a duplicate owner reads differently to different parsers); 2 if
the file is not a rules_requirements/report/v2 report.
It also re-proves what attribute() decides from the rows alone. Every case
key (rows, members, attribution.targets) must be in its one spelling: the
target normalized (@//p:t, @@//p:t, //p and, with the recorded
attribution.main_repo, @<main_repo>//p:t are spellings of //p:t), the
path NFC with no surrounding blanks and no [rr:ID] tag at the end of the
case name. One test’s code may have one owner: two owned rows with the same
file and path in two targets, equal paths in one recorded
attribution.variants group, or equal paths where one target is a suite: or
record: pseudo-target and a file is missing, are rejected when their owners
differ. A row declaring two ids must be quarantined multi-tag; an owner via
tag must be the row’s one declared id, in a hybrid report. An owned
member’s state is its row’s status (or error on a tainted target). The
basis follows the set: an entity with members has basis own or
own+derived (only one without members may read derived), so relabelling
the basis cannot hide a set. An entity with members (or basis own) that
reads VERIFIED, UNDER-VERIFIED or VALIDATED needs a passed member and no
failed, error, skipped, missing, not-run, moved or quarantined one; a passing
verdict derived from no entity is rejected; INVALID needs a quarantined
member; and derived_from names only the entity’s children (refines,
satisfies, method, mitigates, implemented_by).
An error member
that is not owned is a pseudo-member: it names no case of the report, on a
tainted or synthetic-only target. A missing or not-run member never names
a case of the report either (only a moved or quarantined member may), so
no case sits in two verification sets. In Bazel, rr_report adds it as
<name>_check_test whenever it builds the JSON report.
JSON (rules_requirements/report/v2)¶
The JSON report is deterministic — entities sorted by id (REQ-2 before
REQ-10), no timestamps, no machine-specific paths — so it can be checked in as
a golden file and reviewed as a diff. Its JSON Schema is
schema/report.v2.schema.json. Top-level keys:
Key |
Content |
|---|---|
|
|
|
|
|
The model’s |
|
Counts: |
|
|
|
The inverse matrix: every case key with one owner or |
|
The configured levels: |
|
One object per entity (below) |
|
|
|
Ids of high-severity risks that are not MITIGATED |
|
Ids of requirements violating the cost pyramid |
|
|
|
The gap queue (below); a gap only out-of-lane members cause carries |
Every entity object has id, title, status and basis (own, derived
or own+derived: whether its verdict rests on its own set, on other
entities’ verdicts — listed in derived_from — or both), plus description,
open_notes ([{kind, text}]) and evidence when present. User needs,
requirements and mitigations also carry their verification set: set counts its members (complete, members, passed,
failed, error, skipped, missing, not_run, moved, quarantined)
and members lists them — {case, target, selector, via, state, owned} plus
level, stale, flaky, reason and lane_hint when they apply (case is
null for a glob or whole claim that matched nothing). owned says whether
the entity owns the case (it counts as its evidence): an owned member is a row
of cases owned by that entity, and no case key is an owned member of two
entities. The others are pseudo-members (missing, not-run, moved,
quarantined, and an error for a case a tainted target did not report).
evidence is the 0.2 view of the owned members, kept for 0.3.x readers: each
entry is {name, status, level} — the case path, its member state, its level —
with, when applicable, target (the build label or pseudo-target),
stale: true, and message (the first line, at most 300 characters, of a
failure). Since 0.3 a whole-target claim lists the target’s cases, so
kind: "target" entries no longer occur, and a quarantined case is listed for
no entity.
Section |
Additional keys |
|---|---|
|
|
|
|
|
|
|
|
|
|
implemented_in / verified_in entries are the annotations:
{ids, relation, path, line} plus text and symbol when known.
The gap queue¶
--queue-out FILE writes the gaps as
{"queue": [...]}; each gap is
{
"kind": "under-verified",
"entity": "REQ-5",
"message": "demands hil, best passing evidence is simulation",
"route": "human-gate",
"demanded_level": "hil",
"provided_level": "simulation"
}
demanded_level and provided_level are present when they apply. The queue is
meant to drive work: autonomous items are ones an agent can close by writing
the missing analysis, simulation or software-in-the-loop test; human-gate
items need a bench, a device or a person, and must not be closed by synthesised
evidence.
Markdown¶
A compact rendering for pull-request comments and wikis: the summary line
(with the owned, unowned and quarantined case counts), the attribution mode,
lock and lane, a red banner listing every quarantined case with its code and
every claim’s origin, a banner for high-severity risks that are not mitigated,
then tables for user needs, requirements (each row’s evidence cell starts with
its set line, e.g. set 17/23 passed · 6 not run (out of lane)), risks,
mitigations, test methods, source implementation links (when sources were
scanned) and modules; then “Verification sets” (one member table per entity:
case, state, level, via, selector, lane hint), “Case attribution” (per target:
cases, owned, quarantined, unowned, owners — and the unowned cases, the
granularity backlog), “Lock drift” and “Coarse claims” when they have entries,
and the gaps. The tutorial shows the complete Markdown
report of the example project.
HTML¶
A single self-contained page (no external assets; light and dark themes) with
progress tiles, a quarantine banner (each case, its code and every claim’s
origin), alerts for unmitigated high-severity risks, cost-pyramid violations
and evidence that names undefined ids, the trace graph, and a table per entity
kind. Requirement rows show their traces, their set line — expanding to the
member table — evidence (with levels, staleness and failure messages),
demanded and best-provided level, the basis of a derived verdict, source links
and open notes; the “Case attribution”, “Lock drift” and “Coarse claims”
sections follow. Every id is an anchor, so report.html#REQ-5 links straight
to a row.
Graph exports¶
rr graph exports the trace graph on its own:
$ rr graph --model requirements/ --format mermaid # to stdout
$ rr graph --model requirements/ --format svg --evidence bazel-testlogs --out trace.svg
$ rr graph --model requirements/ --format dot --methods | dot -Tpng -o trace.png
$ rr graph --model requirements/ --format svg --evidence bazel-testlogs --cases --out cases.svg
Format |
Content |
|---|---|
|
Graphviz; needs are ellipses, requirements boxes, mitigations hexagons, risks diamonds, test methods notes. |
|
A Mermaid |
|
A self-contained layered drawing: needs, requirements, mitigations and risks in columns, ordered to reduce crossings. |
|
|
Edges are satisfies, refines and method (dashed or dotted), mitigates
and implemented_by. --methods adds test methods and method edges (the SVG
layout draws only the four main columns). With --evidence, nodes are coloured
by status. --cases (which needs --evidence) adds a node for every owned test
case, coloured by its result, with exactly one verifies edge into it: from the
one entity attribution gave it to. A test case verifies at most one requirement,
so no case node has two; unowned and quarantined cases are left out. The same graph, coloured from the example’s golden report: