dsh-engineering-ui 0.0.0-stage → 1.5.1

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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +772 -3
  3. package/engineering/plugins/dsh-engineering-ui/cordis.patch.yml +5 -0
  4. package/engineering/plugins/dsh-engineering-ui/icon.svg +14 -0
  5. package/engineering/plugins/dsh-engineering-ui/lib/client.js +8257 -0
  6. package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
  7. package/engineering/plugins/dsh-engineering-ui/lib/index.js +5634 -0
  8. package/engineering/plugins/dsh-engineering-ui/lib/job-wait-guard.js +236 -0
  9. package/engineering/plugins/dsh-engineering-ui/lib/mem-guard.js +331 -0
  10. package/engineering/plugins/dsh-engineering-ui/lib/webots.js +114 -0
  11. package/engineering/plugins/dsh-engineering-ui/locale/en.json +6 -0
  12. package/engineering/plugins/dsh-engineering-ui/locale/zh.json +6 -0
  13. package/engineering/plugins/dsh-engineering-ui/preset-engineering.patch.yml +601 -0
  14. package/engineering/skills/assembly-orchestration/SKILL.md +651 -0
  15. package/engineering/skills/cad-workflow/SKILL.md +132 -0
  16. package/engineering/skills/mode-selection/SKILL.md +660 -0
  17. package/engineering/skills/physics-in-loop/SKILL.md +238 -0
  18. package/engineering/skills/physics-in-loop/references/feadocs.md +142 -0
  19. package/engineering/skills/solidworks-bridge/SKILL.md +445 -0
  20. package/engineering/skills/sw-design/SKILL.md +112 -0
  21. package/engineering/skills/sw-design/references/autocad-workflow.md +81 -0
  22. package/engineering/skills/sw-design/references/gbt-drafting.md +71 -0
  23. package/engineering/skills/sw-to-cad/SKILL.md +121 -0
  24. package/engineering/skills/webots-simulation/SKILL.md +58 -0
  25. package/engineering/tools/LICENSE.Auto-SolidWorks +21 -0
  26. package/engineering/tools/_plugin_paths.py +93 -0
  27. package/engineering/tools/_root.py +330 -0
  28. package/engineering/tools/_store.py +446 -0
  29. package/engineering/tools/ac_bridge.py +459 -0
  30. package/engineering/tools/ac_rules.json +21 -0
  31. package/engineering/tools/ac_validate.py +699 -0
  32. package/engineering/tools/ask_user.py +887 -0
  33. package/engineering/tools/build_arm1.py +46 -0
  34. package/engineering/tools/build_assembly.py +94 -0
  35. package/engineering/tools/build_base.py +57 -0
  36. package/engineering/tools/build_housing.py +70 -0
  37. package/engineering/tools/build_structural.py +43 -0
  38. package/engineering/tools/build_transmission.py +69 -0
  39. package/engineering/tools/cadx.lsp +128 -0
  40. package/engineering/tools/choice_contract.py +61 -0
  41. package/engineering/tools/conn_state.py +1671 -0
  42. package/engineering/tools/defense_gate.py +3374 -0
  43. package/engineering/tools/drawing_input.py +314 -0
  44. package/engineering/tools/env_check.py +467 -0
  45. package/engineering/tools/examples/build_axis.py +142 -0
  46. package/engineering/tools/examples/build_cup.py +56 -0
  47. package/engineering/tools/examples/build_plate.py +58 -0
  48. package/engineering/tools/examples/build_sphere.py +30 -0
  49. package/engineering/tools/examples/build_transition_duct.py +381 -0
  50. package/engineering/tools/gate_integrity.py +401 -0
  51. package/engineering/tools/mode2_flow.py +3589 -0
  52. package/engineering/tools/mode_checker.py +77 -0
  53. package/engineering/tools/mode_gate.py +8013 -0
  54. package/engineering/tools/part_registry.py +4266 -0
  55. package/engineering/tools/physics/__init__.py +50 -0
  56. package/engineering/tools/physics/_compat.py +12 -0
  57. package/engineering/tools/physics/design_state.py +142 -0
  58. package/engineering/tools/physics/domain_validator.py +751 -0
  59. package/engineering/tools/physics/fatigue.py +489 -0
  60. package/engineering/tools/physics/fea_solver.py +1163 -0
  61. package/engineering/tools/physics/feapy_solver.py +314 -0
  62. package/engineering/tools/physics/gb_materials.json +1228 -0
  63. package/engineering/tools/physics/geometry_gate.py +337 -0
  64. package/engineering/tools/physics/load_case.py +951 -0
  65. package/engineering/tools/physics/material_db.py +874 -0
  66. package/engineering/tools/physics/mesh_adapter.py +321 -0
  67. package/engineering/tools/physics/refine_rules.py +272 -0
  68. package/engineering/tools/physics/rules/gb_base_rules.json +377 -0
  69. package/engineering/tools/physics/rules/gb_drafting_rules.json +57 -0
  70. package/engineering/tools/physics/rules/gb_electrical_rules.json +25 -0
  71. package/engineering/tools/physics/rules/gb_fasteners_rules.json +73 -0
  72. package/engineering/tools/physics/rules/gb_housing_rules.json +41 -0
  73. package/engineering/tools/physics/rules/gb_manufacturing_rules.json +25 -0
  74. package/engineering/tools/physics/rules/gb_material_verify_rules.json +72 -0
  75. package/engineering/tools/physics/rules/gb_materials_rules.json +377 -0
  76. package/engineering/tools/physics/rules/gb_safety_rules.json +89 -0
  77. package/engineering/tools/physics/rules/gb_springs_rules.json +41 -0
  78. package/engineering/tools/physics/rules/gb_standards_registry.json +1646 -0
  79. package/engineering/tools/physics/rules/gb_structural_rules.json +201 -0
  80. package/engineering/tools/physics/rules/gb_surface_rules.json +25 -0
  81. package/engineering/tools/physics/rules/gb_transmission_rules.json +233 -0
  82. package/engineering/tools/physics/rules/housing_rules.json +48 -0
  83. package/engineering/tools/physics/rules/manifest.json +54 -0
  84. package/engineering/tools/physics/rules/mold_rules.json +48 -0
  85. package/engineering/tools/physics/rules/structural_rules.json +56 -0
  86. package/engineering/tools/physics/rules/transmission_rules.json +48 -0
  87. package/engineering/tools/physics/simulation_report.py +829 -0
  88. package/engineering/tools/physics_bridge.py +2685 -0
  89. package/engineering/tools/profile-manifest.ps1 +72 -0
  90. package/engineering/tools/run_regression.py +109 -0
  91. package/engineering/tools/settings.py +419 -0
  92. package/engineering/tools/solidworks-modeling.md +519 -0
  93. package/engineering/tools/sw_bridge.py +8215 -0
  94. package/engineering/tools/sw_guard.py +1066 -0
  95. package/engineering/tools/sw_surface.py +87 -0
  96. package/engineering/tools/swapi.py +14766 -0
  97. package/engineering/tools/verify_sync.py +218 -0
  98. package/engineering/tools/visual_check.py +2589 -0
  99. package/engineering/tools/webots_env.py +121 -0
  100. package/engineering/tools/webots_mcp/__init__.py +3 -0
  101. package/engineering/tools/webots_mcp/__main__.py +3 -0
  102. package/engineering/tools/webots_mcp/assembly.py +391 -0
  103. package/engineering/tools/webots_mcp/bootstrap.py +55 -0
  104. package/engineering/tools/webots_mcp/controller_worker.py +124 -0
  105. package/engineering/tools/webots_mcp/evidence.py +288 -0
  106. package/engineering/tools/webots_mcp/fake_worker.py +69 -0
  107. package/engineering/tools/webots_mcp/fault_resources.py +121 -0
  108. package/engineering/tools/webots_mcp/host.py +45 -0
  109. package/engineering/tools/webots_mcp/ledger.py +73 -0
  110. package/engineering/tools/webots_mcp/owned_job.py +70 -0
  111. package/engineering/tools/webots_mcp/physical_gate.py +187 -0
  112. package/engineering/tools/webots_mcp/physics.py +152 -0
  113. package/engineering/tools/webots_mcp/pipeline.py +108 -0
  114. package/engineering/tools/webots_mcp/recipes.py +129 -0
  115. package/engineering/tools/webots_mcp/repair.py +132 -0
  116. package/engineering/tools/webots_mcp/repair_trial.py +375 -0
  117. package/engineering/tools/webots_mcp/runtime.py +311 -0
  118. package/engineering/tools/webots_mcp/server.py +217 -0
  119. package/engineering/tools/webots_mcp/sw_export.py +509 -0
  120. package/engineering/tools/webots_mcp/transport.py +156 -0
  121. package/engineering/tools/workflow_gate.py +6134 -0
  122. package/package.json +66 -6
@@ -0,0 +1,129 @@
1
+ """Offline native terrain generation and unresolved experiment plans, no execution.
2
+
3
+ Scenes use Webots ENU directly; this module never transforms CAD coordinates.
4
+ """
5
+ from copy import deepcopy
6
+ import hashlib
7
+ import json
8
+ from pathlib import Path
9
+
10
+ ROOT = Path(__file__).resolve().parent
11
+
12
+
13
+ def load_recipes():
14
+ """Expand the shipped catalog; each caller receives independent raw plans."""
15
+ catalog = json.loads((ROOT / "recipes/catalog.json").read_text(encoding="utf-8"))
16
+ result = {}
17
+ for name, item in catalog["recipes"].items():
18
+ recipe = deepcopy(catalog["defaults"])
19
+ recipe.update(deepcopy(item))
20
+ recipe["budget"] = {**catalog["defaults"]["budget"], **item.get("budget", {})}
21
+ recipe["missing_information"] = [*catalog["defaults"]["missing_information"],
22
+ *item.get("missing_information", [])]
23
+ result[name] = recipe
24
+ return result
25
+
26
+
27
+ def generate_scene(kind, parameters=None):
28
+ """Return synthetic primitives, a self-contained .wbt and its UTF-8 hash.
29
+
30
+ Parameter triples in templates.json are default/min/max, all lengths in m.
31
+ No robot/Physics node, material calibration or physical judgement is added.
32
+ """
33
+ templates = json.loads((ROOT / "scenes/templates.json").read_text(encoding="utf-8"))
34
+ if not isinstance(kind, str) or kind not in templates["templates"]:
35
+ raise ValueError("unknown scene kind")
36
+ parameters = {} if parameters is None else parameters
37
+ if not isinstance(parameters, dict):
38
+ raise ValueError("scene parameters must be an object")
39
+ spec = {**templates["common"], **templates["templates"][kind]}
40
+ if parameters.keys() - spec.keys():
41
+ raise ValueError("unknown scene parameters")
42
+ p = {key: parameters.get(key, bounds[0]) for key, bounds in spec.items()}
43
+ for key, value in p.items():
44
+ if (type(value) not in (int, float) or not spec[key][1] <= value <= spec[key][2]
45
+ or key in ("count", "time_step_ms", "seed") and type(value) is not int):
46
+ raise ValueError(f"invalid scene parameter: {key}")
47
+
48
+ def box(name, center, size):
49
+ return {"name": name, "type": "Box", "translation": center, "size": size}
50
+
51
+ geometry = [box("ground", [0, 0, -p["thickness_m"] / 2],
52
+ [p["length_m"], p["width_m"], p["thickness_m"]])]
53
+ if kind in ("stairs", "slope"):
54
+ run = p["count"] * p["tread_m"] if kind == "stairs" else p["run_m"]
55
+ if (p["start_x_m"] < -p["length_m"] / 2 or
56
+ p["start_x_m"] + run > p["length_m"] / 2 or
57
+ p["feature_width_m"] > p["width_m"]):
58
+ raise ValueError("terrain feature extends beyond ground footprint")
59
+ if kind == "stairs":
60
+ for i in range(p["count"]):
61
+ height = (i + 1) * p["rise_m"]
62
+ geometry.append(box(f"step_{i + 1}",
63
+ [p["start_x_m"] + (i + 0.5) * p["tread_m"], 0, height / 2],
64
+ [p["tread_m"], p["feature_width_m"], height]))
65
+ else:
66
+ geometry.append({"name": "slope", "type": "ElevationGrid",
67
+ "translation": [p["start_x_m"], -p["feature_width_m"] / 2, 0],
68
+ "height": [0, p["rise_m"], 0, p["rise_m"]],
69
+ "xSpacing": p["run_m"], "ySpacing": p["feature_width_m"]})
70
+ if kind == "obstacle":
71
+ if (abs(p["x_m"]) + p["length_obstacle_m"] / 2 > p["length_m"] / 2 or
72
+ abs(p["y_m"]) + p["width_obstacle_m"] / 2 > p["width_m"] / 2):
73
+ raise ValueError("obstacle extends beyond ground footprint")
74
+ geometry.append(box("obstacle", [p["x_m"], p["y_m"], p["height_m"] / 2],
75
+ [p["length_obstacle_m"], p["width_obstacle_m"], p["height_m"]]))
76
+
77
+ def vector(values):
78
+ return " ".join(format(value, ".17g") for value in values)
79
+
80
+ # Reuse the exact same native geometry for display and collision.
81
+ world = '#VRML_SIM R2025a utf8\n# Synthetic terrain only; physical verdict unknown.\n'
82
+ world += f'WorldInfo {{ coordinateSystem "ENU" basicTimeStep {p["time_step_ms"]} randomSeed {p["seed"]} }}\n'
83
+ world += 'Viewpoint { position 3 -4 3 orientation 0.736 0.327 0.592 1.687 }\n'
84
+ world += 'Background { skyColor [ 0.15 0.18 0.23 ] }\nDirectionalLight { }\n'
85
+ for index, item in enumerate(geometry):
86
+ shape = (f'Box {{ size {vector(item["size"])} }}' if item["type"] == "Box" else
87
+ 'ElevationGrid { xDimension 2 yDimension 2 '
88
+ f'xSpacing {item["xSpacing"]:.17g} ySpacing {item["ySpacing"]:.17g} '
89
+ f'height [ {vector(item["height"])} ] thickness {p["thickness_m"]:.17g} }}')
90
+ world += (f'DEF TERRAIN_{index} Solid {{\n translation {vector(item["translation"])}\n'
91
+ f' name "{item["name"]}"\n contactMaterial "terrain"\n'
92
+ ' children [ Shape { appearance PBRAppearance { baseColor 0.5 0.55 0.6 roughness 1 metalness 0 }\n'
93
+ f' geometry DEF GEOM_{index} {shape} }} ]\n boundingObject USE GEOM_{index}\n}}\n')
94
+ return {"schema_version": 1, "provenance": "synthetic", "webots_version": "R2025a",
95
+ "kind": kind, "parameters": p, "units": templates["units"], "coordinate_system": "ENU",
96
+ "geometry": geometry, "world": world, "sha256": hashlib.sha256(world.encode("utf-8")).hexdigest(),
97
+ "physical_verdict": "unknown", "missing_information": ["model", "contact_material_pair_and_friction",
98
+ "real_world_load_and_collision_observations", "A04_physical_acceptance"]}
99
+
100
+
101
+ def prepare_experiment(recipe_id, scene_kind, parameters=None):
102
+ """Prepare a report-compatible, unexecuted plan; required bindings stay null."""
103
+ catalog = load_recipes()
104
+ if not isinstance(recipe_id, str) or recipe_id not in catalog:
105
+ raise ValueError("unknown recipe")
106
+ recipe = catalog[recipe_id]
107
+ if not isinstance(scene_kind, str) or scene_kind not in recipe["scene_kinds"]:
108
+ raise ValueError("scene kind is not applicable to recipe")
109
+ scene = generate_scene(scene_kind, parameters)
110
+ budget = deepcopy(recipe["budget"])
111
+ budget["time_step_ms"] = scene["parameters"]["time_step_ms"]
112
+ if recipe_id == "paired_step_comparison":
113
+ if budget["time_step_ms"] not in (16, 32):
114
+ raise ValueError("paired comparison requires 32ms or 16ms")
115
+ budget["max_steps"] = 64 if budget["time_step_ms"] == 32 else 128
116
+ budget["sim_time_s"] = budget["max_steps"] * budget["time_step_ms"] / 1000
117
+ initial_state = {**recipe["initial_state"], "seed": scene["parameters"]["seed"]}
118
+ # ponytail: plans only; bind devices and enforce real deadlines in A04 worker.
119
+ actions = []
120
+ for start in range(0, budget["max_steps"], budget["batch_steps"]):
121
+ actions.append({"kind": "step_batch", "steps": min(budget["batch_steps"], budget["max_steps"] - start),
122
+ "yield_wall_time_s": budget["batch_yield_s"], "dispatchable": False})
123
+ return {"recipe": {"id": recipe_id, **recipe}, "scene": scene,
124
+ "parameters": {"time_step": budget["time_step_ms"] / 1000, "seed": scene["parameters"]["seed"],
125
+ "initial_state": initial_state, "budget": budget},
126
+ "actions": actions, "observations": [], "issues": [], "model": None,
127
+ "execution_state": "not_started", "core_ready": False,
128
+ "verdict": deepcopy(recipe["judgement"]),
129
+ "missing_information": list(dict.fromkeys(recipe["missing_information"] + scene["missing_information"]))}
@@ -0,0 +1,132 @@
1
+ """Evidence-bound attribution and one isolated assembly distance-mate trial."""
2
+ import argparse
3
+ from hashlib import sha256
4
+ import json
5
+ import math
6
+ from pathlib import Path
7
+
8
+ from .evidence import EvidenceStore, _json_bytes, _load_json
9
+
10
+
11
+ def require(condition, reason):
12
+ if not condition:
13
+ raise ValueError(reason)
14
+
15
+
16
+ def digest(path):
17
+ with path.open("rb") as stream:
18
+ from hashlib import file_digest
19
+ return file_digest(stream, "sha256").hexdigest()
20
+
21
+
22
+ def read_case(store, run):
23
+ reports = [store.read_report(run, kind) for kind in
24
+ ("model_manifest", "conversion_report", "experiment_report")]
25
+ manifest, conversion, experiment = reports
26
+ revisions = {report["source_revision"] for report in reports}
27
+ require(len(revisions) == 1, "REPORT_REVISION_MISMATCH")
28
+ fingerprints = {r["report_type"]: sha256(_json_bytes(r)).hexdigest() for r in reports}
29
+ link = experiment["data"]["model"] or {}
30
+ require(link.get("path") == "model_manifest.json" and
31
+ link.get("sha256") == fingerprints["model_manifest"], "MODEL_LINK_MISMATCH")
32
+ return manifest, conversion, experiment, fingerprints
33
+
34
+
35
+ def worker_observation(store, run, experiment, raw, metric, raw_sha256):
36
+ """Normalize an unmodified A04 PositionSensor receipt using the pinned CAD measurement binding."""
37
+ m = store.read_report(run,"model_manifest")
38
+ c = store.read_report(run,"conversion_report")
39
+ require(raw["run_id"] == run and raw["action"] == "sim_sensor" and
40
+ raw["measurement"]["device_type"] == "PositionSensor" and
41
+ any(e["sha256"] == raw["model_sha256"] and e["kind"] == "exported-model" for e in c["evidence"]),
42
+ "SENSOR_OR_EXPORTED_MODEL_MISMATCH")
43
+ refs = m["data"]["measurement_bindings"]
44
+ entries = [e for e in m["evidence"] if e["kind"] == "sensor-binding" and e["sha256"] == refs[metric]
45
+ and e["provenance"]["kind"] == m["provenance"]["kind"]]
46
+ require(len(entries) == 1, "MISSING_CALIBRATED_SENSOR_BINDING")
47
+ bind = _load_json(store._path(f"webots_runs/{run}/{entries[0]['path']}"))
48
+ require(bind["id"] == metric and bind["device"] == raw["measurement"]["device"] and
49
+ bind["robot"] == raw["robot"] and
50
+ bind["targets"] and bind["unit"] and bind["configuration"], "SENSOR_BINDING_MISMATCH")
51
+ value, scale = raw["measurement"]["value"], bind["scale"]
52
+ require(all(type(n) in (float,int) and math.isfinite(n) for n in (value,scale)) and
53
+ scale > 0 and math.isfinite(value*scale), "NONFINITE_SENSOR_MEASUREMENT")
54
+ sim_time, step_size = raw["sim_time"], experiment["data"]["parameters"]["time_step"]
55
+ require(all(type(n) in (float,int) and math.isfinite(n) for n in (sim_time,step_size))
56
+ and step_size > 0, "INVALID_SENSOR_CLOCK")
57
+ t = sim_time / step_size
58
+ require(math.isfinite(t) and t >= 0 and math.isclose(t,round(t),abs_tol=1e-7,rel_tol=0),
59
+ "NONINTEGRAL_SENSOR_STEP")
60
+ return {**{k:raw[k] for k in ("run_id","generation","world_sha256","robot","sim_time","action","operation_id")},
61
+ "id":metric, "unit":bind["unit"], "value":value*scale, "configuration":bind["configuration"],
62
+ "step":round(t), "operation_steps":raw["step"], "targets":bind["targets"],
63
+ "sensor_binding_sha256":entries[0]["sha256"], "raw_sha256":raw_sha256}
64
+
65
+
66
+ def raw_observation(store, run, experiment, obs):
67
+ index = [e for e in experiment["evidence"] if e["sha256"] == obs.get("raw_sha256")
68
+ and e["kind"] == "observation" and e["provenance"]["kind"] == experiment["provenance"]["kind"]]
69
+ if len(index) != 1 or obs.get("run_id") != run:
70
+ return False
71
+ raw = _load_json(store._path(f"webots_runs/{run}/{index[0]['path']}"))
72
+ if index[0]["producer"].get("name") == "webots_mcp.controller_worker":
73
+ return worker_observation(store,run,experiment,raw,obs["id"],index[0]["sha256"]) == obs
74
+ return raw == {k:v for k,v in obs.items() if k != "raw_sha256"}
75
+
76
+
77
+ def attribute(store, run, issue_id):
78
+ manifest, conversion, experiment, hashes = read_case(store, run)
79
+ issues = [i for i in experiment["data"]["issues"] if i["issue_id"] == issue_id]
80
+ require(len(issues) == 1, "ISSUE_NOT_UNIQUE")
81
+ issue = issues[0]
82
+ observations = [o for o in experiment["data"]["observations"]
83
+ if o.get("id") == issue.get("observation_id") and o.get("id")]
84
+ result = dict(run_id=run, issue=issue, source_revision=manifest["source_revision"],
85
+ report_sha256=hashes, candidate_components=[], observation=None, root_cause=None,
86
+ evidence_level="suspected", missing_information=[], next_check="capture indexed observation and map every target")
87
+ if len(observations) != 1:
88
+ result["candidate_components"] = issue.get("candidate_components", [])
89
+ result["missing_information"].append("unique indexed observation")
90
+ return result
91
+ obs = observations[0]
92
+ result["observation"] = obs
93
+ if not raw_observation(store, run, experiment, obs):
94
+ result["missing_information"].append("matched raw observation")
95
+ return result
96
+ result["evidence_level"] = "observed"
97
+ from .assembly import map_candidates
98
+ context = dict(source_revision=manifest["source_revision"], **{
99
+ k: obs.get(k) for k in ("configuration", "generation")})
100
+ result["context"] = context
101
+ components = manifest["data"]["components"]
102
+ targets = obs.get("targets")
103
+ require(isinstance(targets, list) and targets, "OBSERVATION_TARGETS_MISSING")
104
+ found = {}
105
+ for target in targets:
106
+ candidates = map_candidates(components, conversion["data"]["mappings"], target=target,
107
+ snapshot=context, current=context)
108
+ if not candidates:
109
+ result["missing_information"].append("unmapped target: " + str(target))
110
+ for candidate in candidates:
111
+ found[candidate["id"]] = candidate
112
+ if candidate.get("snapshot_context") != context:
113
+ result["missing_information"].append("stale occurrence snapshot")
114
+ result["candidate_components"] = list(found.values())
115
+ if len(found) != 1:
116
+ result["missing_information"].append("unique affected occurrence")
117
+ if found and not result["missing_information"]:
118
+ result.update(evidence_level="derived", next_check="verify a native distance-mate measurement on the isolated source")
119
+ return result
120
+
121
+
122
+ def main():
123
+ parser = argparse.ArgumentParser(description="Inspect one owned simulation issue (read only)")
124
+ for field in ("state_dir", "task_id", "room_id", "run_id", "issue_id"):
125
+ parser.add_argument(field)
126
+ a = parser.parse_args()
127
+ print(json.dumps(attribute(EvidenceStore(Path(a.state_dir).absolute(), task_id=a.task_id, room_id=a.room_id),
128
+ a.run_id, a.issue_id), ensure_ascii=False, indent=2))
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()
@@ -0,0 +1,375 @@
1
+ """One authorized trial. All writes target a newly created isolated assembly pack."""
2
+ from contextlib import contextmanager
3
+ from hashlib import sha256
4
+ from pathlib import Path
5
+ from shutil import copyfile, copyfileobj
6
+ import json
7
+ import math
8
+ import os
9
+ import tempfile
10
+
11
+ from .evidence import _atomic_write, _identifier, _json_bytes, _load_json, _no_reparse
12
+ from .repair import attribute, digest, raw_observation, read_case, require
13
+
14
+
15
+ @contextmanager
16
+ def sw_lock(room_label):
17
+ import sw_bridge
18
+ token = sw_bridge._gate_sw_lock(room_label)
19
+ require(token.get("skipped") is False and token.get("room") == room_label
20
+ and not token.get("busy") and not token.get("timeout"), "SW_EXCLUSIVE_LOCK_REQUIRED")
21
+ try:
22
+ yield
23
+ finally:
24
+ sw_bridge._gate_sw_unlock(token)
25
+
26
+
27
+ class UnsafeClose(RuntimeError):
28
+ """The owned session might still be able to write the candidate."""
29
+
30
+
31
+ @contextmanager
32
+ def opened_pack(app, path):
33
+ """Adapter for an explicitly authorized, dedicated, already-owned empty SW session."""
34
+ require(app.GetDocumentCount() == 0, "DEDICATED_EMPTY_SESSION_REQUIRED")
35
+ import pythoncom
36
+ from win32com.client import VARIANT
37
+ error = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
38
+ warning = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
39
+ try:
40
+ model = app.OpenDoc6(str(path), 2, 1, "", error, warning)
41
+ require(model is not None and error.value == warning.value == 0 and
42
+ Path(model.GetPathName()).resolve() == path.resolve(), "PACK_OPEN_FAILED")
43
+ yield model
44
+ finally:
45
+ try:
46
+ doc = app.GetOpenDocumentByName(str(path))
47
+ if doc is not None and Path(doc.GetPathName()).resolve() == path.resolve():
48
+ app.CloseDoc(doc.GetTitle())
49
+ require(app.GetDocumentCount() == 0, "DOCUMENTS_STILL_OPEN")
50
+ except Exception as error:
51
+ raise UnsafeClose("OWNED_SESSION_CLOSE_UNCONFIRMED") from error
52
+
53
+
54
+ def number(value):
55
+ return type(value) in (int, float) and math.isfinite(value)
56
+
57
+
58
+ def distance_dimension(model, candidate, context, policy, path, resolve=None):
59
+ """Resolve the feature and occurrence afresh. Never retain a native object across rebuild."""
60
+ from .assembly import resolve_reference, resolved_component
61
+ actual = Path(model.GetPathName()).resolve()
62
+ require(actual == path.resolve() and
63
+ model.ConfigurationManager.ActiveConfiguration.Name == context["configuration"],
64
+ "DIRTY_OR_WRONG_DOCUMENT")
65
+ native = resolve_reference(model.Extension, candidate["persistent_reference"], invoke=resolve)
66
+ require(native.GetSuppression2() in (2, 3) and native.Name2 == candidate["instance_path"]
67
+ and Path(native.GetPathName()).resolve() == (path.parent / Path(candidate["source_file"]).name).resolve(),
68
+ "WRONG_OR_SUPPRESSED_OCCURRENCE")
69
+ resolved_component(candidate, resolution=dict(method="IModelDocExtension.GetObjectByPersistReference3",
70
+ status="resolved", object_type="IComponent2", snapshot=context, component_id=candidate["id"],
71
+ owner_document=candidate["owner_document"], instance_path=native.Name2,
72
+ configuration=native.ReferencedConfiguration, persistent_reference=candidate["persistent_reference"]),
73
+ snapshot=context, current=context)
74
+ feature = resolve_reference(model.Extension, policy["mate_reference"], invoke=resolve)
75
+ mate = feature.GetSpecificFeature2()
76
+ require(feature.IsSuppressed2(1, None) in ([False], (False,)) and mate.Type == 5 and
77
+ mate.GetMateEntityCount() == 2, "ACTIVE_DISTANCE_MATE_REQUIRED")
78
+ entities = [mate.MateEntity(i).ReferenceComponent for i in range(2)]
79
+ require(any(c is not None and c.Name2 == native.Name2 and c.ReferencedConfiguration == native.ReferencedConfiguration
80
+ and c.GetPathName() == native.GetPathName() for c in entities), "MATE_COMPONENT_MISMATCH")
81
+ display = feature.GetFirstDisplayDimension()
82
+ dim = display.GetDimension2(0)
83
+ require(dim.FullName == policy["dimension"] and dim.ReadOnly is False, "DIMENSION_NOT_WRITABLE")
84
+ return dim
85
+
86
+
87
+ def checked_distance(dim, expected):
88
+ values = dim.GetSystemValue3(1, None)
89
+ require(isinstance(values, (list, tuple)) and len(values) == 1 and number(values[0]) and
90
+ math.isclose(values[0], expected, rel_tol=0, abs_tol=1e-12), "DIMENSION_DRIFT")
91
+
92
+
93
+ def edit_distance(model, candidate, context, policy, path, *, resolve=None, save=None):
94
+ """Use only an already opened isolated document. Helpers may be injected for native ABI tests."""
95
+ require(model.GetSaveFlag() is False, "DIRTY_DOCUMENT")
96
+ dim = distance_dimension(model,candidate,context,policy,path,resolve)
97
+ checked_distance(dim,policy["old_m"])
98
+ status = dim.SetSystemValue3(policy["new_m"], 1, None)
99
+ require(type(status) is int and status == 0 and model.ForceRebuild3(False) is True and
100
+ model.Extension.GetWhatsWrongCount() == 0, "SET_OR_REBUILD_FAILED")
101
+ checked_distance(distance_dimension(model,candidate,context,policy,path,resolve),policy["new_m"])
102
+ if save is None:
103
+ import pythoncom
104
+ from win32com.client import VARIANT
105
+ error = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
106
+ warning = VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, 0)
107
+ saved = model.Save3(1, error, warning)
108
+ outcome = (saved, error.value, warning.value)
109
+ else:
110
+ outcome = save(model)
111
+ require(outcome == (True, 0, 0) and not model.GetSaveFlag(), "SAVE_NOT_CLEAN")
112
+
113
+
114
+ def verified_physics(store, run, conversion, experiment):
115
+ from .physical_gate import evaluate
116
+ data = experiment["data"]
117
+ pair = data["physics_pair"]
118
+ policy = pair["policy"]
119
+ traces = []
120
+ for field in ("coarse","fine"):
121
+ ref = pair[field]
122
+ trace_run = ref["run_id"]
123
+ report = experiment if trace_run == run else store.read_report(trace_run,"experiment_report")
124
+ require(report["source_revision"] == experiment["source_revision"] and
125
+ report["provenance"]["kind"] == "real-webots", "PAIR_OWNER_OR_REVISION_MISMATCH")
126
+ raw = [e for e in report["evidence"] if e["kind"] == "physics-trace" and
127
+ e["sha256"] == ref["sha256"] and e["provenance"]["kind"] == "real-webots"]
128
+ require(len(raw) == 1, "PAIR_RAW_TRACE_REQUIRED")
129
+ trace = _load_json(store._path(f"webots_runs/{trace_run}/{raw[0]['path']}"))
130
+ require(trace.get("run_id") == trace_run and "raw_evidence_sha256" not in trace,
131
+ "PAIR_TRACE_BINDING_MISMATCH")
132
+ trace["raw_evidence_sha256"] = ref["sha256"]
133
+ coverage = {e["sha256"]:e["path"] for e in report["evidence"] if e["kind"] == "collision-coverage"
134
+ and e["provenance"]["kind"] == "real-webots"}
135
+ for pair_name, claimed in trace["coverage"].items():
136
+ path = coverage.get(claimed["evidence_sha256"])
137
+ require(path, "UNINDEXED_COLLISION_COVERAGE")
138
+ attachment = _load_json(store._path(f"webots_runs/{trace_run}/{path}"))
139
+ require(all(_json_bytes(attachment.get(k)) == _json_bytes(trace[k])
140
+ for k in ("run_id","generation","world_sha256","robot"))
141
+ and json.dumps(attachment.get("pairs",{}).get(pair_name),sort_keys=True) ==
142
+ json.dumps({k:v for k,v in claimed.items() if k != "evidence_sha256"},sort_keys=True),
143
+ "COLLISION_COVERAGE_CONTENT_MISMATCH")
144
+ traces.append(trace)
145
+ coarse, fine = traces
146
+ require(coarse["run_id"] == run and any(e["sha256"] == coarse["conditions"]["model"] and
147
+ e["kind"] == "exported-model" for e in conversion["evidence"]), "EXPORTED_MODEL_BINDING_REQUIRED")
148
+ gate = evaluate(coarse,fine,policy)
149
+ require(gate == data["verdict"] and gate.get("physical_valid") is True and
150
+ gate.get("provenance") == "real-webots" and gate.get("verdict") in ("PASS","FAIL"),
151
+ "PHYSICS_NOT_VALID")
152
+ metric = policy["metric"]
153
+ criteria = [m for m in data["parameters"]["thresholds"] if m["id"] == metric]
154
+ rows = [o for o in data["observations"] if o["id"] == metric]
155
+ require(len(rows) == 1 and type(rows[0].get("step")) is int and 1 <= rows[0]["step"] <= len(coarse["samples"]),
156
+ "OBSERVATION_STEP_MISMATCH")
157
+ if "sensor_binding_sha256" in rows[0]:
158
+ require(rows[0]["sensor_binding_sha256"] == coarse["conditions"]["sensor_binding"],
159
+ "PHYSICS_SENSOR_BINDING_MISMATCH")
160
+ sample = coarse["samples"][rows[0]["step"]-1]
161
+ parameters = data["parameters"]
162
+ require(type(parameters["seed"]) is int and parameters["seed"] == coarse["seed"] and
163
+ number(parameters["time_step"]) and math.isclose(parameters["time_step"]*1000,coarse["step_ms"],
164
+ rel_tol=0,abs_tol=1e-9) and number(rows[0]["sim_time"]) and
165
+ math.isclose(rows[0]["sim_time"]*1000,sample["time_ms"],rel_tol=0,abs_tol=1e-6) and
166
+ rows[0]["value"] == sample["value"], "OBSERVATION_CLOCK_OR_VALUE_MISMATCH")
167
+ require(len(criteria) == len(rows) == 1 and rows[0]["unit"] == criteria[0]["unit"] == policy["unit"] and
168
+ rows[0]["generation"] == coarse["generation"] and rows[0]["robot"] == coarse["robot"] and
169
+ rows[0]["world_sha256"] == coarse["world_sha256"] and rows[0]["value"] ==
170
+ max(row["value"] for trace in traces for row in trace["samples"]) and
171
+ criteria[0]["direction"] == "max" and criteria[0]["limit"] == policy["metric_max"],
172
+ "PHYSICS_METRIC_BINDING_MISMATCH")
173
+ return {"policy":policy,"seed":coarse["seed"],"step_ms":coarse["step_ms"],
174
+ "conditions":{k:v for k,v in coarse["conditions"].items() if k != "model"}}
175
+
176
+
177
+ def checked_experiment(store, run):
178
+ m, c, e, hashes = read_case(store, run)
179
+ require([r["provenance"]["kind"] for r in (m,c,e)] == ["real-sw-export","real-sw-export","real-webots"]
180
+ and all(r["status"] == "supported" and not r["missing_information"] and r["evidence"] for r in (m,c,e)),
181
+ "REAL_SUPPORTED_EVIDENCE_REQUIRED")
182
+ require(exported_revision(store,run,m["source_revision"]) ==
183
+ {k:hashes[k] for k in ("model_manifest","conversion_report")}, "EXPORT_REPORT_DRIFT")
184
+ data = e["data"]
185
+ link = data.get("conversion") or {}
186
+ require(link == {"path":"conversion_report.json","sha256":hashes["conversion_report"]},
187
+ "CONVERSION_LINK_MISMATCH")
188
+ data["verified_physics_conditions"] = verified_physics(store,run,c,e)
189
+ params = data["parameters"]
190
+ for key in ("seed","initial_state","time_step","thresholds","environment","budget"):
191
+ require(params.get(key) is not None, "COMPARISON_PARAMETER_MISSING:" + key)
192
+ require(data["scene"] and data["actions"] and params["thresholds"], "COMPARISON_CASE_MISSING")
193
+ for obs in data["observations"]:
194
+ require(raw_observation(store, run, e, obs) and obs.get("robot") and
195
+ isinstance(obs.get("world_sha256"), str) and len(obs["world_sha256"]) == 64 and
196
+ type(obs.get("generation")) is int and number(obs.get("sim_time")) and
197
+ type(obs.get("step")) is int, "UNVERIFIED_METRIC_OBSERVATION")
198
+ return m, data, hashes
199
+
200
+
201
+ def scores(data):
202
+ result = {}
203
+ for metric in data["parameters"]["thresholds"]:
204
+ name, unit, limit, direction = (metric[k] for k in ("id","unit","limit","direction"))
205
+ require(name not in result and unit and number(limit) and direction in ("max","min"), "INVALID_CRITERION")
206
+ rows = [o for o in data["observations"] if o.get("id") == name]
207
+ require(len(rows) == 1 and rows[0].get("unit") == unit and number(rows[0].get("value")),
208
+ "MISSING_REGRESSION_METRIC:" + name)
209
+ result[name] = (rows[0]["value"] - limit) * (1 if direction == "max" else -1)
210
+ return result
211
+
212
+
213
+ def restore_member(snapshot, destination):
214
+ """Replace only a staged file and avoid buffering a CAD document in memory."""
215
+ _no_reparse(destination.parent)
216
+ _no_reparse(destination)
217
+ descriptor, name = tempfile.mkstemp(dir=destination.parent)
218
+ try:
219
+ with os.fdopen(descriptor,"wb") as sink, snapshot.open("rb") as source:
220
+ copyfileobj(source,sink)
221
+ sink.flush()
222
+ os.fsync(sink.fileno())
223
+ os.replace(name,destination)
224
+ finally:
225
+ Path(name).unlink(missing_ok=True)
226
+
227
+
228
+ def exported_revision(store, run, revision):
229
+ reports = [store.read_report(run, name) for name in ("model_manifest","conversion_report")]
230
+ for report in reports:
231
+ require(report["source_revision"] == revision and report["provenance"]["kind"] == "real-sw-export"
232
+ and report["status"] == "supported" and not report["missing_information"] and report["evidence"],
233
+ "CANDIDATE_EXPORT_UNVERIFIED")
234
+ manifest, conversion = reports
235
+ pack = manifest["data"].get("source_pack") or {}
236
+ snapshots = {e["sha256"] for e in manifest["evidence"] if e["kind"] == "cad-pack-member"
237
+ and e["provenance"]["kind"] == "real-sw-export"}
238
+ require(revision in snapshots and pack and set(pack.values()) <= snapshots, "CAD_SNAPSHOT_MISSING")
239
+ require(not conversion["data"].get("omissions") and not conversion["data"].get("missing_information")
240
+ and conversion["data"].get("support") == "supported" and any(
241
+ e["kind"] == "exported-model" and e["provenance"]["kind"] == "real-sw-export"
242
+ for e in conversion["evidence"]), "CANDIDATE_MODEL_UNVERIFIED")
243
+ return {r["report_type"]:sha256(_json_bytes(r)).hexdigest() for r in reports}
244
+
245
+
246
+ def trial(store, run, issue_id, policy, *, source_dir, room_label, operation_id, candidate_run,
247
+ open_owned, export_candidate, retest, lock=sw_lock):
248
+ """Host-supplied policy and callbacks are trusted capabilities, never model-generated tool arguments.
249
+ open_owned(root) must open only this detached pack and close it before returning.
250
+ export_candidate(root, candidate_run) publishes the A01 reports under SW lock.
251
+ retest(candidate_run, baseline_run) publishes the A04 experiment using the fixed case, with no SW lock.
252
+ """
253
+ _identifier(operation_id)
254
+ _identifier(issue_id)
255
+ _identifier(candidate_run)
256
+ require(candidate_run != run and not store._path(f"webots_runs/{candidate_run}").exists(),
257
+ "FRESH_CANDIDATE_RUN_REQUIRED")
258
+ require(isinstance(room_label, str) and room_label.strip() and store.room_id ==
259
+ "room-" + sha256(room_label.encode("utf-8")).hexdigest(), "SW_ROOM_OWNER_MISMATCH")
260
+ before, scenario, fingerprints = checked_experiment(store, run)
261
+ attribution = attribute(store, run, issue_id)
262
+ require(attribution["evidence_level"] == "derived" and not attribution["missing_information"] and
263
+ attribution["report_sha256"] == fingerprints == policy["approved_reports"], "ATTRIBUTION_OR_APPROVAL_CHANGED")
264
+ candidate, = attribution["candidate_components"]
265
+ require(candidate["id"] == policy["component_id"] and policy["kind"] == "distance_mate"
266
+ and policy["reason"].strip() and policy["expected_improvement"].strip(), "UNAUTHORIZED_TARGET")
267
+ old, new, low, high = (policy[k] for k in ("old_m","new_m","min_m","max_m"))
268
+ require(all(number(v) for v in (old,new,low,high)) and 0 <= low <= new <= high and old != new,
269
+ "PARAMETER_OUT_OF_RANGE")
270
+ metric = attribution["observation"]["id"]
271
+ require(scenario["verdict"].get("verdict") == "FAIL" and
272
+ metric == scenario["verified_physics_conditions"]["policy"]["metric"], "BASELINE_PHYSICAL_FAILURE_REQUIRED")
273
+ previous = scores(scenario)
274
+ require(previous.get(metric, 0) > 0, "NO_BASELINE_VIOLATION")
275
+ pack = policy["files"]
276
+ root_name = policy["root"]
277
+ source_dir = Path(source_dir).resolve()
278
+ require(pack == before["data"].get("source_pack") and root_name in pack and root_name.lower().endswith(".sldasm") and
279
+ Path(candidate["owner_document"]).resolve() == source_dir / root_name and
280
+ candidate["source_file"] and Path(candidate["source_file"]).resolve().parent == source_dir and
281
+ Path(candidate["source_file"]).name in pack and pack[root_name] == before["source_revision"],
282
+ "PACK_OR_OWNER_MISMATCH")
283
+ for name in pack:
284
+ require(Path(name).name == name and name.lower().endswith((".sldasm",".sldprt")) and ":" not in name,
285
+ "FLAT_CAD_PACK_REQUIRED")
286
+ base = store._path(f"webots_runs/{run}/repair-{operation_id}")
287
+ require(not base.is_relative_to(source_dir) and not source_dir.is_relative_to(base), "ISOLATION_REQUIRED")
288
+ claim = store._path(f"webots_runs/{run}/repair-claim-{issue_id}.json")
289
+ with claim.open("xb") as stream:
290
+ stream.write(_json_bytes({"operation_id":operation_id,"baseline":fingerprints}))
291
+ base.mkdir() # One trial per baseline issue, even if a client supplies a new operation ID.
292
+ original, work = base/"snapshot", base/"candidate"
293
+ original.mkdir()
294
+ work.mkdir()
295
+ journal = dict(operation_id=operation_id, baseline_run=run, reports=fingerprints, policy=policy,
296
+ candidate_component=candidate, original_revision=before["source_revision"])
297
+ def record(state, **fields):
298
+ journal.update(state=state, **fields)
299
+ _atomic_write(base/"trial.json", _json_bytes(journal))
300
+ def verify_sources():
301
+ for name, expected in pack.items():
302
+ src = source_dir / name
303
+ _no_reparse(src)
304
+ require(digest(src) == expected, "SOURCE_DRIFT:" + name)
305
+ record("PREPARING")
306
+ verify_sources()
307
+ for name, expected in pack.items():
308
+ copyfile(source_dir/name, original/name)
309
+ require(digest(original/name) == expected, "SNAPSHOT_DRIFT")
310
+ copyfile(original/name, work/name)
311
+ root = work/root_name
312
+ try:
313
+ with lock(room_label):
314
+ verify_sources()
315
+ require(read_case(store,run)[3] == fingerprints, "BASELINE_DRIFT")
316
+ require(all(digest(work/name) == expected for name,expected in pack.items()), "STAGED_PACK_DRIFT")
317
+ record("EDIT_DISPATCHED_OUTCOME_UNKNOWN")
318
+ with open_owned(root) as model:
319
+ dependencies = model.GetDependencies2(True,True,False)
320
+ expected = {work/name for name in pack if name != root_name}
321
+ require(isinstance(dependencies,(list,tuple)) and len(dependencies) % 2 == 0 and
322
+ {Path(p).resolve() for p in dependencies[1::2]} == expected,
323
+ "EXTERNAL_OR_MISSING_DOCUMENT_REFERENCE")
324
+ members = model.GetComponents(False)
325
+ require(isinstance(members,(list,tuple)) and members and all(
326
+ c.GetSuppression2() in (2,3) and Path(c.GetPathName()).resolve() in expected
327
+ for c in members), "EXTERNAL_ASSEMBLY_DEPENDENCY")
328
+ edit_distance(model, candidate, attribution["context"], policy, root)
329
+ require(all(digest(work/name) == expected for name,expected in pack.items() if name != root_name),
330
+ "UNAUTHORIZED_MEMBER_CHANGE")
331
+ revised = digest(root)
332
+ require(revised != pack[root_name], "NO_SAVED_REVISION")
333
+ record("REBUILT", revised=revised, candidate_run=candidate_run)
334
+ export_candidate(root, candidate_run)
335
+ export_hashes = exported_revision(store,candidate_run,revised)
336
+ require(not store._path(f"webots_runs/{candidate_run}/experiment_report.json").exists(),
337
+ "PREEXISTING_RETEST_REPORT")
338
+ record("EXPORTED", export_reports=export_hashes)
339
+ verify_sources()
340
+ record("RETEST_DISPATCHED_OUTCOME_UNKNOWN")
341
+ retest(candidate_run,run)
342
+ after, replay, new_hashes = checked_experiment(store, candidate_run)
343
+ require(all(new_hashes[k] == v for k,v in export_hashes.items()), "EXPORT_CHANGED_DURING_RETEST")
344
+ require(after["data"].get("source_pack") == pack | {root_name:revised}, "RETEST_CAD_DEPENDENCY_DRIFT")
345
+ require(after["source_revision"] == revised and all(_json_bytes(replay[k]) == _json_bytes(scenario[k]) for k in
346
+ ("scene","parameters","actions","verified_physics_conditions")), "RETEST_NOT_COMPARABLE")
347
+ require(replay["verdict"].get("verdict") == "PASS", "RETEST_PHYSICAL_FAILURE")
348
+ require(not {i["issue_id"] for i in replay["issues"] if i.get("severity") in ("error","critical")}
349
+ - {i["issue_id"] for i in scenario["issues"] if i.get("severity") in ("error","critical")},
350
+ "NEW_SEVERE_ISSUE")
351
+ require(read_case(store,run)[3] == fingerprints, "BASELINE_DRIFT")
352
+ current = scores(replay)
353
+ require(current[metric] <= 0 and all(current[k] <= previous[k] for k in previous),
354
+ "NO_IMPROVEMENT_OR_REGRESSION")
355
+ require(digest(root) == revised, "CANDIDATE_DRIFT")
356
+ verify_sources()
357
+ record("CANDIDATE_VERIFIED", retest_run=candidate_run, retest_reports=new_hashes,
358
+ baseline_scores=previous, retest_scores=current)
359
+ except UnsafeClose as error:
360
+ record("OUTCOME_UNKNOWN", error=str(error))
361
+ raise
362
+ except Exception as error:
363
+ try:
364
+ record("ROLLBACK_PENDING", error=str(error))
365
+ finally:
366
+ for name, expected in pack.items():
367
+ _no_reparse(base)
368
+ _no_reparse(original)
369
+ _no_reparse(original/name)
370
+ require(digest(original/name) == expected, "SNAPSHOT_CORRUPTED")
371
+ restore_member(original/name,work/name)
372
+ require(digest(work/name) == expected, "ROLLBACK_FAILED")
373
+ record("ROLLED_BACK", restored=pack)
374
+ raise
375
+ return journal