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.
- package/LICENSE +21 -0
- package/README.md +772 -3
- package/engineering/plugins/dsh-engineering-ui/cordis.patch.yml +5 -0
- package/engineering/plugins/dsh-engineering-ui/icon.svg +14 -0
- package/engineering/plugins/dsh-engineering-ui/lib/client.js +8257 -0
- package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
- package/engineering/plugins/dsh-engineering-ui/lib/index.js +5634 -0
- package/engineering/plugins/dsh-engineering-ui/lib/job-wait-guard.js +236 -0
- package/engineering/plugins/dsh-engineering-ui/lib/mem-guard.js +331 -0
- package/engineering/plugins/dsh-engineering-ui/lib/webots.js +114 -0
- package/engineering/plugins/dsh-engineering-ui/locale/en.json +6 -0
- package/engineering/plugins/dsh-engineering-ui/locale/zh.json +6 -0
- package/engineering/plugins/dsh-engineering-ui/preset-engineering.patch.yml +601 -0
- package/engineering/skills/assembly-orchestration/SKILL.md +651 -0
- package/engineering/skills/cad-workflow/SKILL.md +132 -0
- package/engineering/skills/mode-selection/SKILL.md +660 -0
- package/engineering/skills/physics-in-loop/SKILL.md +238 -0
- package/engineering/skills/physics-in-loop/references/feadocs.md +142 -0
- package/engineering/skills/solidworks-bridge/SKILL.md +445 -0
- package/engineering/skills/sw-design/SKILL.md +112 -0
- package/engineering/skills/sw-design/references/autocad-workflow.md +81 -0
- package/engineering/skills/sw-design/references/gbt-drafting.md +71 -0
- package/engineering/skills/sw-to-cad/SKILL.md +121 -0
- package/engineering/skills/webots-simulation/SKILL.md +58 -0
- package/engineering/tools/LICENSE.Auto-SolidWorks +21 -0
- package/engineering/tools/_plugin_paths.py +93 -0
- package/engineering/tools/_root.py +330 -0
- package/engineering/tools/_store.py +446 -0
- package/engineering/tools/ac_bridge.py +459 -0
- package/engineering/tools/ac_rules.json +21 -0
- package/engineering/tools/ac_validate.py +699 -0
- package/engineering/tools/ask_user.py +887 -0
- package/engineering/tools/build_arm1.py +46 -0
- package/engineering/tools/build_assembly.py +94 -0
- package/engineering/tools/build_base.py +57 -0
- package/engineering/tools/build_housing.py +70 -0
- package/engineering/tools/build_structural.py +43 -0
- package/engineering/tools/build_transmission.py +69 -0
- package/engineering/tools/cadx.lsp +128 -0
- package/engineering/tools/choice_contract.py +61 -0
- package/engineering/tools/conn_state.py +1671 -0
- package/engineering/tools/defense_gate.py +3374 -0
- package/engineering/tools/drawing_input.py +314 -0
- package/engineering/tools/env_check.py +467 -0
- package/engineering/tools/examples/build_axis.py +142 -0
- package/engineering/tools/examples/build_cup.py +56 -0
- package/engineering/tools/examples/build_plate.py +58 -0
- package/engineering/tools/examples/build_sphere.py +30 -0
- package/engineering/tools/examples/build_transition_duct.py +381 -0
- package/engineering/tools/gate_integrity.py +401 -0
- package/engineering/tools/mode2_flow.py +3589 -0
- package/engineering/tools/mode_checker.py +77 -0
- package/engineering/tools/mode_gate.py +8013 -0
- package/engineering/tools/part_registry.py +4266 -0
- package/engineering/tools/physics/__init__.py +50 -0
- package/engineering/tools/physics/_compat.py +12 -0
- package/engineering/tools/physics/design_state.py +142 -0
- package/engineering/tools/physics/domain_validator.py +751 -0
- package/engineering/tools/physics/fatigue.py +489 -0
- package/engineering/tools/physics/fea_solver.py +1163 -0
- package/engineering/tools/physics/feapy_solver.py +314 -0
- package/engineering/tools/physics/gb_materials.json +1228 -0
- package/engineering/tools/physics/geometry_gate.py +337 -0
- package/engineering/tools/physics/load_case.py +951 -0
- package/engineering/tools/physics/material_db.py +874 -0
- package/engineering/tools/physics/mesh_adapter.py +321 -0
- package/engineering/tools/physics/refine_rules.py +272 -0
- package/engineering/tools/physics/rules/gb_base_rules.json +377 -0
- package/engineering/tools/physics/rules/gb_drafting_rules.json +57 -0
- package/engineering/tools/physics/rules/gb_electrical_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_fasteners_rules.json +73 -0
- package/engineering/tools/physics/rules/gb_housing_rules.json +41 -0
- package/engineering/tools/physics/rules/gb_manufacturing_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_material_verify_rules.json +72 -0
- package/engineering/tools/physics/rules/gb_materials_rules.json +377 -0
- package/engineering/tools/physics/rules/gb_safety_rules.json +89 -0
- package/engineering/tools/physics/rules/gb_springs_rules.json +41 -0
- package/engineering/tools/physics/rules/gb_standards_registry.json +1646 -0
- package/engineering/tools/physics/rules/gb_structural_rules.json +201 -0
- package/engineering/tools/physics/rules/gb_surface_rules.json +25 -0
- package/engineering/tools/physics/rules/gb_transmission_rules.json +233 -0
- package/engineering/tools/physics/rules/housing_rules.json +48 -0
- package/engineering/tools/physics/rules/manifest.json +54 -0
- package/engineering/tools/physics/rules/mold_rules.json +48 -0
- package/engineering/tools/physics/rules/structural_rules.json +56 -0
- package/engineering/tools/physics/rules/transmission_rules.json +48 -0
- package/engineering/tools/physics/simulation_report.py +829 -0
- package/engineering/tools/physics_bridge.py +2685 -0
- package/engineering/tools/profile-manifest.ps1 +72 -0
- package/engineering/tools/run_regression.py +109 -0
- package/engineering/tools/settings.py +419 -0
- package/engineering/tools/solidworks-modeling.md +519 -0
- package/engineering/tools/sw_bridge.py +8215 -0
- package/engineering/tools/sw_guard.py +1066 -0
- package/engineering/tools/sw_surface.py +87 -0
- package/engineering/tools/swapi.py +14766 -0
- package/engineering/tools/verify_sync.py +218 -0
- package/engineering/tools/visual_check.py +2589 -0
- package/engineering/tools/webots_env.py +121 -0
- package/engineering/tools/webots_mcp/__init__.py +3 -0
- package/engineering/tools/webots_mcp/__main__.py +3 -0
- package/engineering/tools/webots_mcp/assembly.py +391 -0
- package/engineering/tools/webots_mcp/bootstrap.py +55 -0
- package/engineering/tools/webots_mcp/controller_worker.py +124 -0
- package/engineering/tools/webots_mcp/evidence.py +288 -0
- package/engineering/tools/webots_mcp/fake_worker.py +69 -0
- package/engineering/tools/webots_mcp/fault_resources.py +121 -0
- package/engineering/tools/webots_mcp/host.py +45 -0
- package/engineering/tools/webots_mcp/ledger.py +73 -0
- package/engineering/tools/webots_mcp/owned_job.py +70 -0
- package/engineering/tools/webots_mcp/physical_gate.py +187 -0
- package/engineering/tools/webots_mcp/physics.py +152 -0
- package/engineering/tools/webots_mcp/pipeline.py +108 -0
- package/engineering/tools/webots_mcp/recipes.py +129 -0
- package/engineering/tools/webots_mcp/repair.py +132 -0
- package/engineering/tools/webots_mcp/repair_trial.py +375 -0
- package/engineering/tools/webots_mcp/runtime.py +311 -0
- package/engineering/tools/webots_mcp/server.py +217 -0
- package/engineering/tools/webots_mcp/sw_export.py +509 -0
- package/engineering/tools/webots_mcp/transport.py +156 -0
- package/engineering/tools/workflow_gate.py +6134 -0
- 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
|