Integrating into an existing monorepo¶
Most projects adopt requirements traceability part-way through their life, with tests, ids and conventions already in place. This page covers bringing rules_requirements into such a repository without a flag day.
Add the dependency¶
# MODULE.bazel
bazel_dep(name = "rules_requirements", version = "0.3.0")
git_override(
module_name = "rules_requirements",
remote = "https://github.com/Studio-Fug/rules_requirements.git",
commit = "<commit sha>",
)
For a vendored copy or a sibling checkout use
local_path_override(module_name = "rules_requirements", path = "third_party/rules_requirements").
Nothing in the module registers toolchains or a pip hub for you: the Python
library needs only the standard library, pytest comes from your own pip hub
(deps = [requirement("pytest")] on rr_py_test), and the C++ and Rust hooks
use your registered toolchains.
Keep your ids¶
If requirements are already numbered — PR-12, say — keep the numbers and
configure the prefix:
# requirements/project.yaml
project:
name: My product
config:
prefixes: {requirement: PR}
Every other kind keeps its default prefix unless you change it too; prefixes
must be distinct from one another. A different numbering scheme (SRS-3.2.1,
say) needs a matching id_pattern, for example '{prefix}-\d+(\.\d+)*'.
Model risk controls as mitigations¶
A common starting point is “derived requirements”: requirements that exist because a risk analysis asked for them, linked straight to the risk. In this model a risk control is its own entity — a mitigation — which says what the control is and is implemented by one or more requirements:
# before: a derived requirement pointing at the risk
# - id: PR-31
# title: Close the socket when a TLS handshake stalls
# kind: derived
# mitigates: [RISK-4]
risks:
- id: RISK-4
title: Stalled handshakes exhaust connection slots
severity: high
likelihood: likely
mitigations:
- id: MIT-4
title: Bound every handshake in time
type: protective
mitigates: [RISK-4]
implemented_by: [PR-31]
requirements:
- id: PR-31
title: Close the socket when a TLS handshake stalls
The requirement no longer needs to name the risk: it traces upward through the mitigation, so it is not an orphan. A risk the analysis controlled with several measures gets one mitigation per measure, or one mitigation implemented by several requirements — whichever matches how you argue it in the risk management file.
Keep your annotations¶
Existing traceability markers usually carry over unchanged:
The pytest marker is also registered as
@pytest.mark.requirements(...), with the samelevel=keyword.JUnit written by other tooling works as long as it uses the
requirementproperty (The JUnit conventions).0.2’s whole-target
verified_bylists still parse — a bare label or{target, level}is a whole-target claim, with abare-target-referencewarning — as long as no two entities name the same target. That would let one test case verify two requirements, so since 0.3 it is ashared-casemodel error: narrow both claims to the cases each one owns (Claims: which test cases verify an entity, Migrating to one requirement per test case).A docstring convention such as
Requirements: PR-1, PR-2is recognised by adding a pattern toconfig.annotation_patterns(Source annotations):config: annotation_patterns: - 'Requirements:\s*([A-Z0-9,\s-]+)'
Adopt the rules gradually¶
Coverage rules default to errors. On a large existing model, start them as warnings and tighten over time:
config:
rules:
need-unsatisfied: warning
requirement-orphan: warning
risk-unmitigated: warning
mitigation-unimplemented: warning
The shape and reference checks stay errors: a dangling reference is always a bug.
One owner per test case¶
A test case verifies at most one requirement
(One test case, one requirement). A project coming from 0.2, where a
test counted toward every id it was tagged with and every requirement listing
its target, moves there in steps that each keep CI green:
Migrating to one requirement per test case walks through them. The end state, which
examples/thermostat
shows, is:
# requirements/project.yaml
config:
prefixes: {requirement: PR}
attribution: model # the claims decide; tags only cross-check
sets_lock: verification.rrlock # every set's members, pinned
main_repo: my_product # '@my_product//x:y' reads as '//x:y'
with verified_by claims per case ({target: //web:clocksync_test, cases: ["clocksync::*"]}), single-id tags (or none), and a lock written by rr sets lock --write and reviewed like a golden file. 0.3 defaults to
attribution: hybrid, so a single-id tag still owns a case no claim covers
while you get there.
In Bazel, for the hermetic suites:
rr_model(
name = "model",
srcs = glob(["requirements/*.yaml"]),
lock = "requirements/verification.rrlock", # :model_test checks it against the claims
)
rr_report(
name = "report",
model = [":model"],
evidence = [":evidence"],
# check = True is the default with a JSON report: :report_check_test runs
# `rr check-report` on report.json. A quarantined case fails the build.
)
rr_sets_lock_test( # `bazel run :sets_lock_test.update` re-locks
name = "sets_lock_test",
model = ":model",
evidence = [":evidence"],
)
CI: aggregate real test logs¶
Suites that run only in CI or on hardware cannot run inside rr_evidence.
Run them normally, then build the report from the test logs, stamping the
identity of what was tested so that older evidence is marked stale:
# .github/workflows/traceability.yaml (excerpt)
- name: Tests
run: bazel test //... || true # a failing test must still produce a report
- name: Traceability report
run: |
bazel run @rules_requirements//python:rr -- report \
--model "$GITHUB_WORKSPACE/requirements" \
--evidence "$(bazel info bazel-testlogs)" \
--current-build dut_git_sha="$GITHUB_SHA" \
--queue-out "$GITHUB_WORKSPACE/traceability-queue.json" \
--html "$GITHUB_WORKSPACE/traceability-report.html" \
--json "$GITHUB_WORKSPACE/traceability-report.json" \
--md "$GITHUB_WORKSPACE/traceability-report.md" \
--fail-on failed
- uses: actions/upload-artifact@v4
if: always() # upload the report also when the step above failed
with:
name: traceability
path: traceability-*
rr report exits 3 when a test case is quarantined — its evidence names two
ids, or two entities claim it — after writing the reports, so the artifact is
there to read (the upload step runs if: always(), or GitHub would skip it
after the failed step); every entity it names reads INVALID. Add the
one-owner checks to the same job:
- name: Attribution checks (one test case, one requirement)
if: always() # also after a quarantine (exit 3) or a failure
run: |
bazel query 'tests(//...)' > "$RUNNER_TEMP/targets.txt"
bazel run @rules_requirements//python:rr -- validate requirements \
--known-targets "$RUNNER_TEMP/targets.txt"
bazel run @rules_requirements//python:rr -- sets check \
--model requirements --evidence "$(bazel info bazel-testlogs)"
bazel run @rules_requirements//python:rr -- check-report \
"$GITHUB_WORKSPACE/traceability-report.json"
--known-targets turns a claim on a label that does not exist (a typo would
otherwise read as not-run forever) into an unknown-target error; rr sets check fails when a locked case is missing or an owned one is not locked;
rr check-report re-proves from the published JSON (written with --json)
that no case has two owners.
Keep the test jobs as the pass/fail gate and let the report job describe the state; tighten
--fail-on(unverified,gaps) once coverage is meant to be complete.gapsalso fails on every attribution issue, warnings included (Attribution issues are gaps).Pass the whole
bazel-testlogsdirectory rather than a**/test.xmlglob if you retry tests: the earlier attempts undertest_attempts/are merged with the final result, and a pass that needed a retry then reads UNDER-VERIFIED (config.flaky) instead of a plain pass (Evidence and ingestors).Hardware harnesses should stamp their evidence with the same identity keys you pass to
--current-build—JUnitWriter(..., artifact={"dut_git_sha": sha})(Test hooks) — so a bench result from an older build is reported as stale rather than verified.The work queue is the natural input for follow-up automation:
autonomousgaps are candidates for an agent;human-gategaps need a bench session or a reviewer.Lanes. When hardware runs in another pipeline, stamp each report with its lane:
--lane software --lane-targets targets.txt(the targets this lane runs). Members in other lanes’ targets read “out of lane” and stay out of the queue, but verdicts never change: a requirement whose set spans both lanes reads INCOMPLETE in each lane’s report, and only a report over both lanes’ evidence can read VERIFIED. In Bazel:rr_report(lane = ..., lane_targets = ...).
A repository can use both paths at once: rr_evidence + rr_report +
rr_golden_test for the hermetic suites, pinned in review, and the aggregated
report in CI for the complete picture including hardware.