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.
Files changed (52) hide show
  1. lockwire/__init__.py +9 -0
  2. lockwire/__main__.py +9 -0
  3. lockwire/advice.py +119 -0
  4. lockwire/api.py +1216 -0
  5. lockwire/api_payload.py +985 -0
  6. lockwire/bench.py +182 -0
  7. lockwire/blame.py +100 -0
  8. lockwire/catalog.py +100 -0
  9. lockwire/catalog_seed.yaml +545 -0
  10. lockwire/checks.py +3483 -0
  11. lockwire/cli.py +1799 -0
  12. lockwire/cli_line.py +73 -0
  13. lockwire/constraints.py +313 -0
  14. lockwire/copy.py +302 -0
  15. lockwire/describe.py +682 -0
  16. lockwire/edit.py +2098 -0
  17. lockwire/emit_ts.py +150 -0
  18. lockwire/export_mhs.py +115 -0
  19. lockwire/factdiff.py +254 -0
  20. lockwire/forces.py +334 -0
  21. lockwire/gitref.py +188 -0
  22. lockwire/hw.py +388 -0
  23. lockwire/mcp_server.py +276 -0
  24. lockwire/mechanism.py +379 -0
  25. lockwire/mesh_dae.py +555 -0
  26. lockwire/mesh_simple.py +198 -0
  27. lockwire/page.py +1485 -0
  28. lockwire/plan.py +849 -0
  29. lockwire/plan_payload.py +151 -0
  30. lockwire/plugins.py +70 -0
  31. lockwire/report.py +848 -0
  32. lockwire/report_html.py +707 -0
  33. lockwire/report_json.py +168 -0
  34. lockwire/review_draft.py +315 -0
  35. lockwire/scan.py +732 -0
  36. lockwire/scene.py +535 -0
  37. lockwire/scene_import.py +309 -0
  38. lockwire/scene_urdf.py +750 -0
  39. lockwire/scene_view.py +376 -0
  40. lockwire/schema.py +3616 -0
  41. lockwire/sdf.py +240 -0
  42. lockwire/serve.py +1256 -0
  43. lockwire/start_operation.py +92 -0
  44. lockwire/statics.py +77 -0
  45. lockwire/units.py +115 -0
  46. lockwire/urdf.py +348 -0
  47. lockwire/vcs.py +402 -0
  48. lockwire/whatif.py +161 -0
  49. lockwire-0.2.0.dist-info/METADATA +192 -0
  50. lockwire-0.2.0.dist-info/RECORD +52 -0
  51. lockwire-0.2.0.dist-info/WHEEL +4 -0
  52. lockwire-0.2.0.dist-info/entry_points.txt +3 -0
@@ -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}[/]")