Source annotations

Annotations tie model entities to the code that implements and verifies them. They make the report say not only that a requirement is verified but where it lives — and they fail the build when code refers to an id that no longer exists.

Syntax

The universal form is a tag in a comment or docstring:

# @rr(REQ-0001): Implements isolated access to secure data
class SecureStore: ...
// @rr.verifies(REQ-0002)
TEST(Parser, RejectsEmpty) { ... }

Form

Relation

@rr(ID, ...)

implements in production code, verifies in test files

@rr.implements(ID, ...)

implements

@rr.verifies(ID, ...)

verifies

@pytest.mark.rr(...), @pytest.mark.requirements(...)

verifies

rr::verifies!(...) (Rust)

verifies

RR_VERIFIES(...) (googletest)

verifies

RR_CASE(name, ID) (rr_case.h), on one line

verifies

The language hooks (Test hooks) double as annotations, so a tagged test is both evidence (at run time) and a verification link (in the source). @rr.implements(...) / @rr.verifies(...) are also real Python decorators (from rules_requirements import rr), so the same text works as a comment or as code.

Details:

  • Ids are recognised by the configured prefixes and id pattern; anything else inside the parentheses (quotes, level="hil", prose) is ignored. A tag without a recognisable id is ignored.

  • Text after ): up to the end of the line is the annotation’s description (a trailing */, -->, """ or ''' is dropped).

  • RR_CASE(name, "ID") is recognised only when written on one line. The ids of rr_case.h’s list form ({"name", fn, "REQ-7"}) are evidence only, not annotations.

  • Any entity kind can be annotated — # @rr(MIT-2) on the function that realises a risk control is as useful as a requirement link.

  • A verifies annotation names one id: a test case verifies at most one requirement. Several ids in one verifies annotation (@rr.verifies(REQ-1, REQ-2), or @rr(REQ-1, REQ-2) in a test file) are deprecated: rr scan (rr check-annotations) and rr report --scan raise multi-verifies-annotation for each (a warning by default, an error under --strict or with config.rules: {multi-verifies-annotation: error}, and an error by default from 0.4). Annotations are display-only, so a verdict never depends on them. Implements annotations may name several ids.

Which files are tests

@rr(...) means verifies when the file’s path looks like a test: a test, tests, testing, spec or __tests__ directory component, or a file name like test_*, *_test.*, *_tests.*, *.test.* or *.spec.*. Otherwise it means implements. Use the explicit forms where the heuristic is wrong.

Symbol binding

Each annotation records the definition it annotates, which the reports show next to the location (thermostat/controller.py:27 (class Controller)):

  • a tag on a line of its own binds to the next definition, looking past decorators, attributes and comment lines — a blank line or any other code ends the search;

  • a tag trailing a definition line binds to that definition;

  • the in-body hooks (rr::verifies!, RR_VERIFIES) bind to the nearest enclosing definition above them.

Recognised definitions are def, class, fn, struct, enum, trait, impl, func, function, interface, type, module, mod and namespace (with common modifiers such as pub, async, export, static), googletest’s TEST, TEST_F, TEST_P and TYPED_TEST, which bind as Suite.Name, and RR_CASE(name), which binds as name (a tagged RR_CASE binds to itself).

Legacy conventions

Projects often already have a convention, such as a docstring line Requirements: PR-1, PR-2. Keep it by adding a pattern to the model’s config:; capture group 1 holds the id list:

config:
  annotation_patterns:
    - 'Requirements:\s*([A-Z0-9,\s-]+)'

Matches of extra patterns use the path-based default relation.

Checking annotations

rr scan (alias check-annotations) scans the workspace and fails if any annotation names an undefined id:

$ rr scan --model requirements/ --list        # in examples/thermostat
interlock/interlock.h:7: implements REQ-5 [class Interlock] — Over-temperature interlock, independent of the controller.
interlock/interlock_test.cc:13: verifies REQ-5 [Interlock.TripsAtLimit]
...
setpoint/src/lib.rs:24: implements REQ-3, REQ-4 [fn parse] — Explicit unit required; the result is range-checked in °C.
setpoint/src/lib.rs:46: verifies REQ-3 [fn parses_celsius_and_fahrenheit]
...
thermostat/controller.py:27: implements REQ-1, REQ-2 [class Controller] — Bang-bang control with a ±0.5 °C hysteresis band.
thermostat/display.py:5: implements REQ-7 [def format_setpoint] — The unit is always shown next to the number.
scanned: 18 annotation(s), 21 reference(s)
all references resolve.

Which files are scanned:

  • In a git checkout, the tracked files plus untracked files that are not ignored; otherwise every file under --root (skipping dot-directories, bazel-* and node_modules).

  • By default only source files: .py, .pyi, .rs, .c, .cc, .cpp, .cxx, .h, .hh, .hpp, .hxx, .go, .java, .kt, .swift, .m, .mm, .js, .jsx, .ts, .tsx, .mjs, .cs, .rb, .sh, .bzl, .proto, .v, .sv, .vhd, .vue, .svelte, and files named BUILD, BUILD.bazel or MODULE.bazel. --include GLOB replaces this filter.

  • Always skipped: .git/, node_modules/, bazel-*, third_party/, _vendor/ and .venv/; add more with --exclude GLOB.

  • --files F ... scans exactly the given files (relative to --root).

  • Files that are not valid UTF-8 are skipped.

--json FILE writes every annotation as {ids, relation, path, line, text, symbol}.

In Bazel, rr_annotations_test runs the same check hermetically over declared sources, and rr_report(srcs = ...) (or rr report --scan) adds the links to the report together with a no-implementation gap for every requirement that no annotation implements.