from __future__ import annotations import ast import re import subprocess import sys import tempfile from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from step_editor.scdm_backend import ScdmBackendInfo # noqa: E402 from step_editor.scdm_edit_runner import generate_scdm_edit_script, prepare_scdm_edit_job, run_scdm_edit_job # noqa: E402 from step_editor.scdm_schema import read_json, write_json # noqa: E402 def _assert(condition: bool, message: str) -> None: if not condition: raise AssertionError(message) def _fake_spaceclaim(path: Path) -> Path: path.parent.mkdir(parents=True, exist_ok=True) path.write_text("fake", encoding="utf-8") return path def _script_path_from_command(command: list[str] | tuple[str, ...]) -> Path: for item in command: if item.startswith("/RunScript="): return Path(item.split("=", 1)[1]) raise AssertionError(f"missing /RunScript argument: {command}") def _job_path_from_script(script_path: Path) -> Path: text = script_path.read_text(encoding="utf-8") match = re.search(r"^JOB_PATH = (.+)$", text, flags=re.MULTILINE) _assert(match is not None, f"generated script should embed JOB_PATH: {script_path}") return Path(ast.literal_eval(match.group(1))) def _successful_runner(command, **_kwargs): # type: ignore[no-untyped-def] script_path = _script_path_from_command(command) job_path = _job_path_from_script(script_path) job = read_json(job_path) outputs = job.get("outputs") _assert(isinstance(outputs, dict), f"job outputs should be an object: {job}") output_step = Path(str(outputs["outputStep"])) output_step.write_text("ISO-10303-21;\n/* fake SCDM output */\nEND-ISO-10303-21;\n", encoding="utf-8") write_json( outputs["result"], { "ok": True, "reason": "ok", "message": "fake edit finished", "outputStep": str(output_step), "backendOperation": job.get("target", {}).get("backendOperation"), }, ) return subprocess.CompletedProcess(command, 0, stdout="ok", stderr="") def _failure_runner(command, **_kwargs): # type: ignore[no-untyped-def] script_path = _script_path_from_command(command) job_path = _job_path_from_script(script_path) job = read_json(job_path) outputs = job.get("outputs") _assert(isinstance(outputs, dict), f"job outputs should be an object: {job}") write_json( outputs["error"], { "ok": False, "reason": "fake-failed", "message": "fake SCDM command failed", "backendOperation": job.get("target", {}).get("backendOperation"), }, ) return subprocess.CompletedProcess(command, 7, stdout="", stderr="fake failure") def _missing_output_runner(command, **_kwargs): # type: ignore[no-untyped-def] script_path = _script_path_from_command(command) job_path = _job_path_from_script(script_path) job = read_json(job_path) outputs = job.get("outputs") _assert(isinstance(outputs, dict), f"job outputs should be an object: {job}") write_json( outputs["result"], { "ok": True, "reason": "ok", "message": "fake success without STEP", "outputStep": str(outputs["outputStep"]), }, ) return subprocess.CompletedProcess(command, 0, stdout="", stderr="") def _empty_output_runner(command, **_kwargs): # type: ignore[no-untyped-def] script_path = _script_path_from_command(command) job_path = _job_path_from_script(script_path) job = read_json(job_path) outputs = job.get("outputs") _assert(isinstance(outputs, dict), f"job outputs should be an object: {job}") Path(str(outputs["outputStep"])).write_text("", encoding="utf-8") write_json( outputs["result"], { "ok": True, "reason": "ok", "message": "fake success with empty STEP", "outputStep": str(outputs["outputStep"]), }, ) return subprocess.CompletedProcess(command, 0, stdout="", stderr="") def main() -> int: with tempfile.TemporaryDirectory(prefix="step_editor_scdm_edit_") as temp: root = Path(temp) step_path = root / "sample.step" step_path.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8") backend = ScdmBackendInfo(path=_fake_spaceclaim(root / "SpaceClaim.exe"), source="test", version="v222") signature = { "objectType": "hole", "faceIds": [85, 94], "bodyIndex": 0, "faceOrdinal": 12, "faceOrdinals": [12, 19], "scdmFaceLocators": [ {"bodyIndex": 0, "faceOrdinal": 12, "globalFaceOrdinal": 85}, {"bodyIndex": 0, "faceOrdinal": 19, "globalFaceOrdinal": 96}, ], "center": [0.5, 1.0, 9.5], "axis": [0.0, 0.0, 1.0], } prepared = prepare_scdm_edit_job( step_path, output_dir=root / "prepared", backend=backend, capability_key="hole.diameter", target_value="0.75", object_id="hole:85-94", object_signature=signature, timeout_seconds=45.0, ) _assert(prepared.get("ok") is True, f"edit job should be prepared: {prepared}") job_path = Path(str(prepared["job_path"])) script_path = Path(str(prepared["script_path"])) _assert(job_path.is_file(), "scdm_edit_job.json should be written") _assert(script_path.is_file(), "scdm_edit.py should be written") job = read_json(job_path) _assert(job.get("adapter") == "spaceclaim-v1", f"bad adapter: {job}") _assert(job.get("schemaVersion") == 1, f"bad schema version: {job}") _assert(job.get("model", {}).get("sourceStep") == str(step_path.resolve(strict=False)), f"bad source path: {job}") _assert(job.get("model", {}).get("rollbackStep") == str(step_path.resolve(strict=False)), f"bad rollback path: {job}") _assert(job.get("target", {}).get("capabilityKey") == "hole.diameter", f"bad capability: {job}") _assert(job.get("target", {}).get("backendOperation") == "change_hole_diameter", f"bad operation: {job}") _assert(job.get("target", {}).get("value") == 0.75, f"target should be numeric: {job}") _assert(job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [85, 94], f"bad signature: {job}") _assert(job.get("execution", {}).get("isolatedProcess") is True, f"job should record isolated execution: {job}") script = script_path.read_text(encoding="utf-8") for token in ( "change_hole_diameter", "move_hole_axis", "move_slot", "change_slot_width", "change_slot_depth", "move_boss", "change_boss_height", "change_boss_diameter", "change_round_radius", "change_chamfer_distance", "change_shell_thickness", "change_pattern_spacing", "change_pattern_segment_spacing", "move_pattern_instance", "segment_before", "segment_split", "segment_single_left", "segment_single_right", "wallFaceLocators", "pull_face_offset", "fill_feature", "delete_round_or_chamfer", "StandardHoles.ModifyHoleRadius", "ConstantRound.ModifyRadius", "Chamfer.ModifyDistance", "OffsetFaces.Execute", "Move.Translate", "MoveOptions", "ConstantRoundOptions", "ChamferOptions", "OffsetFaceOptions", "FillOptions", "FillMode", "Delete.Execute", "DocumentSave", "scdmFaceLocators", "heightFaceLocators", "depthFaceLocators", "diameterFaceLocators", "patternInstances", "bodyLocators", "instanceKind", "result.json", "error.json", ): _assert(token in script, f"generated edit script missing {token}") _assert("Pull.Execute" not in script, "generated edit script should use documented OffsetFaces instead of Pull.Execute") _assert("_delete_selection(selection, fill_errors)" in script, "generated edit script should fall back from Fill to Delete") _assert( "if fill is None:\n raise Exception('capability_not_implemented: Fill command not available')" not in script, "Delete-capable SCDM environments must not fail merely because Fill is missing", ) generated = generate_scdm_edit_script(job_path) _assert("JOB_PATH =" in generated and job_path.name in generated, "generated edit script should embed the job path") slot_signature = { "objectType": "slot", "faceIds": [30, 31, 32], "bodyIndex": 0, "faceOrdinal": 30, "faceOrdinals": [30, 31, 32], "globalFaceOrdinal": 40, "globalFaceOrdinals": [40, 41, 42], "width": 2.0, "depth": 1.5, "depthAxis": [0.0, 0.0, -1.0], "depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 42}], "center": [1.0, 2.0, 3.0], "axis": [1.0, 0.0, 0.0], } slot_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "slot-position-prepared", backend=backend, capability_key="slot.position", target_value=[1.0, 2.0, 5.0], object_id="slot:30-31-32", object_signature=slot_signature, ) _assert(slot_prepared.get("ok") is True, f"slot.position should be productized and prepare an edit job: {slot_prepared}") slot_job = read_json(slot_prepared["job_path"]) _assert(slot_job.get("target", {}).get("backendOperation") == "move_slot", f"slot.position should route to move_slot: {slot_job}") _assert(slot_job.get("target", {}).get("value") == [1.0, 2.0, 5.0], f"slot.position target should be vector3: {slot_job}") _assert(slot_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [30, 31, 32], f"slot faces should be preserved: {slot_job}") slot_width_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "slot-width-prepared", backend=backend, capability_key="slot.width", target_value="2.5", object_id="slot:30-31-32", object_signature=slot_signature, ) _assert(slot_width_prepared.get("ok") is True, f"slot.width should be productized and prepare an edit job: {slot_width_prepared}") slot_width_job = read_json(slot_width_prepared["job_path"]) _assert(slot_width_job.get("target", {}).get("backendOperation") == "change_slot_width", f"slot.width should route to change_slot_width: {slot_width_job}") _assert(slot_width_job.get("target", {}).get("value") == 2.5, f"slot.width target should be numeric: {slot_width_job}") _assert(slot_width_job.get("object", {}).get("geometrySignature", {}).get("width") == 2.0, f"slot width signature should be preserved: {slot_width_job}") slot_width_noop = prepare_scdm_edit_job( step_path, output_dir=root / "slot-width-noop", backend=backend, capability_key="slot.width", target_value="2.0", object_id="slot:30-31-32", object_signature=slot_signature, ) _assert( slot_width_noop.get("ok") is False and slot_width_noop.get("reason") == "target-already-current", f"slot.width no-op should be blocked before launching SCDM: {slot_width_noop}", ) slot_depth_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "slot-depth-prepared", backend=backend, capability_key="slot.depth", target_value="2.0", object_id="slot:30-31-32", object_signature=slot_signature, ) _assert(slot_depth_prepared.get("ok") is True, f"slot.depth should be productized and prepare an edit job: {slot_depth_prepared}") slot_depth_job = read_json(slot_depth_prepared["job_path"]) _assert(slot_depth_job.get("target", {}).get("backendOperation") == "change_slot_depth", f"slot.depth should route to change_slot_depth: {slot_depth_job}") _assert(slot_depth_job.get("target", {}).get("value") == 2.0, f"slot.depth target should be numeric: {slot_depth_job}") slot_depth_signature = slot_depth_job.get("object", {}).get("geometrySignature", {}) _assert(slot_depth_signature.get("depth") == 1.5, f"slot depth signature should be preserved: {slot_depth_job}") _assert(slot_depth_signature.get("depthFaceLocators"), f"slot depth bottom face locator should be preserved: {slot_depth_job}") _assert(slot_depth_signature.get("depthAxis") == [0.0, 0.0, -1.0], f"slot depth axis should be preserved: {slot_depth_job}") slot_depth_missing_locator_signature = dict(slot_signature) slot_depth_missing_locator_signature.pop("depthFaceLocators", None) slot_depth_missing_locator = prepare_scdm_edit_job( step_path, output_dir=root / "slot-depth-missing-locator", backend=backend, capability_key="slot.depth", target_value="2.0", object_id="slot:30-31-32", object_signature=slot_depth_missing_locator_signature, ) _assert( slot_depth_missing_locator.get("ok") is False and slot_depth_missing_locator.get("reason") == "object-signature-missing-depth-face-locator", f"slot.depth should be blocked before SCDM when bottom face locator is missing: {slot_depth_missing_locator}", ) boss_signature = { "objectType": "cylindrical_boss", "faceIds": [50, 51, 52], "bodyIndex": 0, "faceOrdinal": 50, "faceOrdinals": [50, 51, 52], "globalFaceOrdinal": 70, "globalFaceOrdinals": [70, 71, 72], "height": 4.0, "diameter": 3.0, "heightFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 52, "globalFaceOrdinal": 72}], "diameterFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 50, "globalFaceOrdinal": 70}], "center": [0.0, 0.0, 2.0], "axis": [0.0, 0.0, 1.0], } boss_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "boss-position-prepared", backend=backend, capability_key="boss.position", target_value=[2.0, 0.0, 2.0], object_id="boss:50-51-52", object_signature=boss_signature, ) _assert(boss_prepared.get("ok") is True, f"boss.position should be productized and prepare an edit job: {boss_prepared}") boss_job = read_json(boss_prepared["job_path"]) _assert(boss_job.get("target", {}).get("backendOperation") == "move_boss", f"boss.position should route to move_boss: {boss_job}") _assert(boss_job.get("target", {}).get("value") == [2.0, 0.0, 2.0], f"boss.position target should be vector3: {boss_job}") _assert(boss_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [50, 51, 52], f"boss faces should be preserved: {boss_job}") boss_height_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "boss-height-prepared", backend=backend, capability_key="boss.height", target_value="5.5", object_id="boss:50-51-52", object_signature=boss_signature, ) _assert(boss_height_prepared.get("ok") is True, f"boss.height should be productized and prepare an edit job: {boss_height_prepared}") boss_height_job = read_json(boss_height_prepared["job_path"]) _assert(boss_height_job.get("target", {}).get("backendOperation") == "change_boss_height", f"boss.height should route to change_boss_height: {boss_height_job}") _assert(boss_height_job.get("target", {}).get("value") == 5.5, f"boss.height target should be numeric: {boss_height_job}") boss_height_signature = boss_height_job.get("object", {}).get("geometrySignature", {}) _assert(boss_height_signature.get("height") == 4.0, f"boss height signature should be preserved: {boss_height_job}") _assert(boss_height_signature.get("heightFaceLocators"), f"boss height top face locator should be preserved: {boss_height_job}") boss_height_missing_locator_signature = dict(boss_signature) boss_height_missing_locator_signature.pop("heightFaceLocators", None) boss_height_missing_locator = prepare_scdm_edit_job( step_path, output_dir=root / "boss-height-missing-locator", backend=backend, capability_key="boss.height", target_value="5.5", object_id="boss:50-51-52", object_signature=boss_height_missing_locator_signature, ) _assert( boss_height_missing_locator.get("ok") is False and boss_height_missing_locator.get("reason") == "object-signature-missing-height-face-locator", f"boss.height should be blocked before SCDM when top face locator is missing: {boss_height_missing_locator}", ) boss_diameter_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "boss-diameter-prepared", backend=backend, capability_key="boss.diameter", target_value="4.5", object_id="boss:50-51-52", object_signature=boss_signature, ) _assert(boss_diameter_prepared.get("ok") is True, f"boss.diameter should be productized and prepare an edit job: {boss_diameter_prepared}") boss_diameter_job = read_json(boss_diameter_prepared["job_path"]) _assert(boss_diameter_job.get("target", {}).get("backendOperation") == "change_boss_diameter", f"boss.diameter should route to change_boss_diameter: {boss_diameter_job}") _assert(boss_diameter_job.get("target", {}).get("value") == 4.5, f"boss.diameter target should be numeric: {boss_diameter_job}") boss_diameter_signature = boss_diameter_job.get("object", {}).get("geometrySignature", {}) _assert(boss_diameter_signature.get("diameter") == 3.0, f"boss diameter signature should be preserved: {boss_diameter_job}") _assert(boss_diameter_signature.get("diameterFaceLocators"), f"boss diameter side face locator should be preserved: {boss_diameter_job}") boss_diameter_missing_locator_signature = dict(boss_signature) boss_diameter_missing_locator_signature.pop("diameterFaceLocators", None) boss_diameter_missing_locator = prepare_scdm_edit_job( step_path, output_dir=root / "boss-diameter-missing-locator", backend=backend, capability_key="boss.diameter", target_value="4.5", object_id="boss:50-51-52", object_signature=boss_diameter_missing_locator_signature, ) _assert( boss_diameter_missing_locator.get("ok") is False and boss_diameter_missing_locator.get("reason") == "object-signature-missing-diameter-face-locator", f"boss.diameter should be blocked before SCDM when side face locator is missing: {boss_diameter_missing_locator}", ) hole_position_noop = prepare_scdm_edit_job( step_path, output_dir=root / "hole-position-noop", backend=backend, capability_key="hole.position", target_value=[0.5, 1.0, 9.5], object_id="hole:85-94", object_signature=signature, ) _assert( hole_position_noop.get("ok") is False and hole_position_noop.get("reason") == "target-already-current", f"hole.position no-op should be blocked before launching SCDM: {hole_position_noop}", ) boss_height_zero = prepare_scdm_edit_job( step_path, output_dir=root / "boss-height-zero", backend=backend, capability_key="boss.height", target_value="0", object_id="boss:50-51-52", object_signature=boss_signature, ) _assert( boss_height_zero.get("ok") is False and boss_height_zero.get("reason") == "target-value-illegal", f"positive dimensions should reject zero before launching SCDM: {boss_height_zero}", ) round_signature = { "objectType": "round", "faceIds": [60], "bodyIndex": 0, "faceOrdinal": 60, "globalFaceOrdinal": 80, "center": [1.0, 0.0, 2.0], "axis": [0.0, 0.0, 1.0], "radius": 0.5, "roundType": "ConstantRound", "isConstantRound": True, } round_radius_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "round-radius-prepared", backend=backend, capability_key="round.radius", target_value="0.75", object_id="round:60", object_signature=round_signature, ) _assert(round_radius_prepared.get("ok") is True, f"round.radius should be productized and prepare an edit job: {round_radius_prepared}") round_radius_job = read_json(round_radius_prepared["job_path"]) _assert(round_radius_job.get("target", {}).get("backendOperation") == "change_round_radius", f"round.radius should route to change_round_radius: {round_radius_job}") _assert(round_radius_job.get("target", {}).get("value") == 0.75, f"round.radius target should be numeric: {round_radius_job}") round_radius_signature = round_radius_job.get("object", {}).get("geometrySignature", {}) _assert(round_radius_signature.get("isConstantRound") is True, f"round.radius should preserve constant round evidence: {round_radius_job}") _assert(round_radius_signature.get("radius") == 0.5, f"round.radius should preserve current radius: {round_radius_job}") round_delete_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "round-delete-prepared", backend=backend, capability_key="feature.delete_round_or_chamfer", target_value="", object_id="round:60", object_signature=round_signature, ) _assert(round_delete_prepared.get("ok") is True, f"round/chamfer delete should be productized and prepare an edit job: {round_delete_prepared}") round_delete_job = read_json(round_delete_prepared["job_path"]) _assert(round_delete_job.get("target", {}).get("backendOperation") == "delete_round_or_chamfer", f"round delete should route to delete_round_or_chamfer: {round_delete_job}") _assert(round_delete_job.get("target", {}).get("value") is True, f"command target should be true: {round_delete_job}") chamfer_signature = { "objectType": "chamfer", "faceIds": [63], "bodyIndex": 0, "faceOrdinal": 63, "globalFaceOrdinal": 83, "distance": 0.8, "distance1": 0.8, "distance2": 0.8, "chamferType": "EqualDistanceChamfer", "isEqualDistanceChamfer": True, } chamfer_distance_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "chamfer-distance-prepared", backend=backend, capability_key="chamfer.distance", target_value="1.2", object_id="chamfer:63", object_signature=chamfer_signature, ) _assert(chamfer_distance_prepared.get("ok") is True, f"chamfer.distance should be productized and prepare an edit job: {chamfer_distance_prepared}") chamfer_distance_job = read_json(chamfer_distance_prepared["job_path"]) _assert(chamfer_distance_job.get("target", {}).get("backendOperation") == "change_chamfer_distance", f"chamfer.distance should route to change_chamfer_distance: {chamfer_distance_job}") _assert(chamfer_distance_job.get("target", {}).get("value") == 1.2, f"chamfer.distance target should be numeric: {chamfer_distance_job}") chamfer_job_signature = chamfer_distance_job.get("object", {}).get("geometrySignature", {}) _assert(chamfer_job_signature.get("isEqualDistanceChamfer") is True, f"chamfer.distance should preserve equal-distance evidence: {chamfer_distance_job}") _assert(chamfer_job_signature.get("distance") == 0.8, f"chamfer.distance should preserve current distance: {chamfer_distance_job}") shell_signature = { "objectType": "thin_wall", "faceIds": [101, 102], "bodyIndex": 0, "faceOrdinals": [101, 102], "globalFaceOrdinals": [201, 202], "thickness": 1.2, "thicknessAxis": [0.0, 0.0, 1.0], "wallFaceCenters": [[0.0, 0.0, 0.0], [0.0, 0.0, 1.2]], "wallFaceLocators": [ {"bodyIndex": 0, "faceOrdinal": 101, "globalFaceOrdinal": 201}, {"bodyIndex": 0, "faceOrdinal": 102, "globalFaceOrdinal": 202}, ], } shell_thickness_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "shell-thickness-prepared", backend=backend, capability_key="shell.thickness", target_value="1.6", object_id="thin_wall:101-102", object_signature=shell_signature, ) _assert(shell_thickness_prepared.get("ok") is True, f"shell.thickness should be productized and prepare an edit job: {shell_thickness_prepared}") shell_thickness_job = read_json(shell_thickness_prepared["job_path"]) _assert(shell_thickness_job.get("target", {}).get("backendOperation") == "change_shell_thickness", f"shell.thickness should route to change_shell_thickness: {shell_thickness_job}") _assert(shell_thickness_job.get("target", {}).get("value") == 1.6, f"shell.thickness target should be numeric: {shell_thickness_job}") shell_job_signature = shell_thickness_job.get("object", {}).get("geometrySignature", {}) _assert(shell_job_signature.get("thickness") == 1.2, f"shell thickness signature should be preserved: {shell_thickness_job}") _assert(shell_job_signature.get("wallFaceLocators"), f"shell wall face locators should be preserved: {shell_thickness_job}") shell_missing_locator_signature = dict(shell_signature) shell_missing_locator_signature.pop("wallFaceLocators", None) shell_missing_locator_signature.pop("faceOrdinals", None) shell_missing_locator_signature.pop("globalFaceOrdinals", None) shell_missing_locator = prepare_scdm_edit_job( step_path, output_dir=root / "shell-thickness-missing-locator", backend=backend, capability_key="shell.thickness", target_value="1.6", object_id="thin_wall:101-102", object_signature=shell_missing_locator_signature, ) _assert( shell_missing_locator.get("ok") is False and shell_missing_locator.get("reason") == "object-signature-missing-wall-face-locator", f"shell.thickness should be blocked before SCDM when both wall locators are missing: {shell_missing_locator}", ) pattern_signature = { "objectType": "linear_pattern", "faceIds": [85, 94, 87, 96, 89, 98], "bodyIndex": 0, "axis": [1.0, 0.0, 0.0], "spacing": 5.0, "pitch": 5.0, "instanceCount": 3, "instanceCenters": [[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [10.0, 0.0, 0.0]], "patternInstances": [ { "sourceObjectId": "hole:a", "center": [0.0, 0.0, 0.0], "faceIds": [85, 94], "scdmFaceLocators": [ {"bodyIndex": 0, "faceOrdinal": 12, "globalFaceOrdinal": 85}, {"bodyIndex": 0, "faceOrdinal": 19, "globalFaceOrdinal": 94}, ], }, { "sourceObjectId": "hole:b", "center": [5.0, 0.0, 0.0], "faceIds": [87, 96], "scdmFaceLocators": [ {"bodyIndex": 0, "faceOrdinal": 22, "globalFaceOrdinal": 87}, {"bodyIndex": 0, "faceOrdinal": 29, "globalFaceOrdinal": 96}, ], }, { "sourceObjectId": "hole:c", "center": [10.0, 0.0, 0.0], "faceIds": [89, 98], "scdmFaceLocators": [ {"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 89}, {"bodyIndex": 0, "faceOrdinal": 39, "globalFaceOrdinal": 98}, ], }, ], } pattern_spacing_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "pattern-spacing-prepared", backend=backend, capability_key="pattern.spacing", target_value="7.5", object_id="pattern:holes-a-b-c", object_signature=pattern_signature, ) _assert(pattern_spacing_prepared.get("ok") is True, f"pattern.spacing should be productized and prepare an edit job: {pattern_spacing_prepared}") pattern_spacing_job = read_json(pattern_spacing_prepared["job_path"]) _assert(pattern_spacing_job.get("target", {}).get("backendOperation") == "change_pattern_spacing", f"pattern.spacing should route to change_pattern_spacing: {pattern_spacing_job}") _assert(pattern_spacing_job.get("target", {}).get("value") == 7.5, f"pattern.spacing target should be numeric: {pattern_spacing_job}") pattern_job_signature = pattern_spacing_job.get("object", {}).get("geometrySignature", {}) _assert(pattern_job_signature.get("spacing") == 5.0, f"pattern spacing signature should be preserved: {pattern_spacing_job}") _assert(len(pattern_job_signature.get("patternInstances") or []) == 3, f"pattern instance locators should be preserved: {pattern_spacing_job}") pattern_segment_signature = dict(pattern_signature) pattern_segment_signature["segmentIndex"] = 1 pattern_segment_signature["movingSide"] = "after" pattern_segment_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "pattern-segment-spacing-prepared", backend=backend, capability_key="pattern.segment_spacing", target_value="6.5", object_id="pattern:holes-a-b-c", object_signature=pattern_segment_signature, ) _assert(pattern_segment_prepared.get("ok") is True, f"pattern.segment_spacing should prepare an edit job: {pattern_segment_prepared}") pattern_segment_job = read_json(pattern_segment_prepared["job_path"]) _assert( pattern_segment_job.get("target", {}).get("backendOperation") == "change_pattern_segment_spacing", f"pattern.segment_spacing should route to change_pattern_segment_spacing: {pattern_segment_job}", ) _assert(pattern_segment_job.get("target", {}).get("value") == 6.5, f"pattern.segment_spacing target should be numeric: {pattern_segment_job}") _assert( pattern_segment_job.get("object", {}).get("geometrySignature", {}).get("segmentIndex") == 1, f"pattern.segment_spacing should preserve the selected adjacent segment: {pattern_segment_job}", ) pattern_segment_before_signature = dict(pattern_segment_signature) pattern_segment_before_signature["movingSide"] = "before" pattern_segment_before_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "pattern-segment-spacing-before-prepared", backend=backend, capability_key="pattern.segment_spacing", target_value="6.5", object_id="pattern:holes-a-b-c", object_signature=pattern_segment_before_signature, ) _assert( pattern_segment_before_prepared.get("ok") is True and read_json(pattern_segment_before_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "before", f"pattern.segment_spacing should preserve fix-right/move-left semantics: {pattern_segment_before_prepared}", ) pattern_segment_split_signature = dict(pattern_segment_signature) pattern_segment_split_signature["movingSide"] = "split" pattern_segment_split_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "pattern-segment-spacing-split-prepared", backend=backend, capability_key="pattern.segment_spacing", target_value="6.5", object_id="pattern:holes-a-b-c", object_signature=pattern_segment_split_signature, ) _assert( pattern_segment_split_prepared.get("ok") is True and read_json(pattern_segment_split_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "split", f"pattern.segment_spacing should preserve split/keep-center semantics: {pattern_segment_split_prepared}", ) pattern_segment_single_left_signature = dict(pattern_segment_signature) pattern_segment_single_left_signature["movingSide"] = "single_left" pattern_segment_single_left_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "pattern-segment-spacing-single-left-prepared", backend=backend, capability_key="pattern.segment_spacing", target_value="6.5", object_id="pattern:holes-a-b-c", object_signature=pattern_segment_single_left_signature, ) _assert( pattern_segment_single_left_prepared.get("ok") is True and read_json(pattern_segment_single_left_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "single_left", f"pattern.segment_spacing should preserve move-only-left-instance semantics: {pattern_segment_single_left_prepared}", ) pattern_segment_single_right_signature = dict(pattern_segment_signature) pattern_segment_single_right_signature["movingSide"] = "single_right" pattern_segment_single_right_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "pattern-segment-spacing-single-right-prepared", backend=backend, capability_key="pattern.segment_spacing", target_value="6.5", object_id="pattern:holes-a-b-c", object_signature=pattern_segment_single_right_signature, ) _assert( pattern_segment_single_right_prepared.get("ok") is True and read_json(pattern_segment_single_right_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "single_right", f"pattern.segment_spacing should preserve move-only-right-instance semantics: {pattern_segment_single_right_prepared}", ) pattern_instance_signature = dict(pattern_signature["patternInstances"][1]) pattern_instance_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "pattern-instance-position-prepared", backend=backend, capability_key="pattern.instance_position", target_value=[6.0, 0.0, 0.0], object_id="pattern:holes-a-b-c:instance-b", object_signature=pattern_instance_signature, ) _assert(pattern_instance_prepared.get("ok") is True, f"pattern.instance_position should prepare an edit job when instance locators exist: {pattern_instance_prepared}") pattern_instance_job = read_json(pattern_instance_prepared["job_path"]) _assert(pattern_instance_job.get("target", {}).get("backendOperation") == "move_pattern_instance", f"pattern.instance_position should route to move_pattern_instance: {pattern_instance_job}") _assert(pattern_instance_job.get("target", {}).get("value") == [6.0, 0.0, 0.0], f"pattern.instance_position target should be vector3: {pattern_instance_job}") pattern_instance_missing_locator = dict(pattern_instance_signature) pattern_instance_missing_locator.pop("scdmFaceLocators", None) pattern_instance_missing_locator.pop("faceOrdinals", None) pattern_instance_missing_locator.pop("globalFaceOrdinals", None) pattern_instance_missing_locator.pop("bodyLocators", None) pattern_instance_missing_locator.pop("componentLocators", None) pattern_instance_missing_locator.pop("bodyIndex", None) blocked_pattern_instance = prepare_scdm_edit_job( step_path, output_dir=root / "pattern-instance-position-missing-locator", backend=backend, capability_key="pattern.instance_position", target_value=[6.0, 0.0, 0.0], object_id="pattern:holes-a-b-c:instance-b", object_signature=pattern_instance_missing_locator, ) _assert( blocked_pattern_instance.get("ok") is False and blocked_pattern_instance.get("reason") == "object-signature-missing-pattern-instance-locator", f"pattern.instance_position should be blocked before SCDM when instance locator is missing: {blocked_pattern_instance}", ) body_pattern_signature = { "objectType": "linear_pattern", "patternKind": "body", "instanceKind": "body", "faceIds": [300, 301, 302], "bodyIndices": [20, 21, 22], "axis": [0.0, 1.0, 0.0], "spacing": 5.0, "pitch": 5.0, "instanceCount": 3, "instanceCenters": [[0.0, 20.0, 0.0], [0.0, 25.0, 0.0], [0.0, 30.0, 0.0]], "patternInstances": [ {"sourceObjectId": "body:20", "instanceKind": "body", "center": [0.0, 20.0, 0.0], "bodyIndex": 20, "bodyLocators": [{"bodyIndex": 20}], "faceIds": [300]}, {"sourceObjectId": "body:21", "instanceKind": "body", "center": [0.0, 25.0, 0.0], "bodyIndex": 21, "bodyLocators": [{"bodyIndex": 21}], "faceIds": [301]}, {"sourceObjectId": "body:22", "instanceKind": "body", "center": [0.0, 30.0, 0.0], "bodyIndex": 22, "bodyLocators": [{"bodyIndex": 22}], "faceIds": [302]}, ], } body_pattern_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "body-pattern-spacing-prepared", backend=backend, capability_key="pattern.spacing", target_value="8", object_id="pattern:parts-20-22", object_signature=body_pattern_signature, ) _assert( body_pattern_prepared.get("ok") is False and body_pattern_prepared.get("reason") == "body-pattern-spacing-missing-component-locators", f"body pattern spacing should be blocked until component occurrence locators are available: {body_pattern_prepared}", ) body_segment_pattern_signature = dict(body_pattern_signature) body_segment_pattern_signature["segmentIndex"] = 0 body_segment_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "body-pattern-segment-spacing-prepared", backend=backend, capability_key="pattern.segment_spacing", target_value="6", object_id="pattern:parts-20-22", object_signature=body_segment_pattern_signature, ) _assert( body_segment_prepared.get("ok") is False and body_segment_prepared.get("reason") == "body-pattern-spacing-missing-component-locators", f"body pattern local segment spacing should also require component occurrence locators: {body_segment_prepared}", ) component_body_pattern_signature = dict(body_pattern_signature) component_body_pattern_signature["componentInstanceCount"] = 3 component_body_pattern_signature["patternInstances"] = [ { **dict(item), "componentLocators": [ { "componentIndex": index, "componentPath": [index], "componentBodyIndex": 0, "bodyIndex": item.get("bodyIndex"), "componentName": f"Part {index + 1}", } ], } for index, item in enumerate(body_pattern_signature["patternInstances"]) ] component_body_pattern_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "component-body-pattern-spacing-prepared", backend=backend, capability_key="pattern.spacing", target_value="8", object_id="pattern:components-20-22", object_signature=component_body_pattern_signature, ) _assert( component_body_pattern_prepared.get("ok") is True, f"body pattern spacing should prepare when every member has a component occurrence locator: {component_body_pattern_prepared}", ) component_body_pattern_job = read_json(component_body_pattern_prepared["job_path"]) component_instances = component_body_pattern_job.get("object", {}).get("geometrySignature", {}).get("patternInstances") or [] _assert( all(item.get("componentLocators") for item in component_instances if isinstance(item, dict)), f"component occurrence locators should be preserved in pattern.spacing job: {component_body_pattern_job}", ) component_body_segment_signature = dict(component_body_pattern_signature) component_body_segment_signature["segmentIndex"] = 0 component_body_segment_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "component-body-pattern-segment-spacing-prepared", backend=backend, capability_key="pattern.segment_spacing", target_value="6", object_id="pattern:components-20-22", object_signature=component_body_segment_signature, ) _assert( component_body_segment_prepared.get("ok") is True, f"body pattern local segment spacing should prepare with component occurrence locators: {component_body_segment_prepared}", ) blocked_pattern_signature = dict(pattern_signature) blocked_pattern_signature["supportPatternFit"] = { "supportFaceIds": [92], "localUnitScale": 0.001, "maxSpacing": 0.0012, "maxSpacingLocal": 1.2, "instanceCount": 3, } blocked_pattern_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "body-pattern-spacing-blocked", backend=backend, capability_key="pattern.spacing", target_value=0.002, object_id="pattern:holes-a-b-c", object_signature=blocked_pattern_signature, ) _assert( blocked_pattern_prepared.get("ok") is False and blocked_pattern_prepared.get("reason") == "pattern-spacing-exceeds-support", f"pattern spacing should be blocked before SCDM when it exceeds support Face limits: {blocked_pattern_prepared}", ) blocked_segment_signature = dict(pattern_segment_signature) blocked_segment_signature["supportPatternFit"] = { "supportFaceIds": [92], "localUnitScale": 0.001, "maxSegmentSpacing": 0.0013, "maxSegmentSpacingLocal": 1.3, "instanceCount": 3, } blocked_segment_prepared = prepare_scdm_edit_job( step_path, output_dir=root / "pattern-segment-spacing-blocked", backend=backend, capability_key="pattern.segment_spacing", target_value=0.002, object_id="pattern:holes-a-b-c", object_signature=blocked_segment_signature, ) _assert( blocked_segment_prepared.get("ok") is False and blocked_segment_prepared.get("reason") == "pattern-spacing-exceeds-support", f"pattern segment spacing should be blocked before SCDM when it exceeds support Face limits: {blocked_segment_prepared}", ) unsupported = prepare_scdm_edit_job( step_path, output_dir=root / "unsupported-unknown", backend=backend, capability_key="not.real", target_value="1", object_signature=signature, ) _assert(unsupported.get("ok") is False and unsupported.get("reason") == "unsupported-capability", f"unknown capability should fail early: {unsupported}") success = run_scdm_edit_job( step_path, output_dir=root / "success", backend=backend, capability_key="hole.diameter", target_value=0.9, object_id="hole:85-94", object_signature=signature, runner=_successful_runner, ) _assert(success.get("ok") is True, f"fake edit should succeed: {success}") _assert(Path(str(success["output_step"])).is_file(), f"output STEP should exist: {success}") success_backend = success.get("backend") _assert(isinstance(success_backend, dict), f"successful edit should carry backend status: {success}") _assert(success_backend.get("runScriptOk") is True, f"successful edit should mark /RunScript usable: {success_backend}") _assert(success_backend.get("licenseOk") is True, f"successful edit should mark license usable: {success_backend}") failed = run_scdm_edit_job( step_path, output_dir=root / "failed", backend=backend, capability_key="hole.position", target_value=[0.5, 1.0, 6.0], object_id="hole:85-94", object_signature=signature, runner=_failure_runner, ) _assert(failed.get("ok") is False and failed.get("reason") == "fake-failed", f"error.json should drive failure reason: {failed}") slot_success = run_scdm_edit_job( step_path, output_dir=root / "slot-success", backend=backend, capability_key="slot.position", target_value=[1.0, 2.0, 6.0], object_id="slot:30-31-32", object_signature=slot_signature, runner=_successful_runner, ) _assert(slot_success.get("ok") is True, f"fake slot.position edit should succeed: {slot_success}") _assert(slot_success.get("backend_operation") == "move_slot", f"slot.position should report move_slot: {slot_success}") slot_width_success = run_scdm_edit_job( step_path, output_dir=root / "slot-width-success", backend=backend, capability_key="slot.width", target_value=2.5, object_id="slot:30-31-32", object_signature=slot_signature, runner=_successful_runner, ) _assert(slot_width_success.get("ok") is True, f"fake slot.width edit should succeed: {slot_width_success}") _assert(slot_width_success.get("backend_operation") == "change_slot_width", f"slot.width should report change_slot_width: {slot_width_success}") slot_depth_success = run_scdm_edit_job( step_path, output_dir=root / "slot-depth-success", backend=backend, capability_key="slot.depth", target_value=2.0, object_id="slot:30-31-32", object_signature=slot_signature, runner=_successful_runner, ) _assert(slot_depth_success.get("ok") is True, f"fake slot.depth edit should succeed: {slot_depth_success}") _assert(slot_depth_success.get("backend_operation") == "change_slot_depth", f"slot.depth should report change_slot_depth: {slot_depth_success}") boss_success = run_scdm_edit_job( step_path, output_dir=root / "boss-success", backend=backend, capability_key="boss.position", target_value=[3.0, 0.0, 2.0], object_id="boss:50-51-52", object_signature=boss_signature, runner=_successful_runner, ) _assert(boss_success.get("ok") is True, f"fake boss.position edit should succeed: {boss_success}") _assert(boss_success.get("backend_operation") == "move_boss", f"boss.position should report move_boss: {boss_success}") boss_height_success = run_scdm_edit_job( step_path, output_dir=root / "boss-height-success", backend=backend, capability_key="boss.height", target_value=5.5, object_id="boss:50-51-52", object_signature=boss_signature, runner=_successful_runner, ) _assert(boss_height_success.get("ok") is True, f"fake boss.height edit should succeed: {boss_height_success}") _assert(boss_height_success.get("backend_operation") == "change_boss_height", f"boss.height should report change_boss_height: {boss_height_success}") boss_diameter_success = run_scdm_edit_job( step_path, output_dir=root / "boss-diameter-success", backend=backend, capability_key="boss.diameter", target_value=4.5, object_id="boss:50-51-52", object_signature=boss_signature, runner=_successful_runner, ) _assert(boss_diameter_success.get("ok") is True, f"fake boss.diameter edit should succeed: {boss_diameter_success}") _assert(boss_diameter_success.get("backend_operation") == "change_boss_diameter", f"boss.diameter should report change_boss_diameter: {boss_diameter_success}") round_radius_success = run_scdm_edit_job( step_path, output_dir=root / "round-radius-success", backend=backend, capability_key="round.radius", target_value=0.75, object_id="round:60", object_signature=round_signature, runner=_successful_runner, ) _assert(round_radius_success.get("ok") is True, f"fake round.radius edit should succeed: {round_radius_success}") _assert(round_radius_success.get("backend_operation") == "change_round_radius", f"round.radius should report change_round_radius: {round_radius_success}") round_delete_success = run_scdm_edit_job( step_path, output_dir=root / "round-delete-success", backend=backend, capability_key="feature.delete_round_or_chamfer", target_value="", object_id="round:60", object_signature=round_signature, runner=_successful_runner, ) _assert(round_delete_success.get("ok") is True, f"fake round/chamfer delete edit should succeed: {round_delete_success}") _assert(round_delete_success.get("backend_operation") == "delete_round_or_chamfer", f"round/chamfer delete should report delete_round_or_chamfer: {round_delete_success}") chamfer_distance_success = run_scdm_edit_job( step_path, output_dir=root / "chamfer-distance-success", backend=backend, capability_key="chamfer.distance", target_value=1.2, object_id="chamfer:63", object_signature=chamfer_signature, runner=_successful_runner, ) _assert(chamfer_distance_success.get("ok") is True, f"fake chamfer.distance edit should succeed: {chamfer_distance_success}") _assert(chamfer_distance_success.get("backend_operation") == "change_chamfer_distance", f"chamfer.distance should report change_chamfer_distance: {chamfer_distance_success}") shell_thickness_success = run_scdm_edit_job( step_path, output_dir=root / "shell-thickness-success", backend=backend, capability_key="shell.thickness", target_value=1.6, object_id="thin_wall:101-102", object_signature=shell_signature, runner=_successful_runner, ) _assert(shell_thickness_success.get("ok") is True, f"fake shell.thickness edit should succeed: {shell_thickness_success}") _assert(shell_thickness_success.get("backend_operation") == "change_shell_thickness", f"shell.thickness should report change_shell_thickness: {shell_thickness_success}") pattern_spacing_success = run_scdm_edit_job( step_path, output_dir=root / "pattern-spacing-success", backend=backend, capability_key="pattern.spacing", target_value=7.5, object_id="pattern:holes-a-b-c", object_signature=pattern_signature, runner=_successful_runner, ) _assert(pattern_spacing_success.get("ok") is True, f"fake pattern.spacing edit should succeed: {pattern_spacing_success}") _assert(pattern_spacing_success.get("backend_operation") == "change_pattern_spacing", f"pattern.spacing should report change_pattern_spacing: {pattern_spacing_success}") pattern_segment_success = run_scdm_edit_job( step_path, output_dir=root / "pattern-segment-spacing-success", backend=backend, capability_key="pattern.segment_spacing", target_value=6.5, object_id="pattern:holes-a-b-c", object_signature=pattern_segment_signature, runner=_successful_runner, ) _assert(pattern_segment_success.get("ok") is True, f"fake pattern.segment_spacing edit should succeed: {pattern_segment_success}") _assert( pattern_segment_success.get("backend_operation") == "change_pattern_segment_spacing", f"pattern.segment_spacing should report change_pattern_segment_spacing: {pattern_segment_success}", ) pattern_instance_success = run_scdm_edit_job( step_path, output_dir=root / "pattern-instance-position-success", backend=backend, capability_key="pattern.instance_position", target_value=[6.0, 0.0, 0.0], object_id="pattern:holes-a-b-c:instance-b", object_signature=pattern_instance_signature, runner=_successful_runner, ) _assert(pattern_instance_success.get("ok") is True, f"fake pattern.instance_position edit should succeed: {pattern_instance_success}") _assert(pattern_instance_success.get("backend_operation") == "move_pattern_instance", f"pattern.instance_position should report move_pattern_instance: {pattern_instance_success}") missing_output = run_scdm_edit_job( step_path, output_dir=root / "missing-output", backend=backend, capability_key="face.offset", target_value=5, object_id="face:9", object_signature={"objectType": "face", "faceIds": [9], "bodyIndex": 0, "faceOrdinal": 3}, runner=_missing_output_runner, ) _assert(missing_output.get("ok") is False and missing_output.get("reason") == "missing-output-step", f"missing output STEP should be rejected: {missing_output}") empty_output = run_scdm_edit_job( step_path, output_dir=root / "empty-output", backend=backend, capability_key="face.offset", target_value=5, object_id="face:9", object_signature={"objectType": "face", "faceIds": [9], "bodyIndex": 0, "faceOrdinal": 3}, runner=_empty_output_runner, ) _assert(empty_output.get("ok") is False and empty_output.get("reason") == "empty-output-step", f"empty output STEP should be rejected: {empty_output}") print("scdm edit runner ok") return 0 if __name__ == "__main__": raise SystemExit(main())