devagent-physical-engine 0.10.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 (84) hide show
  1. devagent_physical_engine/__init__.py +44 -0
  2. devagent_physical_engine/agent/__init__.py +40 -0
  3. devagent_physical_engine/agent/compiler.py +285 -0
  4. devagent_physical_engine/agent/contracts.py +129 -0
  5. devagent_physical_engine/agent/coordinator.py +108 -0
  6. devagent_physical_engine/agent/critic.py +72 -0
  7. devagent_physical_engine/agent/evidence.py +34 -0
  8. devagent_physical_engine/agent/interpreter.py +179 -0
  9. devagent_physical_engine/agent/planner.py +105 -0
  10. devagent_physical_engine/agent/recovery.py +54 -0
  11. devagent_physical_engine/agent/routing.py +76 -0
  12. devagent_physical_engine/agent/runtime.py +270 -0
  13. devagent_physical_engine/agent/semantic.py +304 -0
  14. devagent_physical_engine/agent/structured.py +423 -0
  15. devagent_physical_engine/ai_cli.py +226 -0
  16. devagent_physical_engine/cli.py +392 -0
  17. devagent_physical_engine/doctor.py +20 -0
  18. devagent_physical_engine/engineering_agent.py +243 -0
  19. devagent_physical_engine/engineering_request.py +630 -0
  20. devagent_physical_engine/execution.py +90 -0
  21. devagent_physical_engine/models.py +143 -0
  22. devagent_physical_engine/operating_envelope.py +120 -0
  23. devagent_physical_engine/optimization/__init__.py +50 -0
  24. devagent_physical_engine/optimization/benchmark.py +122 -0
  25. devagent_physical_engine/optimization/candidates.py +198 -0
  26. devagent_physical_engine/optimization/contracts.py +235 -0
  27. devagent_physical_engine/optimization/evaluator.py +107 -0
  28. devagent_physical_engine/optimization/evidence.py +53 -0
  29. devagent_physical_engine/optimization/experience.py +105 -0
  30. devagent_physical_engine/optimization/measured.py +125 -0
  31. devagent_physical_engine/optimization/optimizer.py +215 -0
  32. devagent_physical_engine/optimization/orchestrator.py +155 -0
  33. devagent_physical_engine/physical_campaign.py +413 -0
  34. devagent_physical_engine/physical_evidence.py +214 -0
  35. devagent_physical_engine/physical_motion.py +196 -0
  36. devagent_physical_engine/planning.py +80 -0
  37. devagent_physical_engine/preexecution_contract.py +65 -0
  38. devagent_physical_engine/provider_adapters/__init__.py +22 -0
  39. devagent_physical_engine/provider_adapters/anthropic.py +112 -0
  40. devagent_physical_engine/provider_adapters/common.py +187 -0
  41. devagent_physical_engine/provider_adapters/factory.py +20 -0
  42. devagent_physical_engine/provider_adapters/gemini.py +126 -0
  43. devagent_physical_engine/provider_adapters/openai.py +95 -0
  44. devagent_physical_engine/provider_qualification.py +268 -0
  45. devagent_physical_engine/providers.py +94 -0
  46. devagent_physical_engine/qualification.py +44 -0
  47. devagent_physical_engine/qualification_cli.py +195 -0
  48. devagent_physical_engine/qualification_harness.py +917 -0
  49. devagent_physical_engine/robot_platform.py +411 -0
  50. devagent_physical_engine/robots.py +76 -0
  51. devagent_physical_engine/ros2/__init__.py +35 -0
  52. devagent_physical_engine/ros2/acceptance.py +324 -0
  53. devagent_physical_engine/ros2/commands.py +175 -0
  54. devagent_physical_engine/ros2/doctor.py +116 -0
  55. devagent_physical_engine/ros2/fk_probe.py +83 -0
  56. devagent_physical_engine/ros2/frame_alignment.py +61 -0
  57. devagent_physical_engine/ros2/gazebo_world.py +125 -0
  58. devagent_physical_engine/ros2/joint_state_recorder.py +64 -0
  59. devagent_physical_engine/ros2/measured_motion.py +233 -0
  60. devagent_physical_engine/ros2/moveit_scene.py +121 -0
  61. devagent_physical_engine/ros2/preexecution.py +113 -0
  62. devagent_physical_engine/ros2/qualification.py +81 -0
  63. devagent_physical_engine/ros2/qualification_v10.py +252 -0
  64. devagent_physical_engine/ros2/scene_probe.py +219 -0
  65. devagent_physical_engine/ros2/state_validity_probe.py +125 -0
  66. devagent_physical_engine/ros2/tf_probe.py +51 -0
  67. devagent_physical_engine/ros2/trajectory.py +188 -0
  68. devagent_physical_engine/ros2/ur5e.py +59 -0
  69. devagent_physical_engine/ros2/ur5e_adapter.py +349 -0
  70. devagent_physical_engine/ros2/ur5e_v10_adapter.py +292 -0
  71. devagent_physical_engine/setup_profile.py +356 -0
  72. devagent_physical_engine/simulation.py +32 -0
  73. devagent_physical_engine/simulation_platform.py +269 -0
  74. devagent_physical_engine/trajectory_qualification.py +201 -0
  75. devagent_physical_engine/twin.py +939 -0
  76. devagent_physical_engine/twin_builder.py +309 -0
  77. devagent_physical_engine/twin_materialization.py +404 -0
  78. devagent_physical_engine/verification.py +46 -0
  79. devagent_physical_engine-0.10.0.dist-info/METADATA +315 -0
  80. devagent_physical_engine-0.10.0.dist-info/RECORD +84 -0
  81. devagent_physical_engine-0.10.0.dist-info/WHEEL +5 -0
  82. devagent_physical_engine-0.10.0.dist-info/entry_points.txt +3 -0
  83. devagent_physical_engine-0.10.0.dist-info/licenses/NOTICE +2 -0
  84. devagent_physical_engine-0.10.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,404 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from hashlib import sha256
5
+ import json
6
+ from math import asin, atan2, cos, isfinite, sin
7
+ from pathlib import Path
8
+ import re
9
+ from typing import Any
10
+
11
+ from .twin import GeometryKind, Pose3D, TwinSpec
12
+
13
+
14
+ class TwinMaterializationError(ValueError):
15
+ pass
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class MaterializationIssue:
20
+ code: str
21
+ detail: str
22
+ entity_id: str | None = None
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class MaterializedObject:
27
+ entity_id: str
28
+ backend_id: str
29
+ kind: str
30
+ dimensions_m: tuple[float, ...]
31
+ position_m: tuple[float, float, float]
32
+ quaternion_xyzw: tuple[float, float, float, float]
33
+ static: bool
34
+ mass_kg: float | None = None
35
+ friction: float | None = None
36
+ restitution: float | None = None
37
+
38
+ def to_dict(self) -> dict[str, Any]:
39
+ return asdict(self)
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class CanonicalTwinMaterialization:
44
+ """One canonical geometry set consumed by both Gazebo and MoveIt."""
45
+
46
+ twin_id: str
47
+ twin_hash: str
48
+ robot_profile_key: str
49
+ planning_frame: str
50
+ objects: tuple[MaterializedObject, ...]
51
+ materializer_version: str = "canonical-twin-v1"
52
+
53
+ def to_dict(self) -> dict[str, Any]:
54
+ return {
55
+ "twin_id": self.twin_id,
56
+ "twin_hash": self.twin_hash,
57
+ "robot_profile_key": self.robot_profile_key,
58
+ "planning_frame": self.planning_frame,
59
+ "objects": [item.to_dict() for item in self.objects],
60
+ "materializer_version": self.materializer_version,
61
+ }
62
+
63
+ @property
64
+ def fingerprint(self) -> str:
65
+ return _hash(self.to_dict())
66
+
67
+ def canonical_scene(self) -> dict[str, Any]:
68
+ return {
69
+ "planning_frame": self.planning_frame,
70
+ "objects": [
71
+ {
72
+ "id": item.backend_id,
73
+ "geometry_kind": item.kind,
74
+ "dimensions_m": [_canon_float(v) for v in item.dimensions_m],
75
+ "position_m": [_canon_float(v) for v in item.position_m],
76
+ "quaternion_xyzw": [_canon_float(v) for v in item.quaternion_xyzw],
77
+ }
78
+ for item in self.objects
79
+ ],
80
+ }
81
+
82
+ @property
83
+ def scene_hash(self) -> str:
84
+ return _hash(self.canonical_scene())
85
+
86
+ def moveit_scene_payload(self) -> dict[str, Any]:
87
+ return {
88
+ "materialization_hash": self.fingerprint,
89
+ "twin_hash": self.twin_hash,
90
+ "planning_frame": self.planning_frame,
91
+ "scene_hash": self.scene_hash,
92
+ "objects": [
93
+ {
94
+ "id": item.backend_id,
95
+ "geometry_kind": item.kind,
96
+ "dimensions_m": list(item.dimensions_m),
97
+ "pose": {
98
+ "position_m": list(item.position_m),
99
+ "quaternion_xyzw": list(item.quaternion_xyzw),
100
+ },
101
+ }
102
+ for item in self.objects
103
+ ],
104
+ }
105
+
106
+ def to_sdf(self) -> str:
107
+ lines = [
108
+ "<sdf version='1.10'>",
109
+ " <world name='default'>",
110
+ " <gravity>0 0 -9.8</gravity>",
111
+ " <physics type='ode'><max_step_size>0.001</max_step_size><real_time_factor>1</real_time_factor><real_time_update_rate>1000</real_time_update_rate></physics>",
112
+ ]
113
+ for item in self.objects:
114
+ x, y, z = item.position_m
115
+ roll, pitch, yaw = _rpy(item.quaternion_xyzw)
116
+ lines.extend([
117
+ f" <model name='{item.backend_id}'>",
118
+ f" <pose>{_f(x)} {_f(y)} {_f(z)} {_f(roll)} {_f(pitch)} {_f(yaw)}</pose>",
119
+ " <link name='body'>",
120
+ ])
121
+ if not item.static:
122
+ if item.mass_kg is None:
123
+ raise TwinMaterializationError(f"dynamic_mass_missing:{item.entity_id}")
124
+ ixx, iyy, izz = _inertia(item, item.mass_kg)
125
+ lines.extend([
126
+ " <inertial>",
127
+ f" <mass>{_f(item.mass_kg)}</mass>",
128
+ " <inertia>",
129
+ f" <ixx>{_f(ixx)}</ixx><ixy>0</ixy><ixz>0</ixz>",
130
+ f" <iyy>{_f(iyy)}</iyy><iyz>0</iyz><izz>{_f(izz)}</izz>",
131
+ " </inertia>",
132
+ " </inertial>",
133
+ ])
134
+ geometry = _sdf_geometry(item)
135
+ lines.extend([
136
+ " <collision name='collision'><geometry>",
137
+ *[" " + value for value in geometry],
138
+ " </geometry>",
139
+ ])
140
+ if item.friction is not None:
141
+ lines.append(
142
+ " <surface><friction><ode>"
143
+ f"<mu>{_f(item.friction)}</mu><mu2>{_f(item.friction)}</mu2>"
144
+ "</ode></friction></surface>"
145
+ )
146
+ lines.extend([
147
+ " </collision>",
148
+ " <visual name='visual'><geometry>",
149
+ *[" " + value for value in geometry],
150
+ " </geometry></visual>",
151
+ " </link>",
152
+ f" <static>{str(item.static).lower()}</static>",
153
+ " </model>",
154
+ ])
155
+ lines.extend([" </world>", "</sdf>"])
156
+ return "\n".join(lines) + "\n"
157
+
158
+ def write_sdf(self, path: Path | str) -> Path:
159
+ target = Path(path).expanduser().resolve()
160
+ target.parent.mkdir(parents=True, exist_ok=True)
161
+ temporary = target.with_suffix(target.suffix + ".tmp")
162
+ temporary.write_text(self.to_sdf(), encoding="utf-8")
163
+ temporary.replace(target)
164
+ return target
165
+
166
+
167
+ @dataclass(frozen=True, slots=True)
168
+ class TwinMaterializationResult:
169
+ twin_hash: str
170
+ materialization: CanonicalTwinMaterialization | None
171
+ issues: tuple[MaterializationIssue, ...]
172
+
173
+ @property
174
+ def ready(self) -> bool:
175
+ return self.materialization is not None and not self.issues
176
+
177
+
178
+ @dataclass(frozen=True, slots=True)
179
+ class _Transform:
180
+ translation: tuple[float, float, float]
181
+ quaternion: tuple[float, float, float, float]
182
+
183
+
184
+ def _canon_float(value: float) -> float:
185
+ value = float(value)
186
+ if abs(value) < 1e-12:
187
+ return 0.0
188
+ return float(format(value, ".12g"))
189
+
190
+
191
+ def _hash(value: object) -> str:
192
+ text = json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
193
+ return sha256(text.encode("utf-8")).hexdigest()
194
+
195
+
196
+ def _qmul(first, second):
197
+ ax, ay, az, aw = first
198
+ bx, by, bz, bw = second
199
+ return (
200
+ aw * bx + ax * bw + ay * bz - az * by,
201
+ aw * by - ax * bz + ay * bw + az * bx,
202
+ aw * bz + ax * by - ay * bx + az * bw,
203
+ aw * bw - ax * bx - ay * by - az * bz,
204
+ )
205
+
206
+
207
+ def _qconj(quaternion):
208
+ return (-quaternion[0], -quaternion[1], -quaternion[2], quaternion[3])
209
+
210
+
211
+ def _qrot(quaternion, vector):
212
+ output = _qmul(_qmul(quaternion, (vector[0], vector[1], vector[2], 0.0)), _qconj(quaternion))
213
+ return output[:3]
214
+
215
+
216
+ def _compose(first: _Transform, second: _Transform) -> _Transform:
217
+ rotated = _qrot(first.quaternion, second.translation)
218
+ return _Transform(
219
+ tuple(first.translation[index] + rotated[index] for index in range(3)),
220
+ _qmul(first.quaternion, second.quaternion),
221
+ )
222
+
223
+
224
+ def _inverse(value: _Transform) -> _Transform:
225
+ quaternion = _qconj(value.quaternion)
226
+ return _Transform(_qrot(quaternion, tuple(-component for component in value.translation)), quaternion)
227
+
228
+
229
+ def _pose(value: Pose3D) -> _Transform:
230
+ cr, sr = cos(value.roll_rad / 2), sin(value.roll_rad / 2)
231
+ cp, sp = cos(value.pitch_rad / 2), sin(value.pitch_rad / 2)
232
+ cy, sy = cos(value.yaw_rad / 2), sin(value.yaw_rad / 2)
233
+ quaternion = (
234
+ sr * cp * cy - cr * sp * sy,
235
+ cr * sp * cy + sr * cp * sy,
236
+ cr * cp * sy - sr * sp * cy,
237
+ cr * cp * cy + sr * sp * sy,
238
+ )
239
+ return _Transform((float(value.x_m), float(value.y_m), float(value.z_m)), quaternion)
240
+
241
+
242
+ def _canonical_quaternion(quaternion):
243
+ canonical = tuple(_canon_float(value) for value in quaternion)
244
+ return tuple(-value for value in canonical) if canonical[3] < 0 else canonical
245
+
246
+
247
+ def _rpy(quaternion):
248
+ x, y, z, w = quaternion
249
+ return (
250
+ atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y)),
251
+ asin(max(-1.0, min(1.0, 2 * (w * y - z * x)))),
252
+ atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)),
253
+ )
254
+
255
+
256
+ def _backend_id(entity_id: str) -> str:
257
+ slug = re.sub(r"[^A-Za-z0-9_]+", "_", entity_id).strip("_").lower()[:32] or "entity"
258
+ return f"devagent_{slug}_{sha256(entity_id.encode('utf-8')).hexdigest()[:10]}"
259
+
260
+
261
+ def _numeric(value: object) -> float | None:
262
+ if value is None or isinstance(value, bool) or not isinstance(value, (int, float)):
263
+ return None
264
+ output = float(value)
265
+ return output if isfinite(output) else None
266
+
267
+
268
+ def _f(value: float) -> str:
269
+ return format(float(value), ".12g")
270
+
271
+
272
+ def _sdf_geometry(item: MaterializedObject) -> list[str]:
273
+ if item.kind == GeometryKind.BOX.value:
274
+ x, y, z = item.dimensions_m
275
+ return [f"<box><size>{_f(x)} {_f(y)} {_f(z)}</size></box>"]
276
+ radius, height = item.dimensions_m
277
+ return [f"<cylinder><radius>{_f(radius)}</radius><length>{_f(height)}</length></cylinder>"]
278
+
279
+
280
+ def _inertia(item: MaterializedObject, mass: float):
281
+ if item.kind == GeometryKind.BOX.value:
282
+ x, y, z = item.dimensions_m
283
+ return (
284
+ mass * (y * y + z * z) / 12,
285
+ mass * (x * x + z * z) / 12,
286
+ mass * (x * x + y * y) / 12,
287
+ )
288
+ radius, height = item.dimensions_m
289
+ transverse = mass * (3 * radius * radius + height * height) / 12
290
+ return transverse, transverse, mass * radius * radius / 2
291
+
292
+
293
+ class TwinMaterializer:
294
+ """Resolve the engineering Twin once for both physics and planning backends."""
295
+
296
+ def __init__(self, *, planning_frame: str = "base_link") -> None:
297
+ if not planning_frame.strip():
298
+ raise ValueError("planning_frame_required")
299
+ self.planning_frame = planning_frame
300
+
301
+ def compile(self, twin: TwinSpec) -> TwinMaterializationResult:
302
+ if twin.robot_base_pose is None:
303
+ return TwinMaterializationResult(
304
+ twin.fingerprint,
305
+ None,
306
+ (MaterializationIssue("robot_base_pose_missing", "Resolved robot base pose is required."),),
307
+ )
308
+
309
+ issues: list[MaterializationIssue] = []
310
+ frame_specs = {frame.child_frame_id: frame for frame in twin.frames}
311
+ cache: dict[str, _Transform] = {
312
+ "world": _Transform((0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))
313
+ }
314
+ visiting: set[str] = set()
315
+
316
+ def resolve_frame(frame_id: str) -> _Transform:
317
+ if frame_id in cache:
318
+ return cache[frame_id]
319
+ if frame_id in visiting:
320
+ raise TwinMaterializationError(f"frame_cycle:{frame_id}")
321
+ spec = frame_specs.get(frame_id)
322
+ if spec is None:
323
+ raise TwinMaterializationError(f"unknown_frame:{frame_id}")
324
+ visiting.add(frame_id)
325
+ try:
326
+ output = _compose(resolve_frame(spec.parent_frame_id), _pose(spec.transform))
327
+ finally:
328
+ visiting.remove(frame_id)
329
+ cache[frame_id] = output
330
+ return output
331
+
332
+ def world_pose(pose: Pose3D) -> _Transform:
333
+ return _compose(resolve_frame(pose.frame_id), _pose(pose))
334
+
335
+ try:
336
+ base_from_world = _inverse(world_pose(twin.robot_base_pose))
337
+ except TwinMaterializationError as exc:
338
+ return TwinMaterializationResult(
339
+ twin.fingerprint,
340
+ None,
341
+ (MaterializationIssue(str(exc).split(":", 1)[0], str(exc)),),
342
+ )
343
+
344
+ objects: list[MaterializedObject] = []
345
+ for entity in twin.entities:
346
+ if not entity.enabled or not entity.geometry.collision_geometry:
347
+ continue
348
+ if entity.pose is None:
349
+ issues.append(MaterializationIssue("entity_pose_missing", "Collision entity has no pose.", entity.entity_id))
350
+ continue
351
+ if entity.geometry.kind not in {GeometryKind.BOX, GeometryKind.CYLINDER}:
352
+ issues.append(MaterializationIssue(
353
+ "geometry_not_materializable_v1",
354
+ "Only verified box/cylinder primitives are materialized; mesh/CAD requires asset qualification.",
355
+ entity.entity_id,
356
+ ))
357
+ continue
358
+ try:
359
+ backend_pose = _compose(base_from_world, world_pose(entity.pose))
360
+ except TwinMaterializationError as exc:
361
+ issues.append(MaterializationIssue(str(exc).split(":", 1)[0], str(exc), entity.entity_id))
362
+ continue
363
+
364
+ is_workpiece = entity.entity_id == twin.workpiece_entity_id
365
+ mass = _numeric(entity.physics.mass_kg.value)
366
+ friction = _numeric(entity.physics.friction_coefficient.value)
367
+ if is_workpiece and mass is None:
368
+ issues.append(MaterializationIssue("dynamic_workpiece_mass_missing", "Dynamic workpiece mass evidence is required.", entity.entity_id))
369
+ continue
370
+ if is_workpiece and friction is None:
371
+ issues.append(MaterializationIssue("dynamic_workpiece_friction_missing", "Dynamic workpiece friction evidence is required.", entity.entity_id))
372
+ continue
373
+
374
+ dimensions = tuple(float(value) for value in entity.geometry.dimensions_m)
375
+ if any(not isfinite(value) or value <= 0 for value in dimensions):
376
+ issues.append(MaterializationIssue("invalid_geometry_dimensions", "Geometry dimensions must be finite and positive.", entity.entity_id))
377
+ continue
378
+
379
+ objects.append(MaterializedObject(
380
+ entity_id=entity.entity_id,
381
+ backend_id=_backend_id(entity.entity_id),
382
+ kind=entity.geometry.kind.value,
383
+ dimensions_m=dimensions,
384
+ position_m=tuple(_canon_float(value) for value in backend_pose.translation),
385
+ quaternion_xyzw=_canonical_quaternion(backend_pose.quaternion),
386
+ static=not is_workpiece,
387
+ mass_kg=mass,
388
+ friction=friction,
389
+ restitution=_numeric(entity.physics.restitution.value),
390
+ ))
391
+
392
+ if not objects:
393
+ issues.append(MaterializationIssue("collision_environment_empty", "At least one collision object must materialize."))
394
+ if issues:
395
+ return TwinMaterializationResult(twin.fingerprint, None, tuple(issues))
396
+
397
+ materialization = CanonicalTwinMaterialization(
398
+ twin_id=twin.twin_id,
399
+ twin_hash=twin.fingerprint,
400
+ robot_profile_key=twin.robot_profile_key,
401
+ planning_frame=self.planning_frame,
402
+ objects=tuple(sorted(objects, key=lambda item: item.backend_id)),
403
+ )
404
+ return TwinMaterializationResult(twin.fingerprint, materialization, ())
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from .models import Resource, TaskGraph, TaskNode, WorldState
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class VerificationIssue:
10
+ code: str
11
+ task_id: str | None = None
12
+ detail: str = ""
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class VerificationResult:
17
+ passed: bool
18
+ issues: list[VerificationIssue] = field(default_factory=list)
19
+
20
+
21
+ class DeterministicVerifier:
22
+ def verify_graph(self, graph: TaskGraph, resources: list[Resource]) -> VerificationResult:
23
+ issues = [VerificationIssue(error) for error in graph.validate_structure()]
24
+ resource_map = {resource.resource_id: resource for resource in resources}
25
+ try:
26
+ ordered = graph.topological_order()
27
+ except ValueError as exc:
28
+ issues.append(VerificationIssue("invalid_task_graph", detail=str(exc)))
29
+ return VerificationResult(False, issues)
30
+ for task in ordered:
31
+ resource = resource_map.get(task.contract.resource_id)
32
+ if resource is None:
33
+ issues.append(VerificationIssue("unknown_resource", task.task_id, task.contract.resource_id))
34
+ elif not resource.supports(task.contract.action):
35
+ issues.append(VerificationIssue("unsupported_capability", task.task_id, task.contract.action.value))
36
+ return VerificationResult(not issues, issues)
37
+
38
+ def verify_runtime_preconditions(self, task: TaskNode, world: WorldState) -> VerificationResult:
39
+ missing = [item for item in task.contract.preconditions if item not in world.facts]
40
+ issues = [VerificationIssue("missing_precondition", task.task_id, item) for item in missing]
41
+ return VerificationResult(not issues, issues)
42
+
43
+ def verify_simulation_constraints(self, task: TaskNode, satisfied_constraints: set[str]) -> VerificationResult:
44
+ missing = [item for item in task.contract.constraints if item not in satisfied_constraints]
45
+ issues = [VerificationIssue("unsatisfied_constraint", task.task_id, item) for item in missing]
46
+ return VerificationResult(not issues, issues)