realbench-robotics 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. realbench_robotics-0.2.1/LICENSE +21 -0
  2. realbench_robotics-0.2.1/PKG-INFO +146 -0
  3. realbench_robotics-0.2.1/README.md +115 -0
  4. realbench_robotics-0.2.1/pyproject.toml +54 -0
  5. realbench_robotics-0.2.1/setup.cfg +4 -0
  6. realbench_robotics-0.2.1/src/realbench/__init__.py +78 -0
  7. realbench_robotics-0.2.1/src/realbench/acknowledgements.py +216 -0
  8. realbench_robotics-0.2.1/src/realbench/analyze.py +318 -0
  9. realbench_robotics-0.2.1/src/realbench/capture.py +208 -0
  10. realbench_robotics-0.2.1/src/realbench/checks.py +1234 -0
  11. realbench_robotics-0.2.1/src/realbench/cli.py +847 -0
  12. realbench_robotics-0.2.1/src/realbench/components.py +641 -0
  13. realbench_robotics-0.2.1/src/realbench/discovery.py +357 -0
  14. realbench_robotics-0.2.1/src/realbench/doctor.py +829 -0
  15. realbench_robotics-0.2.1/src/realbench/export.py +71 -0
  16. realbench_robotics-0.2.1/src/realbench/gazebo_source.py +395 -0
  17. realbench_robotics-0.2.1/src/realbench/home_resolve.py +31 -0
  18. realbench_robotics-0.2.1/src/realbench/messages.py +181 -0
  19. realbench_robotics-0.2.1/src/realbench/mjcf_model.py +169 -0
  20. realbench_robotics-0.2.1/src/realbench/orchestrator.py +231 -0
  21. realbench_robotics-0.2.1/src/realbench/paths.py +35 -0
  22. realbench_robotics-0.2.1/src/realbench/recorder.py +542 -0
  23. realbench_robotics-0.2.1/src/realbench/records.py +1007 -0
  24. realbench_robotics-0.2.1/src/realbench/report.py +613 -0
  25. realbench_robotics-0.2.1/src/realbench/reset.py +105 -0
  26. realbench_robotics-0.2.1/src/realbench/robot_model.py +56 -0
  27. realbench_robotics-0.2.1/src/realbench/ros2_source.py +1924 -0
  28. realbench_robotics-0.2.1/src/realbench/sensor_extract.py +180 -0
  29. realbench_robotics-0.2.1/src/realbench/state.py +48 -0
  30. realbench_robotics-0.2.1/src/realbench/state_source.py +515 -0
  31. realbench_robotics-0.2.1/src/realbench/urdf_model.py +298 -0
  32. realbench_robotics-0.2.1/src/realbench/verdict.py +95 -0
  33. realbench_robotics-0.2.1/src/realbench/versions.py +170 -0
  34. realbench_robotics-0.2.1/src/realbench/web/__init__.py +1 -0
  35. realbench_robotics-0.2.1/src/realbench/web/app.py +1280 -0
  36. realbench_robotics-0.2.1/src/realbench/web/cloud.py +97 -0
  37. realbench_robotics-0.2.1/src/realbench/web/environment.py +54 -0
  38. realbench_robotics-0.2.1/src/realbench/web/runs.py +127 -0
  39. realbench_robotics-0.2.1/src/realbench/web/scan.py +149 -0
  40. realbench_robotics-0.2.1/src/realbench/web/serve.py +97 -0
  41. realbench_robotics-0.2.1/src/realbench/web/static/assets/index-Cn25aiky.js +9 -0
  42. realbench_robotics-0.2.1/src/realbench/web/static/assets/index-RodZfQe3.css +1 -0
  43. realbench_robotics-0.2.1/src/realbench/web/static/favicon.svg +1 -0
  44. realbench_robotics-0.2.1/src/realbench/web/static/icons.svg +24 -0
  45. realbench_robotics-0.2.1/src/realbench/web/static/index.html +16 -0
  46. realbench_robotics-0.2.1/src/realbench/web/static/realbench_logo.png +0 -0
  47. realbench_robotics-0.2.1/src/realbench_robotics.egg-info/PKG-INFO +146 -0
  48. realbench_robotics-0.2.1/src/realbench_robotics.egg-info/SOURCES.txt +78 -0
  49. realbench_robotics-0.2.1/src/realbench_robotics.egg-info/dependency_links.txt +1 -0
  50. realbench_robotics-0.2.1/src/realbench_robotics.egg-info/entry_points.txt +2 -0
  51. realbench_robotics-0.2.1/src/realbench_robotics.egg-info/requires.txt +15 -0
  52. realbench_robotics-0.2.1/src/realbench_robotics.egg-info/top_level.txt +1 -0
  53. realbench_robotics-0.2.1/tests/test_analyze.py +121 -0
  54. realbench_robotics-0.2.1/tests/test_bench_api.py +261 -0
  55. realbench_robotics-0.2.1/tests/test_capture_home.py +176 -0
  56. realbench_robotics-0.2.1/tests/test_checks.py +493 -0
  57. realbench_robotics-0.2.1/tests/test_cli.py +158 -0
  58. realbench_robotics-0.2.1/tests/test_components.py +618 -0
  59. realbench_robotics-0.2.1/tests/test_doctor.py +301 -0
  60. realbench_robotics-0.2.1/tests/test_dwell.py +113 -0
  61. realbench_robotics-0.2.1/tests/test_error_messages.py +318 -0
  62. realbench_robotics-0.2.1/tests/test_export.py +106 -0
  63. realbench_robotics-0.2.1/tests/test_field_loss.py +220 -0
  64. realbench_robotics-0.2.1/tests/test_gazebo_source.py +59 -0
  65. realbench_robotics-0.2.1/tests/test_mjcf_model.py +140 -0
  66. realbench_robotics-0.2.1/tests/test_orchestrator.py +161 -0
  67. realbench_robotics-0.2.1/tests/test_recorder.py +554 -0
  68. realbench_robotics-0.2.1/tests/test_recorder_ros2_live.py +99 -0
  69. realbench_robotics-0.2.1/tests/test_report.py +137 -0
  70. realbench_robotics-0.2.1/tests/test_ros2_source.py +433 -0
  71. realbench_robotics-0.2.1/tests/test_sensor_arrival.py +161 -0
  72. realbench_robotics-0.2.1/tests/test_setup.py +459 -0
  73. realbench_robotics-0.2.1/tests/test_setup_incomplete.py +345 -0
  74. realbench_robotics-0.2.1/tests/test_source_capabilities.py +96 -0
  75. realbench_robotics-0.2.1/tests/test_state_source.py +91 -0
  76. realbench_robotics-0.2.1/tests/test_timeline.py +202 -0
  77. realbench_robotics-0.2.1/tests/test_urdf_model.py +95 -0
  78. realbench_robotics-0.2.1/tests/test_verdict.py +149 -0
  79. realbench_robotics-0.2.1/tests/test_versions.py +342 -0
  80. realbench_robotics-0.2.1/tests/test_web.py +214 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Raghavan Madabushi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: realbench-robotics
3
+ Version: 0.2.1
4
+ Summary: Fail-closed robot readiness checks for integration-test reset cycles
5
+ Author-email: Raghavan Madabushi <raghavanmadabushi10@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/realbench-robotics/
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Topic :: Scientific/Engineering
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: yourdfpy>=0.0.56
18
+ Requires-Dist: numpy>=1.24
19
+ Requires-Dist: rich>=13
20
+ Requires-Dist: typer>=0.12
21
+ Requires-Dist: fastapi>=0.110
22
+ Requires-Dist: uvicorn[standard]>=0.27
23
+ Requires-Dist: python-multipart>=0.0.9
24
+ Requires-Dist: defusedxml>=0.7.1
25
+ Requires-Dist: mujoco>=3.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest; extra == "dev"
28
+ Requires-Dist: httpx; extra == "dev"
29
+ Provides-Extra: ros2
30
+ Dynamic: license-file
31
+
32
+ # RealBench
33
+
34
+ Fail-closed readiness checks that answer: is this robot verifiably ready for the next run?
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install realbench-robotics
40
+ ```
41
+
42
+ The import package and CLI remain `realbench`. `pip install realbench-robotics[ros2]` does not install `rclpy`; that comes from your ROS2 distro.
43
+
44
+ ## ROS2 warning
45
+
46
+ A positions-only ROS2 connection **cannot reach READY**. Faults and actuator state stay unverifiable, and the run quarantines by design.
47
+
48
+ This is the command that can reach READY:
49
+
50
+ ```bash
51
+ realbench check --urdf robot.urdf --source ros2 \
52
+ --ros2-topic /joint_states \
53
+ --diagnostics-topic /diagnostics \
54
+ --controller-manager /controller_manager
55
+ ```
56
+
57
+ Run `realbench doctor --urdf robot.urdf --source ros2` first if anything is missing.
58
+
59
+ `realbench check --source ros2` with no sensors or nodes declared exits 3
60
+ (usage error) and does not print a readiness verdict. Open the Components
61
+ tab and save, or run `realbench capture-components`.
62
+
63
+ ## Quickstart
64
+
65
+ ```bash
66
+ realbench analyze --urdf robot.urdf
67
+ realbench doctor --urdf robot.urdf --source ros2
68
+ realbench capture-home --urdf robot.urdf --source ros2 --repo .
69
+ realbench check --urdf robot.urdf --source ros2 \
70
+ --diagnostics-topic /diagnostics \
71
+ --controller-manager /controller_manager
72
+ ```
73
+
74
+ `--urdf` accepts a URDF or MJCF file. `analyze` needs only the model. `doctor` diagnoses setup and never produces a readiness verdict. `capture-home` writes `<repo>/.realbench/home.json`. `check` and `cycle` look for that file when `--home` is omitted.
75
+
76
+ ## Serve
77
+
78
+ ```bash
79
+ realbench serve --source ros2 --host 0.0.0.0 --no-browser
80
+ ```
81
+
82
+ No `--urdf`. The model is read from the latched `*/robot_description` topic
83
+ (TRANSIENT_LOCAL `std_msgs/String`). Joint states, diagnostics, and
84
+ controller manager are inferred from the graph, including namespaced
85
+ paths such as `/JK3/platform/joint_states`. Continuous-joint robots skip
86
+ home pose.
87
+
88
+ Until at least one sensor or node is declared, the stored verdict is
89
+ **SETUP INCOMPLETE**, not QUARANTINE. The Components tab pre-ticks
90
+ discoverable sensors and nodes so save is one click. Saving a LaserScan
91
+ or Imu also writes a default `value_field` and plausible range so those
92
+ checks evaluate (PASS or FAIL), not NOT_APPLICABLE.
93
+
94
+ A failing diagnostic, sensor, or node can be marked a known issue with a
95
+ required note. Acknowledged rows stay recorded and visible; they do not
96
+ drive `classify()` until the reason string changes.
97
+
98
+ Host wall-clock is stored next to robot time. If they disagree by more
99
+ than 5 seconds, `check_clock_sane` fails at HEALTH and the UI shows both
100
+ clocks.
101
+
102
+ `check_state_stream_fresh` uses 3× the observed `joint_states` period and
103
+ 3× the recorder sample interval, floored at 2.0s.
104
+
105
+ ## Export
106
+
107
+ Do not copy `.realbench/` while serve is running. The recording database
108
+ is WAL-mode; a folder copy drops the `-wal` file.
109
+
110
+ History lives in `<repo>/.realbench/records.db`. There is no separate
111
+ `runs.db` on the serve path.
112
+
113
+ ```bash
114
+ realbench export
115
+ realbench export --out capture.db --repo .
116
+ ```
117
+
118
+ SQLite's backup API writes one consistent file, prints path and size, and
119
+ only reports success if `integrity_check` passes. Safe while serve is up.
120
+
121
+ ## Exit codes
122
+
123
+ | Code | Meaning |
124
+ |------|---------|
125
+ | 0 | READY |
126
+ | 1 | NOT_READY_RECOVERABLE (pose can be reset) |
127
+ | 2 | QUARANTINE (liveness/health failed or unverifiable) |
128
+ | 3 | Could not run (missing URDF, home, source, or undeclared ROS2 components) |
129
+
130
+ `doctor` uses 0 (no blocking setup problems), 1 (problems that would block a useful verdict), and 3 (could not run).
131
+
132
+ ## CI
133
+
134
+ ```bash
135
+ realbench check --urdf robot.urdf --source ros2 --diagnostics-topic /diagnostics --controller-manager /controller_manager || { echo "robot not ready, aborting run"; exit 1; }
136
+ ```
137
+
138
+ ## Read-only
139
+
140
+ Read-only. RealBench never commands motion, never writes firmware, and never modifies your robot.
141
+
142
+ ## Known limits
143
+
144
+ - Reset execution is stubbed.
145
+ - Sensor and firmware checks are not implemented.
146
+ - The ROS2 path is validated against a Dockerised Jazzy node, not physical hardware.
@@ -0,0 +1,115 @@
1
+ # RealBench
2
+
3
+ Fail-closed readiness checks that answer: is this robot verifiably ready for the next run?
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install realbench-robotics
9
+ ```
10
+
11
+ The import package and CLI remain `realbench`. `pip install realbench-robotics[ros2]` does not install `rclpy`; that comes from your ROS2 distro.
12
+
13
+ ## ROS2 warning
14
+
15
+ A positions-only ROS2 connection **cannot reach READY**. Faults and actuator state stay unverifiable, and the run quarantines by design.
16
+
17
+ This is the command that can reach READY:
18
+
19
+ ```bash
20
+ realbench check --urdf robot.urdf --source ros2 \
21
+ --ros2-topic /joint_states \
22
+ --diagnostics-topic /diagnostics \
23
+ --controller-manager /controller_manager
24
+ ```
25
+
26
+ Run `realbench doctor --urdf robot.urdf --source ros2` first if anything is missing.
27
+
28
+ `realbench check --source ros2` with no sensors or nodes declared exits 3
29
+ (usage error) and does not print a readiness verdict. Open the Components
30
+ tab and save, or run `realbench capture-components`.
31
+
32
+ ## Quickstart
33
+
34
+ ```bash
35
+ realbench analyze --urdf robot.urdf
36
+ realbench doctor --urdf robot.urdf --source ros2
37
+ realbench capture-home --urdf robot.urdf --source ros2 --repo .
38
+ realbench check --urdf robot.urdf --source ros2 \
39
+ --diagnostics-topic /diagnostics \
40
+ --controller-manager /controller_manager
41
+ ```
42
+
43
+ `--urdf` accepts a URDF or MJCF file. `analyze` needs only the model. `doctor` diagnoses setup and never produces a readiness verdict. `capture-home` writes `<repo>/.realbench/home.json`. `check` and `cycle` look for that file when `--home` is omitted.
44
+
45
+ ## Serve
46
+
47
+ ```bash
48
+ realbench serve --source ros2 --host 0.0.0.0 --no-browser
49
+ ```
50
+
51
+ No `--urdf`. The model is read from the latched `*/robot_description` topic
52
+ (TRANSIENT_LOCAL `std_msgs/String`). Joint states, diagnostics, and
53
+ controller manager are inferred from the graph, including namespaced
54
+ paths such as `/JK3/platform/joint_states`. Continuous-joint robots skip
55
+ home pose.
56
+
57
+ Until at least one sensor or node is declared, the stored verdict is
58
+ **SETUP INCOMPLETE**, not QUARANTINE. The Components tab pre-ticks
59
+ discoverable sensors and nodes so save is one click. Saving a LaserScan
60
+ or Imu also writes a default `value_field` and plausible range so those
61
+ checks evaluate (PASS or FAIL), not NOT_APPLICABLE.
62
+
63
+ A failing diagnostic, sensor, or node can be marked a known issue with a
64
+ required note. Acknowledged rows stay recorded and visible; they do not
65
+ drive `classify()` until the reason string changes.
66
+
67
+ Host wall-clock is stored next to robot time. If they disagree by more
68
+ than 5 seconds, `check_clock_sane` fails at HEALTH and the UI shows both
69
+ clocks.
70
+
71
+ `check_state_stream_fresh` uses 3× the observed `joint_states` period and
72
+ 3× the recorder sample interval, floored at 2.0s.
73
+
74
+ ## Export
75
+
76
+ Do not copy `.realbench/` while serve is running. The recording database
77
+ is WAL-mode; a folder copy drops the `-wal` file.
78
+
79
+ History lives in `<repo>/.realbench/records.db`. There is no separate
80
+ `runs.db` on the serve path.
81
+
82
+ ```bash
83
+ realbench export
84
+ realbench export --out capture.db --repo .
85
+ ```
86
+
87
+ SQLite's backup API writes one consistent file, prints path and size, and
88
+ only reports success if `integrity_check` passes. Safe while serve is up.
89
+
90
+ ## Exit codes
91
+
92
+ | Code | Meaning |
93
+ |------|---------|
94
+ | 0 | READY |
95
+ | 1 | NOT_READY_RECOVERABLE (pose can be reset) |
96
+ | 2 | QUARANTINE (liveness/health failed or unverifiable) |
97
+ | 3 | Could not run (missing URDF, home, source, or undeclared ROS2 components) |
98
+
99
+ `doctor` uses 0 (no blocking setup problems), 1 (problems that would block a useful verdict), and 3 (could not run).
100
+
101
+ ## CI
102
+
103
+ ```bash
104
+ realbench check --urdf robot.urdf --source ros2 --diagnostics-topic /diagnostics --controller-manager /controller_manager || { echo "robot not ready, aborting run"; exit 1; }
105
+ ```
106
+
107
+ ## Read-only
108
+
109
+ Read-only. RealBench never commands motion, never writes firmware, and never modifies your robot.
110
+
111
+ ## Known limits
112
+
113
+ - Reset execution is stubbed.
114
+ - Sensor and firmware checks are not implemented.
115
+ - The ROS2 path is validated against a Dockerised Jazzy node, not physical hardware.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "realbench-robotics"
7
+ version = "0.2.1"
8
+ description = "Fail-closed robot readiness checks for integration-test reset cycles"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [
12
+ { name = "Raghavan Madabushi", email = "raghavanmadabushi10@gmail.com" },
13
+ ]
14
+ requires-python = ">=3.10"
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Topic :: Scientific/Engineering",
22
+ ]
23
+ dependencies = [
24
+ "yourdfpy>=0.0.56",
25
+ "numpy>=1.24",
26
+ "rich>=13",
27
+ "typer>=0.12",
28
+ "fastapi>=0.110",
29
+ "uvicorn[standard]>=0.27",
30
+ "python-multipart>=0.0.9",
31
+ "defusedxml>=0.7.1",
32
+ "mujoco>=3.0",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://pypi.org/project/realbench-robotics/"
37
+
38
+ [project.optional-dependencies]
39
+ dev = ["pytest", "httpx"]
40
+ # rclpy is provided by the ROS2 distro, not pip; this extra is a marker only.
41
+ ros2 = []
42
+
43
+ [project.scripts]
44
+ realbench = "realbench.cli:app"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.setuptools.package-data]
50
+ realbench = ["web/static/*", "web/static/assets/*"]
51
+
52
+ [tool.pytest.ini_options]
53
+ pythonpath = ["src"]
54
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,78 @@
1
+ """RealBench: fail-closed readiness checks for robot integration-test reset cycles."""
2
+
3
+ from realbench.orchestrator import CycleReport, evaluate_readiness, run_cycle
4
+ from realbench.report import exit_code_for, render_terminal, to_json
5
+ from realbench.reset import MockResetExecutor, ResetExecutor, ResetOutcome
6
+ from realbench.verdict import ReadinessReport, Verdict, classify
7
+ from realbench.checks import (
8
+ ALL_CHECKS,
9
+ CheckConfig,
10
+ CheckResult,
11
+ CheckStatus,
12
+ CheckTier,
13
+ run_all_checks,
14
+ )
15
+ from realbench.gazebo_source import (
16
+ GazeboStateSource,
17
+ GzLiveStateSource,
18
+ parse_gz_joint_state,
19
+ )
20
+ from realbench.ros2_source import Ros2StateSource, Ros2Unavailable
21
+ from realbench.state import RobotState
22
+ from realbench.state_source import (
23
+ MockStateSource,
24
+ SourceCapabilities,
25
+ StateLoadError,
26
+ StateSource,
27
+ coverage_warning,
28
+ empty_robot_state,
29
+ )
30
+ from realbench.urdf_model import (
31
+ HomePoseError,
32
+ JointLimit,
33
+ ModelProvenance,
34
+ UrdfLoadError,
35
+ UrdfModel,
36
+ load_urdf_model,
37
+ )
38
+ from realbench.robot_model import load_robot_model
39
+
40
+ __all__ = [
41
+ "ALL_CHECKS",
42
+ "CheckConfig",
43
+ "CheckResult",
44
+ "CheckStatus",
45
+ "CheckTier",
46
+ "CycleReport",
47
+ "GazeboStateSource",
48
+ "GzLiveStateSource",
49
+ "HomePoseError",
50
+ "JointLimit",
51
+ "ModelProvenance",
52
+ "MockResetExecutor",
53
+ "MockStateSource",
54
+ "ReadinessReport",
55
+ "ResetExecutor",
56
+ "Ros2StateSource",
57
+ "Ros2Unavailable",
58
+ "ResetOutcome",
59
+ "RobotState",
60
+ "SourceCapabilities",
61
+ "StateLoadError",
62
+ "StateSource",
63
+ "coverage_warning",
64
+ "empty_robot_state",
65
+ "UrdfLoadError",
66
+ "UrdfModel",
67
+ "Verdict",
68
+ "classify",
69
+ "evaluate_readiness",
70
+ "exit_code_for",
71
+ "load_robot_model",
72
+ "load_urdf_model",
73
+ "parse_gz_joint_state",
74
+ "render_terminal",
75
+ "run_all_checks",
76
+ "run_cycle",
77
+ "to_json",
78
+ ]
@@ -0,0 +1,216 @@
1
+ """Known-issue acknowledgements. classify() is unchanged; copies are filtered here."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from dataclasses import replace
7
+
8
+ from realbench.checks import CheckResult, CheckStatus
9
+ from realbench.components import (
10
+ Acknowledgement,
11
+ ComponentsDeclaration,
12
+ rewrite_acknowledgements,
13
+ )
14
+ from realbench.state import RobotState
15
+
16
+ _BLOCKING = {CheckStatus.FAIL, CheckStatus.UNVERIFIABLE}
17
+ _MEMBER_CHECKS = frozenset(
18
+ {
19
+ "check_sensors_reporting",
20
+ "check_sensors_plausible",
21
+ "check_sensors_changing",
22
+ "check_nodes_alive",
23
+ }
24
+ )
25
+
26
+
27
+ def split_fault(fault: str) -> tuple[str, str]:
28
+ """Split ``\"name: message\"`` from diagnostics_to_faults."""
29
+
30
+ if ": " in fault:
31
+ name, message = fault.split(": ", 1)
32
+ return name, message
33
+ return fault, ""
34
+
35
+
36
+ def current_failing_reasons(
37
+ state: RobotState,
38
+ checks: Sequence[CheckResult],
39
+ ) -> dict[str, set[str]]:
40
+ """Map component id → current failing reason strings.
41
+
42
+ One component can fail in more than one check (reporting vs changing).
43
+ Keeping a set is what lets a written ack be read back instead of
44
+ being overwritten by the later check.
45
+ """
46
+
47
+ reasons: dict[str, set[str]] = {}
48
+ for fault in state.faults or ():
49
+ name, _message = split_fault(fault)
50
+ reasons.setdefault(name, set()).add(fault)
51
+ for check in checks:
52
+ for component_id, reason in _failing_members(check):
53
+ reasons.setdefault(component_id, set()).add(reason)
54
+ return reasons
55
+
56
+
57
+ def fault_is_acknowledged(
58
+ fault: str,
59
+ acknowledgements: Mapping[str, Acknowledgement],
60
+ ) -> bool:
61
+ name, message = split_fault(fault)
62
+ for component_id, ack in acknowledgements.items():
63
+ if ack.reason == fault:
64
+ return True
65
+ if component_id == name and ack.reason in (fault, message):
66
+ return True
67
+ return False
68
+
69
+
70
+ def member_is_acknowledged(
71
+ component_id: str,
72
+ reason: str,
73
+ acknowledgements: Mapping[str, Acknowledgement],
74
+ ) -> bool:
75
+ del reason
76
+ return component_id in acknowledgements
77
+
78
+
79
+ def acknowledgement_is_stale(
80
+ ack: Acknowledgement,
81
+ current_reasons: Mapping[str, set[str]],
82
+ faults: Sequence[str],
83
+ ) -> bool:
84
+ """True when this id is currently failing with a different reason."""
85
+
86
+ currents = current_reasons.get(ack.component_id)
87
+ if currents:
88
+ return ack.reason not in currents
89
+ related = [
90
+ fault
91
+ for fault in faults
92
+ if split_fault(fault)[0] == ack.component_id or ack.reason == fault
93
+ ]
94
+ if not related:
95
+ return False
96
+ return not any(
97
+ ack.reason == fault or ack.reason == split_fault(fault)[1] for fault in related
98
+ )
99
+
100
+
101
+ def prune_stale_acknowledgements(
102
+ declaration: ComponentsDeclaration | None,
103
+ state: RobotState,
104
+ checks: Sequence[CheckResult],
105
+ ) -> ComponentsDeclaration | None:
106
+ """Drop acks whose stored reason no longer matches. Writes the file."""
107
+
108
+ if declaration is None or not declaration.acknowledgements:
109
+ return declaration
110
+ reasons = current_failing_reasons(state, checks)
111
+ faults = list(state.faults or ())
112
+ kept: dict[str, Acknowledgement] = {}
113
+ for component_id, ack in declaration.acknowledgements.items():
114
+ if acknowledgement_is_stale(ack, reasons, faults):
115
+ continue
116
+ kept[component_id] = ack
117
+ if kept == dict(declaration.acknowledgements):
118
+ return declaration
119
+ updated = replace(declaration, acknowledgements=kept)
120
+ if declaration.path is not None:
121
+ try:
122
+ rewrite_acknowledgements(declaration.path, kept)
123
+ except OSError:
124
+ pass
125
+ return updated
126
+
127
+
128
+ def active_acknowledgements(
129
+ declaration: ComponentsDeclaration | None,
130
+ state: RobotState,
131
+ checks: Sequence[CheckResult],
132
+ ) -> dict[str, Acknowledgement]:
133
+ """Acks that currently match a failing reason and suppress classify input."""
134
+
135
+ if declaration is None:
136
+ return {}
137
+ reasons = current_failing_reasons(state, checks)
138
+ faults = list(state.faults or ())
139
+ active: dict[str, Acknowledgement] = {}
140
+ for component_id, ack in declaration.acknowledgements.items():
141
+ if acknowledgement_is_stale(ack, reasons, faults):
142
+ continue
143
+ currents = reasons.get(component_id)
144
+ if currents and ack.reason in currents:
145
+ active[component_id] = ack
146
+ continue
147
+ if any(fault_is_acknowledged(fault, {component_id: ack}) for fault in faults):
148
+ active[component_id] = ack
149
+ return active
150
+
151
+
152
+ def known_issues_acknowledged(
153
+ declaration: ComponentsDeclaration | None,
154
+ state: RobotState,
155
+ checks: Sequence[CheckResult],
156
+ ) -> int:
157
+ return len(active_acknowledgements(declaration, state, checks))
158
+
159
+
160
+ def checks_for_verdict(
161
+ checks: Sequence[CheckResult],
162
+ state: RobotState,
163
+ acknowledgements: Mapping[str, Acknowledgement] | None,
164
+ ) -> list[CheckResult]:
165
+ """Copies for classify(). Original check objects are not mutated."""
166
+
167
+ acks = acknowledgements or {}
168
+ out: list[CheckResult] = []
169
+ for check in checks:
170
+ if check.status not in _BLOCKING:
171
+ out.append(check)
172
+ continue
173
+ if check.name == "check_no_latched_faults":
174
+ faults = state.faults
175
+ if (
176
+ isinstance(faults, list)
177
+ and len(faults) > 0
178
+ and all(fault_is_acknowledged(item, acks) for item in faults)
179
+ ):
180
+ out.append(replace(check, status=CheckStatus.PASS))
181
+ else:
182
+ out.append(check)
183
+ continue
184
+ if check.name in _MEMBER_CHECKS:
185
+ members = _failing_members(check)
186
+ if members and all(
187
+ member_is_acknowledged(component_id, reason, acks)
188
+ for component_id, reason in members
189
+ ):
190
+ out.append(replace(check, status=CheckStatus.PASS))
191
+ else:
192
+ out.append(check)
193
+ continue
194
+ out.append(check)
195
+ return out
196
+
197
+
198
+ def _failing_members(check: CheckResult) -> list[tuple[str, str]]:
199
+ observed = check.observed
200
+ if not isinstance(observed, dict):
201
+ return []
202
+ components = observed.get("components")
203
+ if not isinstance(components, dict):
204
+ return []
205
+ members: list[tuple[str, str]] = []
206
+ for component_id, spec in components.items():
207
+ if not isinstance(spec, dict):
208
+ continue
209
+ if spec.get("required") is False:
210
+ continue
211
+ status = spec.get("status")
212
+ if status in ("FAIL", "UNVERIFIABLE", CheckStatus.FAIL, CheckStatus.UNVERIFIABLE):
213
+ members.append(
214
+ (str(component_id), str(spec.get("reason") or check.reason))
215
+ )
216
+ return members