Tutorial: a requirements-first thermostat¶
The repository’s examples/thermostat
is a deliberately small product developed the way this tool intends: user
needs first, then requirements and a risk analysis, then code and tests that
trace back to them — ending in a report that a golden test pins in review.
Every requirement names the test cases that verify it, and each test case
verifies exactly one requirement (One test case, one requirement). It
is split into three components, one per supported test framework:
Component |
Language |
Hook |
Requirements |
|---|---|---|---|
|
Python |
pytest, unittest |
REQ-1, 2, 4, 6, 7 |
|
C++ |
googletest |
REQ-5 |
|
Rust |
|
REQ-3, 4 |
panel inspection |
— |
signed record |
REQ-7 |
To follow along, clone the repository and work in examples/thermostat, which
is a Bazel module of its own that uses the ruleset from the checkout
(local_path_override(path = "../..")).
1. User needs¶
Start from what the occupants of the room need, in their terms:
user_needs:
- id: UN-1
title: Keep the room at a comfortable temperature
description: >-
Once a target temperature is set, the room should settle near it without
the heater cycling rapidly.
- id: UN-2
title: Set the target temperature in Celsius or Fahrenheit
description: >-
Occupants enter a target like "21.5C" or "70F" on the wall panel and can
read back what they set.
2. Risks and their controls¶
A risk analysis asks what could go wrong and how badly (ISO 14971; see Background: the standards). Each risk names its hazard, the situation in which people are exposed to it, the harm, and an estimate of severity and likelihood — before and after control. Each mitigation is one risk control measure:
# Risk analysis (ISO 14971 §5) and risk control (§7). Each mitigation is a
# risk control measure implemented by requirements, so its effectiveness is
# verified by the same tests (§7.2).
risks:
- id: RISK-1
title: Room overheats
hazard: Heater energised continuously
hazardous_situation: >-
A control fault or a sensor reading stuck low keeps the heater on while
the room is occupied.
harm: Heat stress; burns from surfaces near the heater
severity: high
likelihood: possible
residual_severity: high
residual_likelihood: rare
residual: >-
Two independent controls (interlock + sensor plausibility) must both
fail. Accepted.
- id: RISK-2
title: Implausible setpoint accepted
hazard: Setpoint far above comfort range
hazardous_situation: >-
"80" meant as °F is taken as °C, or a typo like "250C", and the
controller drives the room towards it.
harm: Heat stress
severity: medium
likelihood: likely
residual_likelihood: unlikely
mitigations:
- id: MIT-1
title: Independent over-temperature protection
type: protective
description: >-
A hardware-near interlock and a sensor plausibility check, each able to
de-energise the heater on its own.
mitigates: [RISK-1]
implemented_by: [REQ-5, REQ-6]
- id: MIT-2
title: Validate setpoints at entry
type: inherent
description: Require an explicit unit and a bounded range for every setpoint.
mitigates: [RISK-2]
implemented_by: [REQ-3, REQ-4]
project.yaml sets the risk acceptability threshold used to check the residual
estimates (high × rare scores 4 × 1 = 4 and medium × unlikely 3 × 2 = 6, both
within 6). It also puts the model in charge of which requirement each test case
verifies (attribution: model) and names the lock that pins every
requirement’s set of cases (sets_lock, step 6):
# The thermostat's requirements model. Files in this directory are merged;
# this one holds project metadata and configuration, the others one entity
# kind each. Validate with `bazel test //:model_test`.
project:
name: Thermostat
description: >-
A room thermostat: a Python controller, a C++ over-temperature interlock
and a Rust setpoint parser, developed against user needs, requirements and
a risk analysis, and traced to tests.
software_safety_class: B # IEC 62304 §4.3 (illustrative)
config:
# Risk evaluation (ISO 14971 §5.5 / §7.3): score = (severity index + 1) x
# (likelihood index + 1) on the default five-point scales. Residual risk
# above this is flagged.
acceptable_risk_score: 6
# Test case ownership: the claims (`verified_by`) decide which requirement
# a test case verifies, and tags only cross-check them. A test case
# verifies at most one requirement.
attribution: model
# Every verification set's members, pinned: written by
# `bazel run //:sets_lock_test.update`, reviewed like a golden.
sets_lock: verification.rrlock
3. Requirements and test methods¶
Requirements say what the product must do. Some satisfy user needs; others
exist because a mitigation needs them (REQ-5, REQ-6), and they trace upward
through the mitigation instead. Each one also claims the test cases that
verify it (verified_by): a target and the case paths it owns, literally or
with * as the only wildcard — test_rejects_setpoints_outside_range[*]
takes every parameter of that pytest test, Interlock::* every googletest
case of the interlock suite. No two requirements may claim one case
(Claims: which test cases verify an entity):
# Each requirement claims the test cases that verify it (`verified_by`): a
# literal case path, or a pattern whose only wildcard is '*'. A test case
# verifies at most one requirement, so no two requirements' claims may select
# one case (`rr validate` proves it: `shared-case`). The tags in the tests
# (`@pytest.mark.rr`, `RR_VERIFIES`, ...) only cross-check these claims, and
# verification.rrlock pins every set's members (`bazel run //:sets_lock_test.update`).
requirements:
- id: REQ-1
title: Heat when the room is below the setpoint band
description: >-
The heater shall turn on when the measured temperature is below
setpoint - 0.5 °C.
satisfies: [UN-1]
modules: [thermostat]
verified_by:
- target: //:controller_test
cases: ["tests.test_controller::test_heats_below_band"]
- id: REQ-2
title: Stop heating above the band; hold state inside it
description: >-
The heater shall turn off when the measured temperature is above
setpoint + 0.5 °C, and keep its previous state inside the band
(hysteresis), so it does not cycle rapidly.
satisfies: [UN-1]
modules: [thermostat]
verified_by:
- target: //:controller_test
cases: ["tests.test_controller::test_stops_above_band_and_holds_inside_it"]
- id: REQ-3
title: Parse setpoints in °C and °F
description: >-
The panel shall accept "<number>C" and "<number>F" (case-insensitive,
optional whitespace) and convert to °C.
satisfies: [UN-2]
modules: [setpoint]
verified_by:
- target: //:setpoint_test
cases:
- "tests::parses_celsius_and_fahrenheit"
- "tests::requires_a_unit"
- id: REQ-4
title: Reject setpoints outside 5-30 °C
description: The panel shall reject any setpoint that converts to below 5 °C or above 30 °C.
satisfies: [UN-2]
category: safety
modules: [setpoint, thermostat]
verified_by:
- target: //:controller_test
cases:
- "tests.test_controller::test_rejects_setpoints_outside_range[*]"
- "tests.test_controller::test_accepts_range_limits"
- target: //:setpoint_test
cases:
- "tests::rejects_out_of_range"
- "tests::checks_the_range_after_converting"
- id: REQ-5
title: Independent over-temperature cutoff
description: >-
The interlock shall force the heater off at or above 35 °C regardless of
the controller, and keep it off until the temperature falls below 30 °C.
category: safety
method: TM-1
modules: [interlock]
verified_by:
- target: //:interlock_test
cases: ["Interlock::*"]
- id: REQ-6
title: Fail safe on an invalid sensor reading
description: >-
The controller shall turn the heater off when the reading is not a
number or outside the sensor's -40..85 °C range.
category: safety
modules: [thermostat]
verified_by:
- target: //:controller_test
cases: ["tests.test_controller::test_invalid_reading_turns_heater_off[*]"]
- id: REQ-7
title: Show the setpoint with its unit
description: The panel shall display the active setpoint with its unit, e.g. "21.5 °C".
satisfies: [UN-2]
method: TM-2
modules: [thermostat]
verified_by:
- target: //:display_test
cases: ["display_test.DisplayTest::test_setpoint_shows_unit"]
- target: record:panel_inspection
level: inspection # what the record provides; also when it is missing
cases: ["inspection.TM-2::panel-shows-setpoint-unit"]
Two requirements demand more than the default simulation rigor, through test
methods: the interlock must be verified software-in-the-loop, and the panel
text must be signed off by inspection:
test_methods:
- id: TM-1
title: Interlock software-in-the-loop test
level: sil
procedure: >-
Drive the interlock state machine through temperature ramps that cross
the trip and reset thresholds, checking the heater enable at each step.
- id: TM-2
title: Visual inspection of the panel
level: inspection
procedure: A reviewer checks the rendered panel text and signs the record.
bazel test //:model_test validates all of this — the claims included: had
REQ-4 also claimed tests::requires_*, it would fail with
requirements/requirements.yaml:55: error: [shared-case] REQ-4 and REQ-3 both claim cases of //:setpoint_test ('tests::requires_*' vs 'tests::requires_a_unit'), e.g. 'tests::requires_a_unit' (REQ-3 claims it at requirements/requirements.yaml:39). A test case verifies at most one requirement: narrow one selector.
The trace graph, coloured by the final verdicts:
4. Implementation, annotated¶
Code carries @rr(...) annotations naming what it implements
(Source annotations):
# @rr(REQ-6): A reading the sensor cannot produce means the sensor is broken.
def plausible(reading_c: float) -> bool:
lo, hi = SENSOR_RANGE_C
return not math.isnan(reading_c) and lo <= reading_c <= hi
# @rr(REQ-4): Setpoints outside the comfort range are refused, whatever their source.
def check_setpoint(setpoint_c: float) -> float:
if not MIN_SETPOINT_C <= setpoint_c <= MAX_SETPOINT_C:
raise ValueError(f"setpoint {setpoint_c:.1f} °C is outside {MIN_SETPOINT_C:g}-{MAX_SETPOINT_C:g} °C")
return setpoint_c
# @rr(REQ-1, REQ-2): Bang-bang control with a ±0.5 °C hysteresis band.
class Controller:
def __init__(self, setpoint_c: float) -> None:
self.setpoint_c = check_setpoint(setpoint_c)
self.heater_on = False
def update(self, reading_c: float) -> bool:
"""Feed one temperature reading; returns whether the heater should run."""
if not plausible(reading_c):
self.heater_on = False
elif reading_c < self.setpoint_c - BAND_C:
self.heater_on = True
elif reading_c > self.setpoint_c + BAND_C:
self.heater_on = False
return self.heater_on
// @rr(REQ-5): Over-temperature interlock, independent of the controller.
// Trips at kTripC and stays tripped until the temperature drops below kResetC.
class Interlock {
public:
static constexpr double kTripC = 35.0;
static constexpr double kResetC = 30.0;
// Feeds a reading; returns whether the heater may be energised.
bool HeaterAllowed(double reading_c);
bool tripped() const { return tripped_; }
private:
bool tripped_ = false;
};
// @rr(REQ-3, REQ-4): Explicit unit required; the result is range-checked in °C.
pub fn parse(text: &str) -> Result<f64, Error> {
let t = text.trim();
let (num, unit) = t.split_at(t.len().saturating_sub(1));
let value: f64 = num.trim().parse().map_err(|_| Error::Syntax(text.to_string()))?;
let celsius = match unit {
"C" | "c" => value,
"F" | "f" => (value - 32.0) * 5.0 / 9.0,
_ => return Err(Error::Syntax(text.to_string())),
};
if !(MIN_C..=MAX_C).contains(&celsius) {
return Err(Error::OutOfRange(celsius));
}
Ok(celsius)
}
5. Tests, one hook per language¶
The model’s claims decide which requirement a test verifies, so a test needs
no tag. Each test here still names its one requirement through its framework’s
hook, as a cross-check: a tag that disagrees with the claim owning its case is
a tag-mismatch gap in the report.
pytest tests use the rr marker:
@pytest.mark.rr("REQ-1")
def test_heats_below_band():
ctl = Controller(21.0)
assert ctl.update(20.4) is True
@pytest.mark.rr("REQ-2")
def test_stops_above_band_and_holds_inside_it():
ctl = Controller(21.0)
ctl.update(19.0)
assert ctl.update(21.4) is True # inside the band: keep heating
assert ctl.update(21.6) is False
assert ctl.update(20.6) is False # inside the band: stay off
The unittest suite uses the decorator and rr.unittest_main():
class DisplayTest(unittest.TestCase):
# Automated check of the text; the rendered panel is signed off by
# inspection (evidence/panel_inspection.rr.yaml) as TM-2 demands.
@rr.verifies("REQ-7")
def test_setpoint_shows_unit(self):
self.assertEqual(format_setpoint(21.5), "21.5 °C")
self.assertEqual(format_setpoint(5), "5.0 °C")
if __name__ == "__main__":
rr.unittest_main()
googletest tests call RR_VERIFIES and declare the level they provide — sil,
which is what TM-1 demands of REQ-5:
TEST(Interlock, TripsAtLimit) {
RR_VERIFIES("REQ-5");
RR_LEVEL("sil");
Interlock lock;
EXPECT_TRUE(lock.HeaterAllowed(34.9));
EXPECT_FALSE(lock.HeaterAllowed(35.0));
EXPECT_TRUE(lock.tripped());
}
Rust tests call rr::verifies!. Until 0.3, requires_a_unit called
rr::verifies!("REQ-3", "REQ-4"); a test case verifies at most one
requirement, so that case was quarantined and both requirements read INVALID.
Its assertions are about syntax, so it now verifies REQ-3, and the range check
after converting °F, which is REQ-4’s, is a test of its own:
#[test]
fn requires_a_unit() {
rr::verifies!("REQ-3");
assert!(matches!(parse("21"), Err(Error::Syntax(_))));
assert!(matches!(parse("warm"), Err(Error::Syntax(_))));
assert!(matches!(parse(""), Err(Error::Syntax(_))));
}
#[test]
fn checks_the_range_after_converting() {
rr::verifies!("REQ-4");
assert!(matches!(parse("40F"), Err(Error::OutOfRange(_))));
assert_eq!(parse("41F"), Ok(5.0));
assert_eq!(parse("86F"), Ok(30.0));
assert!(matches!(parse("87F"), Err(Error::OutOfRange(_))));
}
Finally, the inspection TM-2 demands is recorded as evidence in its own right (Evidence and ingestors):
# Manual verification record (TM-2). Records like this are evidence exactly
# like test results. Each is a test case of the document's `target`, which
# the model claims (REQ-7: record:panel_inspection); `requirement` names the
# one requirement it verifies, as a cross-check, and `level` the rigor it
# provides.
target: record:panel_inspection
evidence:
- name: panel-shows-setpoint-unit
classname: inspection.TM-2
status: passed
requirement: REQ-7
level: inspection
properties:
procedure: TM-2
inspector: QA reviewer
date: "2026-09-30"
record: "Panel photo shows '21.5 °C' after entering 21.5C and '21.1 °C' after 70F."
6. The build¶
The BUILD.bazel file wires it together — the model with its lock, one test
per hook, an annotation check, and the evidence → report → golden chain, with
the lock checked against the same evidence:
rr_py_test(
name = "controller_test",
srcs = ["tests/test_controller.py"],
deps = [
":thermostat",
requirement("pytest"),
],
)
py_test(
name = "display_test",
srcs = ["tests/display_test.py"],
deps = [
":thermostat",
"@rules_requirements//python",
],
)
cc_test(
name = "interlock_test",
srcs = ["interlock/interlock_test.cc"],
deps = [
":interlock",
"@googletest//:gtest_main",
"@rules_requirements//cc:gtest",
],
)
rr_rust_test(
name = "setpoint_test",
crate = ":setpoint",
rule = rust_test,
deps = ["@rules_requirements//rust:rr"],
)
SOURCES = glob([
"thermostat/*.py",
"interlock/*",
"setpoint/src/*.rs",
"tests/*.py",
])
# Every @rr(...) annotation must name an entity that exists.
rr_annotations_test(
name = "annotations_test",
srcs = SOURCES,
model = ":model",
)
# --------------------------------------------------------------------------- #
# Evidence and the traceability report, pinned by goldens. #
# --------------------------------------------------------------------------- #
rr_evidence(
name = "evidence",
tests = [
":controller_test",
":display_test",
":interlock_test",
":setpoint_test",
],
)
EVIDENCE = [
":evidence",
"evidence/panel_inspection.rr.yaml",
]
# A quarantined test case (one naming two requirements, or claimed by two)
# fails this build; :report_check_test re-proves from report.json alone that
# no test case verifies two requirements.
rr_report(
name = "report",
srcs = SOURCES,
check = True,
evidence = EVIDENCE,
model = [":model"],
)
# The lock agrees with the evidence: no member missing, none unlocked, no
# owner changed (`rr sets check`). `bazel run //:sets_lock_test.update`
# rewrites requirements/verification.rrlock after an intended change.
rr_sets_lock_test(
name = "sets_lock_test",
evidence = EVIDENCE,
model = ":model",
)
rr_golden_test(
name = "report_json_golden_test",
src = ":report.json",
golden = "report.golden.json",
)
rr_golden_test(
name = "report_md_golden_test",
src = ":report.md",
golden = "report.golden.md",
)
# `bazel run //:editor` — the web editor on this model (see the web editor guide).
rr_editor(
name = "editor",
model = ":model",
paths = ["requirements"],
)
$ bazel test //...
//:annotations_test PASSED
//:controller_test PASSED
//:display_test PASSED
//:interlock_test PASSED
//:model_test PASSED
//:report_check_test PASSED
//:report_json_golden_test PASSED
//:report_md_golden_test PASSED
//:setpoint_test PASSED
//:sets_lock_test PASSED
$ bazel build //:report
evidence: 5 file(s), 18 test case(s) | validation: 2/2 needs | verification: 7/7 requirements (0 failed, 0 unverified, 0 under-verified, 0 invalid, 0 incomplete) | risks: 2/2 mitigated | gaps: 0
//:report runs the four test targets inside a build action (rr_evidence),
adds the inspection record, scans the sources for annotations and renders
bazel-bin/report.html, report.json and report.md. A quarantined test
case would fail this build. //:report_check_test re-proves from
report.json alone that no test case is owned by two requirements
(rr check-report).
The lock. requirements/verification.rrlock records, for every test
case, the one requirement whose set holds it:
# Generated by `rr sets lock --write`; review its diff like a golden file.
schema: rules_requirements/verification-lock/v1
cases:
//:controller_test:
"tests.test_controller::test_accepts_range_limits": REQ-4
"tests.test_controller::test_heats_below_band": REQ-1
"tests.test_controller::test_invalid_reading_turns_heater_off[85.1]": REQ-6
"tests.test_controller::test_invalid_reading_turns_heater_off[-41.0]": REQ-6
//:sets_lock_test fails when the evidence and the lock disagree — a locked
test that no longer runs, a new test no one locked, a case that changed
owner — so a deleted or renamed test cannot silently shrink a requirement’s
set. After an intended change, bazel run //:sets_lock_test.update rewrites
the lock; its diff is reviewed with the change, like a golden file.
7. The report¶
This is the example’s golden Markdown report, exactly as checked in. Every
requirement is verified, by a set of test cases it alone owns: each member
lists the selector that claims it (via model), and the “Case attribution”
table shows every case of every target owned once, none unowned, none
quarantined:
Thermostat¶
2/2 user needs validated · 7/7 requirements verified (0 under-verified, 0 failed, 0 invalid, 0 incomplete, 0 unverified) · 2/2 risks mitigated · 18 test cases (18 owned, 0 unowned, 0 quarantined) · 0 gaps
attribution: model · lock: requirements/verification.rrlock
User needs — validation¶
ID |
Need |
Requirements |
Status |
|---|---|---|---|
UN-1 |
Keep the room at a comfortable temperature |
REQ-1, REQ-2 |
✅ VALIDATED |
UN-2 |
Set the target temperature in Celsius or Fahrenheit |
REQ-3, REQ-4, REQ-7 |
✅ VALIDATED |
Requirements — verification¶
ID |
Requirement |
Traces |
Demands |
Evidence |
Status |
|---|---|---|---|---|---|
REQ-1 |
Heat when the room is below the setpoint band |
satisfies UN-1 |
simulation |
set 1/1 passed |
✅ VERIFIED |
REQ-2 |
Stop heating above the band; hold state inside it |
satisfies UN-1 |
simulation |
set 1/1 passed |
✅ VERIFIED |
REQ-3 |
Parse setpoints in °C and °F |
satisfies UN-2; implements MIT-2 |
simulation |
set 2/2 passed |
✅ VERIFIED |
REQ-4 |
Reject setpoints outside 5-30 °C |
satisfies UN-2; implements MIT-2 |
simulation |
set 6/6 passed |
✅ VERIFIED |
REQ-5 |
Independent over-temperature cutoff |
implements MIT-1 |
sil |
set 3/3 passed |
✅ VERIFIED |
REQ-6 |
Fail safe on an invalid sensor reading |
implements MIT-1 |
simulation |
set 3/3 passed |
✅ VERIFIED |
REQ-7 |
Show the setpoint with its unit |
satisfies UN-2 |
inspection |
set 2/2 passed |
✅ VERIFIED |
Risks — control¶
ID |
Risk |
Severity × likelihood |
Mitigations |
Status |
|---|---|---|---|---|
RISK-1 |
Room overheats |
high × possible → high × rare |
MIT-1 |
✅ MITIGATED |
RISK-2 |
Implausible setpoint accepted |
medium × likely → medium × unlikely |
MIT-2 |
✅ MITIGATED |
Mitigations — risk control measures¶
ID |
Mitigation |
Type |
Mitigates |
Implemented by |
Status |
|---|---|---|---|---|---|
MIT-1 |
Independent over-temperature protection |
protective |
RISK-1 |
REQ-5, REQ-6 |
✅ VERIFIED |
MIT-2 |
Validate setpoints at entry |
inherent |
RISK-2 |
REQ-3, REQ-4 |
✅ VERIFIED |
Test methods¶
ID |
Method |
Level |
Used by |
|---|---|---|---|
TM-1 |
Interlock software-in-the-loop test |
sil |
REQ-5 |
TM-2 |
Visual inspection of the panel |
inspection |
REQ-7 |
Implementation — source annotations¶
ID |
Implemented in |
Verified in |
|---|---|---|
REQ-1 |
thermostat/controller.py:27 (class Controller) |
tests/test_controller.py:9 (def test_heats_below_band) |
REQ-2 |
thermostat/controller.py:27 (class Controller) |
tests/test_controller.py:15 (def test_stops_above_band_and_holds_inside_it) |
REQ-3 |
setpoint/src/lib.rs:24 (fn parse) |
setpoint/src/lib.rs:46 (fn parses_celsius_and_fahrenheit) |
REQ-4 |
setpoint/src/lib.rs:24 (fn parse) |
setpoint/src/lib.rs:62 (fn rejects_out_of_range) |
REQ-5 |
interlock/interlock.h:7 (class Interlock) |
interlock/interlock_test.cc:13 (Interlock.TripsAtLimit) |
REQ-6 |
thermostat/controller.py:14 (def plausible) |
tests/test_controller.py:37 (def test_invalid_reading_turns_heater_off) |
REQ-7 |
thermostat/display.py:5 (def format_setpoint) |
tests/display_test.py:11 (def test_setpoint_shows_unit) |
MIT-1 |
— |
— |
MIT-2 |
— |
— |
Modules¶
Module |
Status |
|---|---|
interlock |
✅ VERIFIED |
setpoint |
✅ VERIFIED |
thermostat |
✅ VERIFIED |
Verification sets¶
Each entity’s set: the cases it owns, the cases it expects (literal selectors, the lock) and every quarantined case that names it. It is verified only when the whole set passed together.
REQ-1 — ✅ VERIFIED¶
set 1/1 passed
Case |
State |
Level |
Via |
Selector |
Note |
|---|---|---|---|---|---|
//:controller_test#tests.test_controller::test_heats_below_band |
passed |
simulation |
model |
tests.test_controller::test_heats_below_band |
REQ-2 — ✅ VERIFIED¶
set 1/1 passed
Case |
State |
Level |
Via |
Selector |
Note |
|---|---|---|---|---|---|
//:controller_test#tests.test_controller::test_stops_above_band_and_holds_inside_it |
passed |
simulation |
model |
tests.test_controller::test_stops_above_band_and_holds_inside_it |
REQ-3 — ✅ VERIFIED¶
set 2/2 passed
Case |
State |
Level |
Via |
Selector |
Note |
|---|---|---|---|---|---|
//:setpoint_test#tests::parses_celsius_and_fahrenheit |
passed |
simulation |
model |
tests::parses_celsius_and_fahrenheit |
|
//:setpoint_test#tests::requires_a_unit |
passed |
simulation |
model |
tests::requires_a_unit |
REQ-4 — ✅ VERIFIED¶
set 6/6 passed
Case |
State |
Level |
Via |
Selector |
Note |
|---|---|---|---|---|---|
//:controller_test#tests.test_controller::test_accepts_range_limits |
passed |
simulation |
model |
tests.test_controller::test_accepts_range_limits |
|
//:controller_test#tests.test_controller::test_rejects_setpoints_outside_range[4.9] |
passed |
simulation |
model |
tests.test_controller::test_rejects_setpoints_outside_range[*] |
|
//:controller_test#tests.test_controller::test_rejects_setpoints_outside_range[30.1] |
passed |
simulation |
model |
tests.test_controller::test_rejects_setpoints_outside_range[*] |
|
//:controller_test#tests.test_controller::test_rejects_setpoints_outside_range[80.0] |
passed |
simulation |
model |
tests.test_controller::test_rejects_setpoints_outside_range[*] |
|
//:setpoint_test#tests::checks_the_range_after_converting |
passed |
simulation |
model |
tests::checks_the_range_after_converting |
|
//:setpoint_test#tests::rejects_out_of_range |
passed |
simulation |
model |
tests::rejects_out_of_range |
REQ-5 — ✅ VERIFIED¶
set 3/3 passed
Case |
State |
Level |
Via |
Selector |
Note |
|---|---|---|---|---|---|
//:interlock_test#Interlock::NanReadingTrips |
passed |
sil |
model |
Interlock::* |
|
//:interlock_test#Interlock::StaysTrippedUntilBelowReset |
passed |
sil |
model |
Interlock::* |
|
//:interlock_test#Interlock::TripsAtLimit |
passed |
sil |
model |
Interlock::* |
REQ-6 — ✅ VERIFIED¶
set 3/3 passed
Case |
State |
Level |
Via |
Selector |
Note |
|---|---|---|---|---|---|
//:controller_test#tests.test_controller::test_invalid_reading_turns_heater_off[85.1] |
passed |
simulation |
model |
tests.test_controller::test_invalid_reading_turns_heater_off[*] |
|
//:controller_test#tests.test_controller::test_invalid_reading_turns_heater_off[-41.0] |
passed |
simulation |
model |
tests.test_controller::test_invalid_reading_turns_heater_off[*] |
|
//:controller_test#tests.test_controller::test_invalid_reading_turns_heater_off[nan] |
passed |
simulation |
model |
tests.test_controller::test_invalid_reading_turns_heater_off[*] |
REQ-7 — ✅ VERIFIED¶
set 2/2 passed
Case |
State |
Level |
Via |
Selector |
Note |
|---|---|---|---|---|---|
//:display_test#display_test.DisplayTest::test_setpoint_shows_unit |
passed |
simulation |
model |
display_test.DisplayTest::test_setpoint_shows_unit |
|
record:panel_inspection#inspection.TM-2::panel-shows-setpoint-unit |
passed |
inspection |
model |
inspection.TM-2::panel-shows-setpoint-unit |
Case attribution¶
Target |
Cases |
Owned |
Quarantined |
Unowned |
Owners |
|---|---|---|---|---|---|
//:controller_test |
9 |
9 |
0 |
0 |
REQ-1, REQ-2, REQ-4, REQ-6 |
//:display_test |
1 |
1 |
0 |
0 |
REQ-7 |
//:interlock_test |
3 |
3 |
0 |
0 |
REQ-5 |
//:setpoint_test |
4 |
4 |
0 |
0 |
REQ-3, REQ-4 |
record:panel_inspection |
1 |
1 |
0 |
0 |
REQ-7 |
8. Things to try¶
Change something and watch the golden test tell you what it did to the traceability argument:
Demand more rigor. Point REQ-5’s
methodat a new test method withlevel: hil. REQ-5 becomes UNDER-VERIFIED (its evidence issil), MIT-1 and RISK-1 become PARTIAL, and RISK-1 — a high-severity risk — is hoisted into the “not mitigated” banner. The gap queue gains anunder-verifieditem routed tohuman-gate, ahigh-risk-openitem, and apyramiditem.Break the code. Remove the hysteresis in
thermostat/controller.py: REQ-2 turns FAILED, and with it UN-1’s validation and the golden test.Lose the sign-off. Delete
evidence/panel_inspection.rr.yamlfrom the report’s evidence: REQ-7’s claimed inspection record did not run, so its set is not whole — REQ-7 reads INCOMPLETE and UN-2 PARTIAL — and theincompletegap is routed tohuman-gate, because the missing member is an inspection (the claim’slevel).Name two requirements in one test. Put
rr::verifies!("REQ-3", "REQ-4")back intorequires_a_unit: the case is quarantined, REQ-3 and REQ-4 read INVALID, andbazel build //:reportfails (rr reportexits 3) with anATTRIBUTION ERROR: multi-tagline naming the case.Claim one case twice. Add
tests::requires_*to REQ-4’s claims on//:setpoint_test://:model_testfails with theshared-caseerror shown in step 3, before any test runs.
Accept an intended change with bazel run //:report_md_golden_test.update
(and its json twin); the diff of the golden file is the change to the
traceability argument, reviewed alongside the code.