lockwire 0.2.0__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 (51) hide show
  1. lockwire-0.2.0/PKG-INFO +192 -0
  2. lockwire-0.2.0/README.md +160 -0
  3. lockwire-0.2.0/pyproject.toml +103 -0
  4. lockwire-0.2.0/src/lockwire/__init__.py +9 -0
  5. lockwire-0.2.0/src/lockwire/__main__.py +9 -0
  6. lockwire-0.2.0/src/lockwire/advice.py +119 -0
  7. lockwire-0.2.0/src/lockwire/api.py +1216 -0
  8. lockwire-0.2.0/src/lockwire/api_payload.py +985 -0
  9. lockwire-0.2.0/src/lockwire/bench.py +182 -0
  10. lockwire-0.2.0/src/lockwire/blame.py +100 -0
  11. lockwire-0.2.0/src/lockwire/catalog.py +100 -0
  12. lockwire-0.2.0/src/lockwire/catalog_seed.yaml +545 -0
  13. lockwire-0.2.0/src/lockwire/checks.py +3483 -0
  14. lockwire-0.2.0/src/lockwire/cli.py +1799 -0
  15. lockwire-0.2.0/src/lockwire/cli_line.py +73 -0
  16. lockwire-0.2.0/src/lockwire/constraints.py +313 -0
  17. lockwire-0.2.0/src/lockwire/copy.py +302 -0
  18. lockwire-0.2.0/src/lockwire/describe.py +682 -0
  19. lockwire-0.2.0/src/lockwire/edit.py +2098 -0
  20. lockwire-0.2.0/src/lockwire/emit_ts.py +150 -0
  21. lockwire-0.2.0/src/lockwire/export_mhs.py +115 -0
  22. lockwire-0.2.0/src/lockwire/factdiff.py +254 -0
  23. lockwire-0.2.0/src/lockwire/forces.py +334 -0
  24. lockwire-0.2.0/src/lockwire/gitref.py +188 -0
  25. lockwire-0.2.0/src/lockwire/hw.py +388 -0
  26. lockwire-0.2.0/src/lockwire/mcp_server.py +276 -0
  27. lockwire-0.2.0/src/lockwire/mechanism.py +379 -0
  28. lockwire-0.2.0/src/lockwire/mesh_dae.py +555 -0
  29. lockwire-0.2.0/src/lockwire/mesh_simple.py +198 -0
  30. lockwire-0.2.0/src/lockwire/page.py +1485 -0
  31. lockwire-0.2.0/src/lockwire/plan.py +849 -0
  32. lockwire-0.2.0/src/lockwire/plan_payload.py +151 -0
  33. lockwire-0.2.0/src/lockwire/plugins.py +70 -0
  34. lockwire-0.2.0/src/lockwire/report.py +848 -0
  35. lockwire-0.2.0/src/lockwire/report_html.py +707 -0
  36. lockwire-0.2.0/src/lockwire/report_json.py +168 -0
  37. lockwire-0.2.0/src/lockwire/review_draft.py +315 -0
  38. lockwire-0.2.0/src/lockwire/scan.py +732 -0
  39. lockwire-0.2.0/src/lockwire/scene.py +535 -0
  40. lockwire-0.2.0/src/lockwire/scene_import.py +309 -0
  41. lockwire-0.2.0/src/lockwire/scene_urdf.py +750 -0
  42. lockwire-0.2.0/src/lockwire/scene_view.py +376 -0
  43. lockwire-0.2.0/src/lockwire/schema.py +3616 -0
  44. lockwire-0.2.0/src/lockwire/sdf.py +240 -0
  45. lockwire-0.2.0/src/lockwire/serve.py +1256 -0
  46. lockwire-0.2.0/src/lockwire/start_operation.py +92 -0
  47. lockwire-0.2.0/src/lockwire/statics.py +77 -0
  48. lockwire-0.2.0/src/lockwire/units.py +115 -0
  49. lockwire-0.2.0/src/lockwire/urdf.py +348 -0
  50. lockwire-0.2.0/src/lockwire/vcs.py +402 -0
  51. lockwire-0.2.0/src/lockwire/whatif.py +161 -0
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: lockwire
3
+ Version: 0.2.0
4
+ Summary: The physical-assumption contract for robots: physics lockfile + preflight gate
5
+ Keywords: robotics,urdf,ros,preflight,lockfile,ci,hardware,drone
6
+ Author: Shivansh
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Intended Audience :: Manufacturing
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Dist: numpy>=2.5.1
18
+ Requires-Dist: pydantic>=2.13.4
19
+ Requires-Dist: rich>=15.0.0
20
+ Requires-Dist: ruamel-yaml>=0.19.1
21
+ Requires-Dist: typer>=0.26.8
22
+ Requires-Dist: mcp>=2.1.1 ; extra == 'mcp'
23
+ Requires-Dist: fastapi>=0.141.1 ; extra == 'serve'
24
+ Requires-Dist: uvicorn>=0.52.4 ; extra == 'serve'
25
+ Requires-Python: >=3.12
26
+ Project-URL: Homepage, https://github.com/Shivansh2703/lockwire
27
+ Project-URL: Repository, https://github.com/Shivansh2703/lockwire
28
+ Project-URL: Issues, https://github.com/Shivansh2703/lockwire/issues
29
+ Provides-Extra: mcp
30
+ Provides-Extra: serve
31
+ Description-Content-Type: text/markdown
32
+
33
+ # lockwire
34
+
35
+ **Find out your robot won't work before you buy the parts.**
36
+
37
+ Every robot is a stack of numbers from a dozen datasheets: what each part weighs, what each power rail can supply, how much the bus can carry, how much torque each joint gets. Lockwire keeps those numbers in one file and tells you the moment they stop adding up. Use it while you're still planning a build, then keep it in CI so a part swap can't quietly break it later.
38
+
39
+ Prefer clicking to typing? The free desktop app does the same visually: Open, Design, Declare, Plan, Check.
40
+
41
+ ## Plan a robot from scratch
42
+
43
+ Start from a URDF (one line is enough), add the parts you're thinking of buying, and check:
44
+
45
+ ```bash
46
+ pip install "git+https://github.com/Shivansh2703/lockwire@v0.2.0" # not on PyPI
47
+ echo '<robot name="rover"><link name="base_link"/></robot>' > robot.urdf
48
+ lockwire start robot.urdf
49
+ lockwire add rail lockwire.yaml --id rail.5v --voltage "5 V" --capacity "15 W" --source "UBEC datasheet: 5 V 3 A" --witness Sam
50
+ lockwire add device lockwire.yaml --id dev.pi5 --family fam.pi --model mod.pi5 --rail rail.5v --draw "12 W" --mass "46 g" --mount-frame base_link --owner you --source "Raspberry Pi 5 product brief" --witness Sam
51
+ lockwire add device lockwire.yaml --id dev.servo --family fam.servo --model mod.mg996r --rail rail.5v --draw "6 W" --mass "55 g" --mount-frame base_link --owner you --source "MG996R datasheet" --witness Sam
52
+ lockwire add budget lockwire.yaml --id bud.5v --kind rail_power --rail rail.5v --limit "15 W" --owner you --source "UBEC datasheet" --witness Sam
53
+ lockwire check lockwire.yaml
54
+ ```
55
+
56
+ `--witness` is the person who read the number off the datasheet; use a real name, not "you".
57
+
58
+ ```console
59
+ VIOLATION budgets/bud.5v — observed 18.00 W, expected <= 15.00 W (Δ +3.00 W (+20%))
60
+ dependent: dev.pi5, dev.servo
61
+ owner: you
62
+ source: UBEC datasheet: 5 V 3 A
63
+ action: shed load on rail 'rail.5v' or raise its capacity with provenance
64
+ warning model/links/base_link/mass — observed zero or missing mass, expected a positive mass with units
65
+ …
66
+ scored 2: 0 add up, 1 do not · 0 not scored
67
+
68
+ NO-GO — 1 violation(s), 1 warning(s)
69
+ ```
70
+
71
+ The check exits 1. A Pi 5 and one servo on a common 3 A regulator: caught before you ordered either. Swap in a 5 A regulator and it passes.
72
+
73
+ ## A real-world example: the X500 drone
74
+
75
+ ```bash
76
+ git clone https://github.com/Shivansh2703/lockwire && cd lockwire # the X500 example lives in the repo, not the wheel
77
+ lockwire check tests/fixtures/x500/contract.yaml
78
+ ```
79
+
80
+ Output (trimmed):
81
+
82
+ ```console
83
+ $ lockwire check tests/fixtures/x500/contract.yaml
84
+ VIOLATION budgets/bud.power.pm02-5v — observed 35.20 W (draw mode: nominal), expected <= 15.60 W (Δ +19.60 W (+126%))
85
+ dependent: dev.camera.oak-d-lite, dev.companion.jetson-orin-nano, dev.radio.rfd900x
86
+ owner: power-integration
87
+ action: shed load on rail 'rail.pm02-5v' or raise its capacity with provenance
88
+ VIOLATION budgets/bud.power.pm02-5v.7w-mode — observed 17.20 W (draw mode: 7W), expected <= 15.60 W (Δ +1.60 W (+10%))
89
+ … (same rail, Jetson in its 7 W mode: the radio's transmit peak still tips it over)
90
+ not scored devices/dev.camera.oak-d-lite/physical/mass — observed mass not declared, expected a positive mass with units
91
+ … (more not-scored lines: things the lockfile does not declare yet, listed, never hidden)
92
+ scored 10: 8 add up, 2 do not · 9 not scored
93
+
94
+ NO-GO — 2 violation(s), 0 warning(s)
95
+ $ echo $?
96
+ 1
97
+ ```
98
+
99
+ ## What just happened
100
+
101
+ A Holybro PM02 power module publishes a regulated 5 V rail at 3 A: 15.6 W. An NVIDIA Jetson
102
+ Orin Nano publishes a 25 W power mode at 5 V: 5 A. Put a camera on the same rail and the
103
+ published parts **cannot power the published companion computer**. This is a custom
104
+ integration of published parts, and the two vendors' own numbers still do not add up.
105
+
106
+ Nobody printed "this build is broken". Nobody printed "5 A" either: that is 25 W / 5 V, a
107
+ quotient of figures published on different pages. The records disagree with each other, and
108
+ you find out before you apply power. Give the Jetson its own regulator
109
+ (`tests/fixtures/x500/contract_corrected.yaml`) and the same check passes.
110
+
111
+ ## Your own robot
112
+
113
+ `lockwire start` reads links, joints, masses and the structure hash from your URDF or SDF. A fresh
114
+ lockfile knows the frame and nothing else; fill in devices, rails and buses as you go, and
115
+ `check` lists what it still could not check.
116
+
117
+ A lockfile whose `schema_version` this lockwire does not recognise is refused before any check runs.
118
+ `check` prints `REFUSED — contract declares schema_version '0.99'; this lockwire supports 0.2, 0.3, 0.4` and exits 2.
119
+ A lockfile at an older supported version is accepted as-is, with no automatic upgrade.
120
+
121
+ Re-running `lockwire start --force` on the same URDF is destructive:
122
+ re-declare every fact in the lockfile by hand, because no command carries those facts forward.
123
+ Any field you hand-edited (including `robot.name`) is silently overwritten back to the URDF-derived value.
124
+
125
+ ## Exit codes
126
+
127
+ | command | 0 | 1 | 2 | 3 |
128
+ |---|---|---|---|---|
129
+ | `check` | `GO`, `GO with notes` | `NO-GO` | a contract that could not be found, read, or parsed | `NOT SCORED` (nothing compared) |
130
+ | `start` | the contract was written (or an existing one kept); no page is written | the URDF/SDF could not be parsed, or the contract could not be read or written | — | — |
131
+
132
+ `NOT SCORED` (exit 3) means no comparison had both operands: it is not a pass and not a failure.
133
+ The `scored N: …` line above the verdict is where a script should look.
134
+
135
+ ## In CI
136
+
137
+ ```yaml
138
+ - uses: actions/checkout@v4
139
+ - uses: Shivansh2703/lockwire@v0 # composite action: installs lockwire, runs the check, uploads an HTML report
140
+ with:
141
+ contract: lockwire.yaml
142
+ ```
143
+
144
+ Inputs: `contract` (default `lockwire.yaml`), `version` (default latest), `python-version` (default 3.12).
145
+ Or just `pip install "git+https://github.com/Shivansh2703/lockwire@v0.2.0" && lockwire check lockwire.yaml`.
146
+
147
+ ## Pre-commit
148
+
149
+ ```yaml
150
+ repos:
151
+ - repo: https://github.com/Shivansh2703/lockwire
152
+ rev: v0.2.0
153
+ hooks:
154
+ - id: lockwire-check
155
+ ```
156
+
157
+ Runs `lockwire check` (on `lockwire.yaml` in the repo root) whenever a lockfile changes.
158
+
159
+ ## What it checks
160
+
161
+ Each check compares **two separately declared figures**, each with its own provenance line.
162
+ It shows who said what; it cannot verify that a datasheet is true.
163
+
164
+ | check | source A | source B |
165
+ |---|---|---|
166
+ | rail power budget | device draw (datasheet) | rail capacity (power module datasheet) |
167
+ | rail tree rollup | child rail draw / efficiency | parent rail capacity |
168
+ | torque budget | joint effort limit (URDF) | actuator continuous torque x gear ratio |
169
+ | bus bandwidth | message rate x payload | bus bitrate x utilisation ceiling |
170
+ | mass budget | per-device mass | declared total |
171
+ | endurance | pack energy | summed draw |
172
+ | voltage compatibility | rail voltage | device input voltage range |
173
+ | reference integrity | every `bus_ref`, `mount_frame`, dependency edge | the thing it points at |
174
+
175
+ No physics simulation, no machine learning, no network. The answer is the same twice.
176
+
177
+ ## Free and Pro
178
+
179
+ - **Free forever (this repo):** the lockfile, all deterministic checks including
180
+ power/thermal/bus/link/torque budgets, edit / describe / plan / what-if, the MCP server, the
181
+ HTML report, and the desktop planning app (Open, Design, Declare, Plan, Check).
182
+ - **Lockwire Pro** adds the powered-on preflight gate (Test, Run), recording analysis
183
+ (rosbag2 / ULog), and health.
184
+
185
+ ## Development
186
+
187
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Python 3.12 or newer; runtime dependencies are
188
+ pydantic, typer, rich, ruamel.yaml and numpy.
189
+
190
+ ## Licence
191
+
192
+ Apache-2.0. See [LICENSE](LICENSE).
@@ -0,0 +1,160 @@
1
+ # lockwire
2
+
3
+ **Find out your robot won't work before you buy the parts.**
4
+
5
+ Every robot is a stack of numbers from a dozen datasheets: what each part weighs, what each power rail can supply, how much the bus can carry, how much torque each joint gets. Lockwire keeps those numbers in one file and tells you the moment they stop adding up. Use it while you're still planning a build, then keep it in CI so a part swap can't quietly break it later.
6
+
7
+ Prefer clicking to typing? The free desktop app does the same visually: Open, Design, Declare, Plan, Check.
8
+
9
+ ## Plan a robot from scratch
10
+
11
+ Start from a URDF (one line is enough), add the parts you're thinking of buying, and check:
12
+
13
+ ```bash
14
+ pip install "git+https://github.com/Shivansh2703/lockwire@v0.2.0" # not on PyPI
15
+ echo '<robot name="rover"><link name="base_link"/></robot>' > robot.urdf
16
+ lockwire start robot.urdf
17
+ lockwire add rail lockwire.yaml --id rail.5v --voltage "5 V" --capacity "15 W" --source "UBEC datasheet: 5 V 3 A" --witness Sam
18
+ lockwire add device lockwire.yaml --id dev.pi5 --family fam.pi --model mod.pi5 --rail rail.5v --draw "12 W" --mass "46 g" --mount-frame base_link --owner you --source "Raspberry Pi 5 product brief" --witness Sam
19
+ lockwire add device lockwire.yaml --id dev.servo --family fam.servo --model mod.mg996r --rail rail.5v --draw "6 W" --mass "55 g" --mount-frame base_link --owner you --source "MG996R datasheet" --witness Sam
20
+ lockwire add budget lockwire.yaml --id bud.5v --kind rail_power --rail rail.5v --limit "15 W" --owner you --source "UBEC datasheet" --witness Sam
21
+ lockwire check lockwire.yaml
22
+ ```
23
+
24
+ `--witness` is the person who read the number off the datasheet; use a real name, not "you".
25
+
26
+ ```console
27
+ VIOLATION budgets/bud.5v — observed 18.00 W, expected <= 15.00 W (Δ +3.00 W (+20%))
28
+ dependent: dev.pi5, dev.servo
29
+ owner: you
30
+ source: UBEC datasheet: 5 V 3 A
31
+ action: shed load on rail 'rail.5v' or raise its capacity with provenance
32
+ warning model/links/base_link/mass — observed zero or missing mass, expected a positive mass with units
33
+ …
34
+ scored 2: 0 add up, 1 do not · 0 not scored
35
+
36
+ NO-GO — 1 violation(s), 1 warning(s)
37
+ ```
38
+
39
+ The check exits 1. A Pi 5 and one servo on a common 3 A regulator: caught before you ordered either. Swap in a 5 A regulator and it passes.
40
+
41
+ ## A real-world example: the X500 drone
42
+
43
+ ```bash
44
+ git clone https://github.com/Shivansh2703/lockwire && cd lockwire # the X500 example lives in the repo, not the wheel
45
+ lockwire check tests/fixtures/x500/contract.yaml
46
+ ```
47
+
48
+ Output (trimmed):
49
+
50
+ ```console
51
+ $ lockwire check tests/fixtures/x500/contract.yaml
52
+ VIOLATION budgets/bud.power.pm02-5v — observed 35.20 W (draw mode: nominal), expected <= 15.60 W (Δ +19.60 W (+126%))
53
+ dependent: dev.camera.oak-d-lite, dev.companion.jetson-orin-nano, dev.radio.rfd900x
54
+ owner: power-integration
55
+ action: shed load on rail 'rail.pm02-5v' or raise its capacity with provenance
56
+ VIOLATION budgets/bud.power.pm02-5v.7w-mode — observed 17.20 W (draw mode: 7W), expected <= 15.60 W (Δ +1.60 W (+10%))
57
+ … (same rail, Jetson in its 7 W mode: the radio's transmit peak still tips it over)
58
+ not scored devices/dev.camera.oak-d-lite/physical/mass — observed mass not declared, expected a positive mass with units
59
+ … (more not-scored lines: things the lockfile does not declare yet, listed, never hidden)
60
+ scored 10: 8 add up, 2 do not · 9 not scored
61
+
62
+ NO-GO — 2 violation(s), 0 warning(s)
63
+ $ echo $?
64
+ 1
65
+ ```
66
+
67
+ ## What just happened
68
+
69
+ A Holybro PM02 power module publishes a regulated 5 V rail at 3 A: 15.6 W. An NVIDIA Jetson
70
+ Orin Nano publishes a 25 W power mode at 5 V: 5 A. Put a camera on the same rail and the
71
+ published parts **cannot power the published companion computer**. This is a custom
72
+ integration of published parts, and the two vendors' own numbers still do not add up.
73
+
74
+ Nobody printed "this build is broken". Nobody printed "5 A" either: that is 25 W / 5 V, a
75
+ quotient of figures published on different pages. The records disagree with each other, and
76
+ you find out before you apply power. Give the Jetson its own regulator
77
+ (`tests/fixtures/x500/contract_corrected.yaml`) and the same check passes.
78
+
79
+ ## Your own robot
80
+
81
+ `lockwire start` reads links, joints, masses and the structure hash from your URDF or SDF. A fresh
82
+ lockfile knows the frame and nothing else; fill in devices, rails and buses as you go, and
83
+ `check` lists what it still could not check.
84
+
85
+ A lockfile whose `schema_version` this lockwire does not recognise is refused before any check runs.
86
+ `check` prints `REFUSED — contract declares schema_version '0.99'; this lockwire supports 0.2, 0.3, 0.4` and exits 2.
87
+ A lockfile at an older supported version is accepted as-is, with no automatic upgrade.
88
+
89
+ Re-running `lockwire start --force` on the same URDF is destructive:
90
+ re-declare every fact in the lockfile by hand, because no command carries those facts forward.
91
+ Any field you hand-edited (including `robot.name`) is silently overwritten back to the URDF-derived value.
92
+
93
+ ## Exit codes
94
+
95
+ | command | 0 | 1 | 2 | 3 |
96
+ |---|---|---|---|---|
97
+ | `check` | `GO`, `GO with notes` | `NO-GO` | a contract that could not be found, read, or parsed | `NOT SCORED` (nothing compared) |
98
+ | `start` | the contract was written (or an existing one kept); no page is written | the URDF/SDF could not be parsed, or the contract could not be read or written | — | — |
99
+
100
+ `NOT SCORED` (exit 3) means no comparison had both operands: it is not a pass and not a failure.
101
+ The `scored N: …` line above the verdict is where a script should look.
102
+
103
+ ## In CI
104
+
105
+ ```yaml
106
+ - uses: actions/checkout@v4
107
+ - uses: Shivansh2703/lockwire@v0 # composite action: installs lockwire, runs the check, uploads an HTML report
108
+ with:
109
+ contract: lockwire.yaml
110
+ ```
111
+
112
+ Inputs: `contract` (default `lockwire.yaml`), `version` (default latest), `python-version` (default 3.12).
113
+ Or just `pip install "git+https://github.com/Shivansh2703/lockwire@v0.2.0" && lockwire check lockwire.yaml`.
114
+
115
+ ## Pre-commit
116
+
117
+ ```yaml
118
+ repos:
119
+ - repo: https://github.com/Shivansh2703/lockwire
120
+ rev: v0.2.0
121
+ hooks:
122
+ - id: lockwire-check
123
+ ```
124
+
125
+ Runs `lockwire check` (on `lockwire.yaml` in the repo root) whenever a lockfile changes.
126
+
127
+ ## What it checks
128
+
129
+ Each check compares **two separately declared figures**, each with its own provenance line.
130
+ It shows who said what; it cannot verify that a datasheet is true.
131
+
132
+ | check | source A | source B |
133
+ |---|---|---|
134
+ | rail power budget | device draw (datasheet) | rail capacity (power module datasheet) |
135
+ | rail tree rollup | child rail draw / efficiency | parent rail capacity |
136
+ | torque budget | joint effort limit (URDF) | actuator continuous torque x gear ratio |
137
+ | bus bandwidth | message rate x payload | bus bitrate x utilisation ceiling |
138
+ | mass budget | per-device mass | declared total |
139
+ | endurance | pack energy | summed draw |
140
+ | voltage compatibility | rail voltage | device input voltage range |
141
+ | reference integrity | every `bus_ref`, `mount_frame`, dependency edge | the thing it points at |
142
+
143
+ No physics simulation, no machine learning, no network. The answer is the same twice.
144
+
145
+ ## Free and Pro
146
+
147
+ - **Free forever (this repo):** the lockfile, all deterministic checks including
148
+ power/thermal/bus/link/torque budgets, edit / describe / plan / what-if, the MCP server, the
149
+ HTML report, and the desktop planning app (Open, Design, Declare, Plan, Check).
150
+ - **Lockwire Pro** adds the powered-on preflight gate (Test, Run), recording analysis
151
+ (rosbag2 / ULog), and health.
152
+
153
+ ## Development
154
+
155
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Python 3.12 or newer; runtime dependencies are
156
+ pydantic, typer, rich, ruamel.yaml and numpy.
157
+
158
+ ## Licence
159
+
160
+ Apache-2.0. See [LICENSE](LICENSE).
@@ -0,0 +1,103 @@
1
+ [project]
2
+ name = "lockwire"
3
+ version = "0.2.0"
4
+ description = "The physical-assumption contract for robots: physics lockfile + preflight gate"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Shivansh" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ license = "Apache-2.0"
11
+ keywords = ["robotics", "urdf", "ros", "preflight", "lockfile", "ci", "hardware", "drone"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "License :: OSI Approved :: Apache Software License",
15
+ "Intended Audience :: Manufacturing",
16
+ "Intended Audience :: Science/Research",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Scientific/Engineering",
20
+ "Topic :: Software Development :: Quality Assurance",
21
+ "Topic :: Software Development :: Testing",
22
+ ]
23
+ dependencies = [
24
+ "numpy>=2.5.1",
25
+ "pydantic>=2.13.4",
26
+ "rich>=15.0.0",
27
+ "ruamel-yaml>=0.19.1",
28
+ "typer>=0.26.8",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ # Serving lockwire over MCP to an agent. Optional for the same reason as `ros2bag`:
33
+ # a plain install must stay lean, and the SDK is only needed by `lockwire mcp`.
34
+ mcp = [
35
+ "mcp>=2.1.1",
36
+ ]
37
+ # The Electron app's sidecar: `lockwire serve` over local HTTP. Optional for the
38
+ # same reason as `desktop`/`ros2bag`/`mcp` — a plain CLI install stays lean; only the
39
+ # packaged app build and anyone running the sidecar by hand need these. Versions are
40
+ # today's PyPI current stable (measured, E1-a).
41
+ serve = [
42
+ "fastapi>=0.141.1",
43
+ "uvicorn>=0.52.4",
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://github.com/Shivansh2703/lockwire"
48
+ Repository = "https://github.com/Shivansh2703/lockwire"
49
+ Issues = "https://github.com/Shivansh2703/lockwire/issues"
50
+
51
+ [project.scripts]
52
+ lockwire = "lockwire:main"
53
+
54
+ [build-system]
55
+ requires = ["uv_build>=0.11.26,<0.12.0"]
56
+ build-backend = "uv_build"
57
+
58
+ [tool.uv.build-backend]
59
+
60
+ [dependency-groups]
61
+ dev = [
62
+ "mcp>=2.1.1",
63
+ # tests/test_serve.py needs the sidecar (`lockwire serve`) importable and
64
+ # runnable as a subprocess. Same reasoning as `mcp`/`mcap` above: CI's
65
+ # `test` job runs a plain `uv sync --dev`, so without these here that
66
+ # whole test file would silently never run. Dev-only: the runtime `serve`
67
+ # extra above stays optional so a plain install stays lean.
68
+ "fastapi>=0.141.1",
69
+ "uvicorn>=0.52.4",
70
+ "httpx>=0.28.1",
71
+ # Freezing the sidecar binary: `uv run pyinstaller packaging/lockwire.spec`. Dev-only —
72
+ # it is a build tool, never imported at runtime — and `tests/test_cli.py` imports it to
73
+ # anchor the uvicorn hidden-import test against uvicorn's real module list (box e1-d2).
74
+ "pyinstaller>=6.21.0",
75
+ "pytest>=9.1.1",
76
+ "pytest-xdist>=3.6.1",
77
+ "ruff>=0.15.20,<0.16", # 0.17 changes default rules
78
+ "pytest-cov>=7.1.0",
79
+ ]
80
+
81
+ [tool.ruff]
82
+ # This codebase is written to ~100 columns: schema.py and checks.py carry aligned
83
+ # trailing comments naming each field's consumer, and wrapping at ruff's default 88
84
+ # splits those annotations off the line they annotate. E501 is not in ruff's default
85
+ # rule set, so before this block the two documented commands disagreed — `ruff check`
86
+ # passed while `ruff format` wanted to rewrite 12 files. At 100 the sweep is a net
87
+ # reduction (83 insertions / 105 deletions); at 88 it was +219/-80.
88
+ line-length = 100
89
+
90
+ # measure_stacked_gate.py bootstraps sys.path before imports (B76) — E402 is intentional there.
91
+ [tool.ruff.lint.per-file-ignores]
92
+ "scripts/measure_stacked_gate.py" = ["E402"]
93
+
94
+ [tool.pytest.ini_options]
95
+ pythonpath = ["."]
96
+ testpaths = ["tests"]
97
+ # chrome: drives real headless Chrome (subprocess spawn). scripts/pytest_slot.sh
98
+ # excludes this marker from every non-exclusive (targeted) run so workers' test
99
+ # runs never spawn Chrome processes; the cadence Check (--exclusive) still runs
100
+ # them. Registered here so --strict-markers, if ever turned on, does not bite.
101
+ markers = [
102
+ "chrome: drives real headless Chrome; excluded from targeted runs by scripts/pytest_slot.sh",
103
+ ]
@@ -0,0 +1,9 @@
1
+ """Lockwire — the physical-assumption contract for robots, enforced in CI and at preflight."""
2
+
3
+ __version__ = "0.2.0"
4
+
5
+
6
+ def main() -> None:
7
+ from lockwire.cli import app
8
+
9
+ app()
@@ -0,0 +1,9 @@
1
+ """``python -m lockwire`` — the same CLI the ``lockwire`` script runs.
2
+
3
+ Exists so ``jobs.py`` can start a run as an argv list against ``sys.executable``
4
+ without depending on a console script being on PATH.
5
+ """
6
+
7
+ from lockwire.cli import app
8
+
9
+ app()
@@ -0,0 +1,119 @@
1
+ """The alternatives half of the advice sentence.
2
+
3
+ ``check_budgets`` (``checks.py``) already knows a rail_power budget is over its
4
+ limit; this module answers the second half of the sentence — "what else could
5
+ carry it" — headroom on every OTHER rail_power budget, plus a projected
6
+ regulator suggestion. It computes nothing about rail membership itself:
7
+ Reuse means ``checks.power_rollup``'s own return values (rail totals, the
8
+ per-rail unknown-consumer sets) are passed in as plain data, never re-walked.
9
+ This module never imports ``checks`` — the call only ever goes the other way,
10
+ so there is no cycle.
11
+
12
+ Every number carries its ``kind`` (a catalog or projected
13
+ figure is shown only with its own visual mark and word, never in the same
14
+ style as a measured one): headroom is ``derived-by-<check>`` — the same
15
+ rollup ``check_budgets`` itself scores against; a suggested regulator rating
16
+ is ``projected``, a number this product is choosing, not measuring.
17
+
18
+ **Raising the over-budget rail's own declared capacity is never offered**
19
+ : that clears the finding by moving the limit rather than
20
+ the draw, which leaves a standing mark. Offering it as
21
+ advice would make the product recommend the exact thing that mark exists to flag.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from pydantic import BaseModel, ConfigDict, model_validator
27
+
28
+ from lockwire.schema import Contract
29
+
30
+
31
+ class Alternative(BaseModel):
32
+ """One remedy suggestion, its number, and the kind that number carries."""
33
+
34
+ model_config = ConfigDict(extra="forbid")
35
+
36
+ text: str
37
+ value: float
38
+ unit: str
39
+ kind: str # "derived-by-<check>" | "projected" | "none"
40
+ #: The word for a ``projected`` figure. Required when ``kind ==
41
+ #: "projected"`` — a projected figure reaching a door carries its mark and
42
+ #: its word, not a corrected sentence saying it is unenforced.
43
+ mark: str | None = None
44
+
45
+ @model_validator(mode="after")
46
+ def _carries_its_word(self) -> Alternative:
47
+ if self.kind == "projected" and not self.mark:
48
+ raise ValueError("a 'projected' number must carry its own word, e.g. mark='projected'")
49
+ return self
50
+
51
+
52
+ def build_rail_alternatives(
53
+ contract: Contract,
54
+ *,
55
+ over_rail_id: str,
56
+ deficit_w: float,
57
+ rail_totals: dict[str, float],
58
+ rail_unknown: dict[str, set[str]],
59
+ check_name: str = "check_budgets",
60
+ ) -> list[Alternative]:
61
+ """Alternatives for a rail_power budget over its limit on ``over_rail_id``.
62
+
63
+ ``rail_totals``/``rail_unknown`` must come from ``checks.power_rollup`` run
64
+ against the same contract and draw mode as the violation being explained —
65
+ the shared builder that every call site (and every mutation bite) goes
66
+ through, so a broken kind anywhere breaks every caller alike.
67
+ """
68
+ alternatives: list[Alternative] = []
69
+ for budget in contract.budgets:
70
+ if budget.kind != "rail_power" or budget.rail in (None, over_rail_id):
71
+ continue
72
+ if rail_unknown.get(budget.rail):
73
+ # a rail with any consumer of unknown draw cannot be summed,
74
+ # so it cannot be claimed to have headroom either.
75
+ continue
76
+ used = rail_totals.get(budget.rail, 0.0)
77
+ headroom_w = budget.limit.base() - used
78
+ if headroom_w > 0.0:
79
+ alternatives.append(
80
+ Alternative(
81
+ text=(
82
+ f"move the device to rail {budget.rail!r}, which has "
83
+ f"{headroom_w:.2f} W free"
84
+ ),
85
+ value=headroom_w,
86
+ unit="W",
87
+ kind=f"derived-by-{check_name}",
88
+ )
89
+ )
90
+
91
+ over_rail = contract.rail(over_rail_id)
92
+ source_rail_id = "battery"
93
+ if over_rail is not None and over_rail.upstream is not None:
94
+ source_rail_id = over_rail.upstream
95
+ rating_w = max(deficit_w, 0.0)
96
+ if rating_w > 0.0:
97
+ alternatives.append(
98
+ Alternative(
99
+ text=f"add a {rating_w:.2f} W regulator fed from rail {source_rail_id!r}",
100
+ value=rating_w,
101
+ unit="W",
102
+ kind="projected",
103
+ mark="projected",
104
+ )
105
+ )
106
+
107
+ if not alternatives:
108
+ alternatives.append(
109
+ Alternative(
110
+ text=(
111
+ f"no other rail has headroom for {over_rail_id!r} and no regulator "
112
+ "rating could be projected"
113
+ ),
114
+ value=0.0,
115
+ unit="W",
116
+ kind="none",
117
+ )
118
+ )
119
+ return alternatives