lockwire 0.2.0__py3-none-any.whl
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.
- lockwire/__init__.py +9 -0
- lockwire/__main__.py +9 -0
- lockwire/advice.py +119 -0
- lockwire/api.py +1216 -0
- lockwire/api_payload.py +985 -0
- lockwire/bench.py +182 -0
- lockwire/blame.py +100 -0
- lockwire/catalog.py +100 -0
- lockwire/catalog_seed.yaml +545 -0
- lockwire/checks.py +3483 -0
- lockwire/cli.py +1799 -0
- lockwire/cli_line.py +73 -0
- lockwire/constraints.py +313 -0
- lockwire/copy.py +302 -0
- lockwire/describe.py +682 -0
- lockwire/edit.py +2098 -0
- lockwire/emit_ts.py +150 -0
- lockwire/export_mhs.py +115 -0
- lockwire/factdiff.py +254 -0
- lockwire/forces.py +334 -0
- lockwire/gitref.py +188 -0
- lockwire/hw.py +388 -0
- lockwire/mcp_server.py +276 -0
- lockwire/mechanism.py +379 -0
- lockwire/mesh_dae.py +555 -0
- lockwire/mesh_simple.py +198 -0
- lockwire/page.py +1485 -0
- lockwire/plan.py +849 -0
- lockwire/plan_payload.py +151 -0
- lockwire/plugins.py +70 -0
- lockwire/report.py +848 -0
- lockwire/report_html.py +707 -0
- lockwire/report_json.py +168 -0
- lockwire/review_draft.py +315 -0
- lockwire/scan.py +732 -0
- lockwire/scene.py +535 -0
- lockwire/scene_import.py +309 -0
- lockwire/scene_urdf.py +750 -0
- lockwire/scene_view.py +376 -0
- lockwire/schema.py +3616 -0
- lockwire/sdf.py +240 -0
- lockwire/serve.py +1256 -0
- lockwire/start_operation.py +92 -0
- lockwire/statics.py +77 -0
- lockwire/units.py +115 -0
- lockwire/urdf.py +348 -0
- lockwire/vcs.py +402 -0
- lockwire/whatif.py +161 -0
- lockwire-0.2.0.dist-info/METADATA +192 -0
- lockwire-0.2.0.dist-info/RECORD +52 -0
- lockwire-0.2.0.dist-info/WHEEL +4 -0
- lockwire-0.2.0.dist-info/entry_points.txt +3 -0
lockwire/plan_payload.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""The plan document's typed shape, free: what ``lockwire plan --json`` writes.
|
|
2
|
+
|
|
3
|
+
``api_payload`` (the app's generated-types source) imports these names from here,
|
|
4
|
+
so the open-source ``api.plan`` and the full product emit one document. Classes
|
|
5
|
+
moved verbatim: the generated schema and ``api.gen.ts`` depend on their names
|
|
6
|
+
and docstrings.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
14
|
+
|
|
15
|
+
from lockwire.plan import AssertionOutcome
|
|
16
|
+
from lockwire.report import Finding
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Strict(BaseModel):
|
|
20
|
+
model_config = ConfigDict(extra="forbid")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Document(_Strict):
|
|
24
|
+
"""``report_json.envelope``'s three header fields, plus the one field the
|
|
25
|
+
MCP transport adds in place (``mcp_server._with_cli_line``). Every machine
|
|
26
|
+
member leads with these; ``artifact_type`` is the discriminator
|
|
27
|
+
already in place, so the union does not invent a second one."""
|
|
28
|
+
|
|
29
|
+
format_version: str
|
|
30
|
+
schema_version: str
|
|
31
|
+
cli_line: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Counts(_Strict):
|
|
35
|
+
violation: int
|
|
36
|
+
warning: int
|
|
37
|
+
not_scored: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
#: ``report.verdict_word``'s spelling, not the raw ``VerdictCode``.
|
|
41
|
+
VerdictWord = Literal["REFUSED", "NO-GO", "GO_WITH_NOTES", "GO", "NOT_SCORED"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _VerdictDocument(_Document):
|
|
45
|
+
"""``api.verdict_document``, field for field — the one function behind
|
|
46
|
+
``check`` and ``plan``, so one model behind both members."""
|
|
47
|
+
|
|
48
|
+
robot: str
|
|
49
|
+
verdict: VerdictWord
|
|
50
|
+
exit_code: int
|
|
51
|
+
counts: Counts
|
|
52
|
+
scored: int | None
|
|
53
|
+
findings: list[Finding]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Instead(_Strict):
|
|
57
|
+
"""One short "Instead" row: verb + number + the kind word (copy.instead_row)."""
|
|
58
|
+
|
|
59
|
+
text: str
|
|
60
|
+
kind: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FindingLabel(_Strict):
|
|
64
|
+
"""One finding's human name and typed targets, index-aligned with ``findings``."""
|
|
65
|
+
|
|
66
|
+
label: str
|
|
67
|
+
targets: list["Target"]
|
|
68
|
+
#: True only for a robot-wide budget (no rail): the whole robot, not "no part found".
|
|
69
|
+
whole: bool = False
|
|
70
|
+
instead: list[Instead] = []
|
|
71
|
+
fix: str = ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PlanPayload(_VerdictDocument):
|
|
75
|
+
"""``api.plan`` / ``lockwire plan --json``. ``outcomes`` rides along only
|
|
76
|
+
when the report carries them (``plan.PlanReport``), exactly as
|
|
77
|
+
``api.verdict_document`` attaches them. ``steps`` is joined from the
|
|
78
|
+
report's own outcomes and findings before the document is emitted."""
|
|
79
|
+
|
|
80
|
+
artifact_type: Literal["lockwire-plan-report"]
|
|
81
|
+
outcomes: list[AssertionOutcome] | None = None
|
|
82
|
+
steps: list[PlanStepPayload] = []
|
|
83
|
+
finding_labels: list[FindingLabel] = []
|
|
84
|
+
|
|
85
|
+
@model_validator(mode="after")
|
|
86
|
+
def _join_steps(self) -> "PlanPayload":
|
|
87
|
+
if self.outcomes:
|
|
88
|
+
self.steps = plan_steps(self.outcomes, self.findings)
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Target(_Strict):
|
|
93
|
+
"""What a card is about, typed by the engine so the app never parses text."""
|
|
94
|
+
|
|
95
|
+
kind: Literal["joint", "link", "device", "rail"]
|
|
96
|
+
id: str
|
|
97
|
+
#: Short display name (the id minus its machine prefix); the id stays the tooltip.
|
|
98
|
+
label: str
|
|
99
|
+
#: Scene part ids (contract ids) this target lights in the 3D view: a device or link is its
|
|
100
|
+
#: own part; a rail is the devices drawing from it (and downstream rails). Engine-resolved.
|
|
101
|
+
parts: list[str] = []
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PlanStepPayload(_Strict):
|
|
105
|
+
"""One plan step's disposition (the app shows each agent call
|
|
106
|
+
and lockwire's reply as a read-only timeline, from the command record; the
|
|
107
|
+
operator sees what the agent was told, and nothing in the app
|
|
108
|
+
steers the agent), joined from ``PlanReport``'s own ``outcomes`` and
|
|
109
|
+
``findings`` — ``plan.py`` already computes both; this only correlates
|
|
110
|
+
what it returns (no second scorer). The join key is identity:
|
|
111
|
+
``AssertionOutcome.path == Finding.quantity`` — every ``UNSCORED`` outcome
|
|
112
|
+
``validate_plan`` emits carries exactly one ``not_scored`` Finding at the
|
|
113
|
+
same path, whose ``action`` names the missing fact.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
path: str
|
|
117
|
+
status: Literal["PASS", "FAIL", "UNSCORED", "ERROR"]
|
|
118
|
+
#: Never empty for an UNSCORED step (a coverage gap is named, not
|
|
119
|
+
#: silent). None for a scored step — there is nothing missing to name.
|
|
120
|
+
not_scored_reason: str | None
|
|
121
|
+
#: What the step touches, typed by the engine (served `/v1/run` plan only; [] on the CLI document).
|
|
122
|
+
targets: list["Target"] = []
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def plan_steps(outcomes: list[AssertionOutcome], findings: list[Finding]) -> list[PlanStepPayload]:
|
|
126
|
+
"""Correlate ``validate_plan``'s outcomes with its own findings by path.
|
|
127
|
+
|
|
128
|
+
Reused by ``PlanPayload`` (the CLI/MCP/library document) rather than
|
|
129
|
+
reimplemented — one join, one place.
|
|
130
|
+
"""
|
|
131
|
+
reasons = {f.quantity: f.action for f in findings if f.severity == "not_scored"}
|
|
132
|
+
steps = []
|
|
133
|
+
for outcome in outcomes:
|
|
134
|
+
reason = reasons.get(outcome.path) if outcome.status == "UNSCORED" else None
|
|
135
|
+
if outcome.status == "UNSCORED" and reason is None:
|
|
136
|
+
msg = (
|
|
137
|
+
f"join miss: UNSCORED outcome at {outcome.path} has no matching not_scored finding"
|
|
138
|
+
)
|
|
139
|
+
raise ValueError(msg)
|
|
140
|
+
steps.append(
|
|
141
|
+
PlanStepPayload(
|
|
142
|
+
path=outcome.path,
|
|
143
|
+
status=outcome.status,
|
|
144
|
+
not_scored_reason=reason,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
return steps
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
FindingLabel.model_rebuild()
|
|
151
|
+
PlanPayload.model_rebuild()
|
lockwire/plugins.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""The plugin seam: extensions (e.g. the paid package) register through the
|
|
2
|
+
``lockwire.plugins`` entry-point group. Free code names no plugin."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import entry_points
|
|
7
|
+
from typing import Any, Callable, NamedTuple
|
|
8
|
+
|
|
9
|
+
# A plugin may swap in a richer implementation of a library function (same name,
|
|
10
|
+
# same signature); free callers go through `pick`, so without a plugin they get their own.
|
|
11
|
+
OVERRIDES: dict[str, Callable[..., Any]] = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Payload members a plugin adds to the generated app types: enveloped ones by
|
|
15
|
+
# `artifact_type`, the rest in a list, and their number-kind rows.
|
|
16
|
+
PAYLOAD_MEMBERS: dict[str, type] = {}
|
|
17
|
+
PAYLOAD_EXTRA: list[type] = []
|
|
18
|
+
PAYLOAD_NUMBER_KINDS: dict[str, dict[str, str]] = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Hooks that add fields to the page payload: each is called as
|
|
22
|
+
# `hook(contract=, contract_path=, preflight=, run_findings=, redact_paths=, payload=)` and returns a dict
|
|
23
|
+
# merged over the payload (so a plugin may also replace `modes`, `mode_labels`, `command_modes`).
|
|
24
|
+
PAGE_EXTRAS: list[Callable[..., dict[str, Any]]] = []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ServeCommand(NamedTuple):
|
|
28
|
+
"""One sidecar command: `handler`, its `args` model, and its request model (a pydantic
|
|
29
|
+
class with a `command: Literal[...]` field). A plugin's handler is called `(body, state)`."""
|
|
30
|
+
|
|
31
|
+
handler: Callable[..., Any]
|
|
32
|
+
args: type
|
|
33
|
+
command: type
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Sidecar commands a plugin adds (or replaces, by name) beside the free ones; callables that
|
|
37
|
+
# prepare the app's `state` at construction; and the factory behind `lockwire serve --profile`,
|
|
38
|
+
# `(profile, required) -> Callable[[request], request]`, raising ValueError to refuse.
|
|
39
|
+
SERVE_COMMANDS: dict[str, ServeCommand] = {}
|
|
40
|
+
SERVE_SETUP: list[Callable[[Any], None]] = []
|
|
41
|
+
SERVE_POLICY: Callable[..., Callable[[Any], Any]] | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def pick(name: str, default: Callable[..., Any]) -> Callable[..., Any]:
|
|
45
|
+
return OVERRIDES.get(name, default)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_ready = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ensure() -> None:
|
|
52
|
+
"""Load plugins once for library callers that never went through the CLI."""
|
|
53
|
+
if not _ready:
|
|
54
|
+
load()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load(app: Any = None, mcp: Any = None) -> None:
|
|
58
|
+
"""Call every plugin with the typer `app` and/or MCP server to extend.
|
|
59
|
+
|
|
60
|
+
A plugin that cannot import must not take the free tool down: one dim line, move on.
|
|
61
|
+
"""
|
|
62
|
+
global _ready
|
|
63
|
+
_ready = True
|
|
64
|
+
for ep in entry_points(group="lockwire.plugins"):
|
|
65
|
+
try:
|
|
66
|
+
ep.load()(app=app, mcp=mcp)
|
|
67
|
+
except ImportError as error:
|
|
68
|
+
from rich.console import Console
|
|
69
|
+
|
|
70
|
+
Console(stderr=True).print(f"[dim]plugin {ep.name} not loaded: {error}[/]")
|