from __future__ import annotations 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_result_validator import ( # noqa: E402 build_scdm_id_mapping, check_scdm_summary_delta, check_scdm_unedited_objects, check_scdm_target, match_scdm_object_by_signature, rewrite_scdm_relation_formula_ids, validate_scdm_edit_result, ) def _assert(condition: bool, message: str) -> None: if not condition: raise AssertionError(message) def _hole(object_id: str, face_id: int, *, diameter: float, center: tuple[float, float, float]) -> dict[str, object]: return { "objectId": object_id, "objectType": "hole", "geometrySignature": { "objectType": "hole", "faceIds": [face_id], "surfaceType": "cylinder", "center": list(center), "axis": [0.0, 0.0, 1.0], "diameter": diameter, }, "capabilities": [ {"key": "hole.diameter", "currentValue": diameter}, {"key": "hole.position", "currentValue": list(center)}, ], } def _feature( object_id: str, object_type: str, face_id: int, *, center: tuple[float, float, float], capability_key: str, ) -> dict[str, object]: return { "objectId": object_id, "objectType": object_type, "geometrySignature": { "objectType": object_type, "faceIds": [face_id], "center": list(center), "axis": [0.0, 0.0, 1.0], }, "capabilities": [ {"key": capability_key, "currentValue": list(center)}, ], } def _cache(*objects: dict[str, object]) -> dict[str, object]: return { "schemaVersion": 1, "source": "SCDM", "objects": list(objects), "diagnostics": {}, } def _cache_with_summary(summary: dict[str, int], *objects: dict[str, object]) -> dict[str, object]: cache = _cache(*objects) cache["diagnostics"] = {"raw_summary": dict(summary)} return cache def main() -> int: with tempfile.TemporaryDirectory(prefix="step_editor_scdm_validate_") as temp: root = Path(temp) output_step = root / "result.step" output_step.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8") before = _cache( _hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)), _hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5)), ) after = _cache( _hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5)), _hole("hole:91", 91, diameter=0.5, center=(2.0, 1.0, 9.5)), ) before_signature = before["objects"][0]["geometrySignature"] # type: ignore[index] match = match_scdm_object_by_signature(before_signature, after, capability_key="hole.diameter") _assert(match.get("status") == "unique", f"changed diameter should still match by center/axis/type: {match}") _assert(match.get("object", {}).get("objectId") == "hole:90", f"wrong match: {match}") ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=before_signature, before_cache=before, after_cache=after, capability_key="hole.diameter", expected_target=0.75, edited_object_id="hole:85", brep_validator=lambda path: {"ok": path.is_file(), "reason": "ok"}, ) _assert(ok.get("ok") is True, f"validated edit should pass: {ok}") _assert(ok.get("targetCheck", {}).get("ok") is True, f"target diameter should be checked: {ok}") _assert(ok.get("topologyCheck", {}).get("ok") is True, f"unchanged objects should be checked: {ok}") drift_after = _cache(_hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5))) drift = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=before_signature, before_cache=before, after_cache=drift_after, capability_key="hole.diameter", expected_target=0.75, edited_object_id="hole:85", ) _assert(drift.get("ok") is False and drift.get("reason") == "unexpected-object-drift", f"missing unrelated hole should fail: {drift}") direct_drift = check_scdm_unedited_objects(before, drift_after, edited_object_id="hole:85", edited_signature=before_signature) _assert(direct_drift.get("ok") is False and direct_drift.get("checked") == 1, f"direct drift check should inspect one unedited object: {direct_drift}") mismatch = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=before_signature, after_cache=after, capability_key="hole.diameter", expected_target=0.9, ) _assert(mismatch.get("ok") is False and mismatch.get("reason") == "target-mismatch", f"wrong target should fail: {mismatch}") missing = validate_scdm_edit_result({"ok": True, "output_step": str(root / "missing.step")}) _assert(missing.get("ok") is False and missing.get("reason") == "missing-output-step", f"missing result STEP should fail: {missing}") mapping = build_scdm_id_mapping(before, after, capability_key="hole.diameter") _assert(mapping.get("faceIdMap") == {85: 90, 87: 91}, f"face IDs should remap through signatures: {mapping}") rewritten = rewrite_scdm_relation_formula_ids("Face87.直径 = Face85.半径", mapping) _assert(rewritten == "Face91.直径 = Face90.半径", f"formula IDs should follow SCDM remap: {rewritten}") position_after = _cache(_hole("hole:91", 91, diameter=0.5, center=(2.0, 1.0, 6.0))) position_check = check_scdm_target(position_after["objects"][0], capability_key="hole.position", expected_target=[2.0, 1.0, 6.0]) _assert(position_check.get("ok") is True, f"position target should pass: {position_check}") before_slot = _cache(_feature("slot:30", "slot", 30, center=(1.0, 2.0, 3.0), capability_key="slot.position")) after_slot = _cache(_feature("slot:40", "slot", 40, center=(1.0, 2.0, 6.0), capability_key="slot.position")) slot_before_signature = before_slot["objects"][0]["geometrySignature"] # type: ignore[index] slot_match = match_scdm_object_by_signature(slot_before_signature, after_slot, capability_key="slot.position") _assert(slot_match.get("status") == "unique", f"moved slot should match without old center lock: {slot_match}") slot_ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=slot_before_signature, before_cache=before_slot, after_cache=after_slot, capability_key="slot.position", expected_target=[1.0, 2.0, 6.0], edited_object_id="slot:30", ) _assert(slot_ok.get("ok") is True, f"slot.position result should validate target center: {slot_ok}") _assert(slot_ok.get("targetCheck", {}).get("ok") is True, f"slot target should be checked: {slot_ok}") before_slot_width = _cache( { "objectId": "slot:30", "objectType": "slot", "geometrySignature": { "objectType": "slot", "faceIds": [30], "center": [1.0, 2.0, 3.0], "axis": [0.0, 0.0, 1.0], "width": 2.0, }, "capabilities": [{"key": "slot.width", "currentValue": 2.0}], } ) after_slot_width = _cache( { "objectId": "slot:40", "objectType": "slot", "geometrySignature": { "objectType": "slot", "faceIds": [40], "center": [1.0, 2.0, 3.0], "axis": [0.0, 0.0, 1.0], "width": 2.5, }, "capabilities": [{"key": "slot.width", "currentValue": 2.5}], } ) slot_width_before_signature = before_slot_width["objects"][0]["geometrySignature"] # type: ignore[index] slot_width_ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=slot_width_before_signature, before_cache=before_slot_width, after_cache=after_slot_width, capability_key="slot.width", expected_target=2.5, edited_object_id="slot:30", ) _assert(slot_width_ok.get("ok") is True, f"slot.width result should validate target width: {slot_width_ok}") slot_width_mismatch = check_scdm_target(after_slot_width["objects"][0], capability_key="slot.width", expected_target=2.1) # type: ignore[index] _assert(slot_width_mismatch.get("ok") is False and slot_width_mismatch.get("reason") == "target-mismatch", f"slot.width mismatch should fail: {slot_width_mismatch}") before_slot_depth = _cache( { "objectId": "slot:31", "objectType": "slot", "geometrySignature": { "objectType": "slot", "faceIds": [31], "center": [1.0, 2.0, 3.0], "depth": 1.5, "depthAxis": [0.0, 0.0, -1.0], }, "capabilities": [{"key": "slot.depth", "currentValue": 1.5}], } ) after_slot_depth = _cache( { "objectId": "slot:41", "objectType": "slot", "geometrySignature": { "objectType": "slot", "faceIds": [41], "center": [1.0, 2.0, 3.0], "depth": 2.0, "depthAxis": [0.0, 0.0, -1.0], }, "capabilities": [{"key": "slot.depth", "currentValue": 2.0}], } ) slot_depth_before_signature = before_slot_depth["objects"][0]["geometrySignature"] # type: ignore[index] slot_depth_ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=slot_depth_before_signature, before_cache=before_slot_depth, after_cache=after_slot_depth, capability_key="slot.depth", expected_target=2.0, edited_object_id="slot:31", ) _assert(slot_depth_ok.get("ok") is True, f"slot.depth result should validate target depth: {slot_depth_ok}") slot_depth_mismatch = check_scdm_target(after_slot_depth["objects"][0], capability_key="slot.depth", expected_target=1.5) # type: ignore[index] _assert(slot_depth_mismatch.get("ok") is False and slot_depth_mismatch.get("reason") == "target-mismatch", f"slot.depth mismatch should fail: {slot_depth_mismatch}") boss_after = _feature("boss:50", "cylindrical_boss", 50, center=(3.0, 0.0, 2.0), capability_key="boss.position") boss_check = check_scdm_target(boss_after, capability_key="boss.position", expected_target=[3.0, 0.0, 2.0]) _assert(boss_check.get("ok") is True, f"boss.position target should pass: {boss_check}") boss_mismatch = check_scdm_target(boss_after, capability_key="boss.position", expected_target=[4.0, 0.0, 2.0]) _assert(boss_mismatch.get("ok") is False and boss_mismatch.get("reason") == "target-mismatch", f"boss.position mismatch should fail: {boss_mismatch}") before_boss_height = _cache( { "objectId": "boss:50", "objectType": "cylindrical_boss", "geometrySignature": { "objectType": "cylindrical_boss", "faceIds": [50, 51, 52], "center": [0.0, 0.0, 2.0], "axis": [0.0, 0.0, 1.0], "diameter": 3.0, "height": 4.0, }, "capabilities": [{"key": "boss.height", "currentValue": 4.0}], } ) after_boss_height = _cache( { "objectId": "boss:55", "objectType": "cylindrical_boss", "geometrySignature": { "objectType": "cylindrical_boss", "faceIds": [55, 56, 57], "center": [0.0, 0.0, 2.75], "axis": [0.0, 0.0, 1.0], "diameter": 3.0, "height": 5.5, }, "capabilities": [{"key": "boss.height", "currentValue": 5.5}], } ) boss_height_signature = before_boss_height["objects"][0]["geometrySignature"] # type: ignore[index] boss_height_ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=boss_height_signature, before_cache=before_boss_height, after_cache=after_boss_height, capability_key="boss.height", expected_target=5.5, edited_object_id="boss:50", ) _assert(boss_height_ok.get("ok") is True, f"boss.height result should validate target height: {boss_height_ok}") boss_height_mismatch = check_scdm_target(after_boss_height["objects"][0], capability_key="boss.height", expected_target=4.5) # type: ignore[index] _assert(boss_height_mismatch.get("ok") is False and boss_height_mismatch.get("reason") == "target-mismatch", f"boss.height mismatch should fail: {boss_height_mismatch}") before_boss_diameter = _cache( { "objectId": "boss:60", "objectType": "cylindrical_boss", "geometrySignature": { "objectType": "cylindrical_boss", "faceIds": [60, 61, 62], "center": [0.0, 0.0, 2.0], "axis": [0.0, 0.0, 1.0], "diameter": 3.0, "height": 4.0, }, "capabilities": [{"key": "boss.diameter", "currentValue": 3.0}], } ) after_boss_diameter = _cache( { "objectId": "boss:63", "objectType": "cylindrical_boss", "geometrySignature": { "objectType": "cylindrical_boss", "faceIds": [63, 64, 65], "center": [0.0, 0.0, 2.0], "axis": [0.0, 0.0, 1.0], "diameter": 4.5, "height": 4.0, }, "capabilities": [{"key": "boss.diameter", "currentValue": 4.5}], } ) boss_diameter_signature = before_boss_diameter["objects"][0]["geometrySignature"] # type: ignore[index] boss_diameter_ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=boss_diameter_signature, before_cache=before_boss_diameter, after_cache=after_boss_diameter, capability_key="boss.diameter", expected_target=4.5, edited_object_id="boss:60", ) _assert(boss_diameter_ok.get("ok") is True, f"boss.diameter result should validate target diameter: {boss_diameter_ok}") boss_diameter_mismatch = check_scdm_target(after_boss_diameter["objects"][0], capability_key="boss.diameter", expected_target=3.5) # type: ignore[index] _assert(boss_diameter_mismatch.get("ok") is False and boss_diameter_mismatch.get("reason") == "target-mismatch", f"boss.diameter mismatch should fail: {boss_diameter_mismatch}") before_round_radius = _cache( { "objectId": "round:70", "objectType": "round", "geometrySignature": { "objectType": "round", "faceIds": [70], "center": [1.0, 0.0, 2.0], "axis": [0.0, 0.0, 1.0], "radius": 0.5, "isConstantRound": True, }, "capabilities": [{"key": "round.radius", "currentValue": 0.5}], } ) after_round_radius = _cache( { "objectId": "round:71", "objectType": "round", "geometrySignature": { "objectType": "round", "faceIds": [71], "center": [1.0, 0.0, 2.0], "axis": [0.0, 0.0, 1.0], "radius": 0.75, "isConstantRound": True, }, "capabilities": [{"key": "round.radius", "currentValue": 0.75}], } ) round_radius_signature = before_round_radius["objects"][0]["geometrySignature"] # type: ignore[index] round_radius_ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=round_radius_signature, before_cache=before_round_radius, after_cache=after_round_radius, capability_key="round.radius", expected_target=0.75, edited_object_id="round:70", ) _assert(round_radius_ok.get("ok") is True, f"round.radius result should validate target radius: {round_radius_ok}") round_radius_mismatch = check_scdm_target(after_round_radius["objects"][0], capability_key="round.radius", expected_target=0.5) # type: ignore[index] _assert(round_radius_mismatch.get("ok") is False and round_radius_mismatch.get("reason") == "target-mismatch", f"round.radius mismatch should fail: {round_radius_mismatch}") before_chamfer_distance = _cache( { "objectId": "chamfer:80", "objectType": "chamfer", "geometrySignature": { "objectType": "chamfer", "faceIds": [80], "center": [2.0, 0.0, 2.0], "axis": [0.0, 0.0, 1.0], "distance": 0.8, "isEqualDistanceChamfer": True, }, "capabilities": [{"key": "chamfer.distance", "currentValue": 0.8}], } ) after_chamfer_distance = _cache( { "objectId": "chamfer:81", "objectType": "chamfer", "geometrySignature": { "objectType": "chamfer", "faceIds": [81], "center": [2.0, 0.0, 2.0], "axis": [0.0, 0.0, 1.0], "distance": 1.2, "isEqualDistanceChamfer": True, }, "capabilities": [{"key": "chamfer.distance", "currentValue": 1.2}], } ) chamfer_distance_signature = before_chamfer_distance["objects"][0]["geometrySignature"] # type: ignore[index] chamfer_distance_ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=chamfer_distance_signature, before_cache=before_chamfer_distance, after_cache=after_chamfer_distance, capability_key="chamfer.distance", expected_target=1.2, edited_object_id="chamfer:80", ) _assert(chamfer_distance_ok.get("ok") is True, f"chamfer.distance result should validate target distance: {chamfer_distance_ok}") chamfer_distance_mismatch = check_scdm_target(after_chamfer_distance["objects"][0], capability_key="chamfer.distance", expected_target=0.8) # type: ignore[index] _assert(chamfer_distance_mismatch.get("ok") is False and chamfer_distance_mismatch.get("reason") == "target-mismatch", f"chamfer.distance mismatch should fail: {chamfer_distance_mismatch}") before_pattern_spacing = _cache( { "objectId": "pattern:holes", "objectType": "linear_pattern", "geometrySignature": { "objectType": "linear_pattern", "faceIds": [85, 87, 89], "center": [5.0, 0.0, 0.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]], }, "capabilities": [{"key": "pattern.spacing", "currentValue": 5.0}], } ) after_pattern_spacing = _cache( { "objectId": "pattern:holes-new", "objectType": "linear_pattern", "geometrySignature": { "objectType": "linear_pattern", "faceIds": [90, 91, 92], "center": [7.5, 0.0, 0.0], "axis": [1.0, 0.0, 0.0], "spacing": 7.5, "pitch": 7.5, "instanceCount": 3, "instanceCenters": [[0.0, 0.0, 0.0], [7.5, 0.0, 0.0], [15.0, 0.0, 0.0]], }, "capabilities": [{"key": "pattern.spacing", "currentValue": 7.5}], } ) pattern_spacing_before_signature = before_pattern_spacing["objects"][0]["geometrySignature"] # type: ignore[index] pattern_spacing_ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=pattern_spacing_before_signature, before_cache=before_pattern_spacing, after_cache=after_pattern_spacing, capability_key="pattern.spacing", expected_target=7.5, edited_object_id="pattern:holes", ) _assert(pattern_spacing_ok.get("ok") is True, f"pattern.spacing result should validate target spacing: {pattern_spacing_ok}") pattern_spacing_mismatch = check_scdm_target(after_pattern_spacing["objects"][0], capability_key="pattern.spacing", expected_target=5.0) # type: ignore[index] _assert(pattern_spacing_mismatch.get("ok") is False and pattern_spacing_mismatch.get("reason") == "target-mismatch", f"pattern.spacing mismatch should fail: {pattern_spacing_mismatch}") before_pattern_segment = _cache( { "objectId": "pattern:holes", "objectType": "linear_pattern", "geometrySignature": { "objectType": "linear_pattern", "faceIds": [85, 87, 89], "center": [5.0, 0.0, 0.0], "axis": [1.0, 0.0, 0.0], "spacing": 5.0, "pitch": 5.0, "segmentIndex": 1, "movingSide": "after", "patternInstances": [ {"sourceObjectId": "a", "center": [0.0, 0.0, 0.0], "faceIds": [85]}, {"sourceObjectId": "b", "center": [5.0, 0.0, 0.0], "faceIds": [87]}, {"sourceObjectId": "c", "center": [10.0, 0.0, 0.0], "faceIds": [89]}, ], }, "capabilities": [{"key": "pattern.segment_spacing", "currentValue": 5.0}], }, _hole("hole:unrelated", 999, diameter=1.0, center=(100.0, 0.0, 0.0)), ) after_pattern_segment = _cache(_hole("hole:unrelated-new", 999, diameter=1.0, center=(100.0, 0.0, 0.0))) pattern_segment_ok = validate_scdm_edit_result( { "ok": True, "output_step": str(output_step), "result": { "applied": { "segmentSpacing": 6.5, "targetSpacing": 6.5, "segmentIndex": 1, "spacingMode": "segment_after", } }, }, before_signature=before_pattern_segment["objects"][0]["geometrySignature"], # type: ignore[index] before_cache=before_pattern_segment, after_cache=after_pattern_segment, capability_key="pattern.segment_spacing", expected_target=6.5, edited_object_id="pattern:holes", ) _assert( pattern_segment_ok.get("ok") is True and pattern_segment_ok.get("targetCheck", {}).get("spacingMode") == "segment_after", f"pattern.segment_spacing should validate from the applied edit result even when the old uniform pattern no longer matches: {pattern_segment_ok}", ) pattern_segment_mismatch = validate_scdm_edit_result( { "ok": True, "output_step": str(output_step), "applied": {"segmentSpacing": 6.0, "segmentIndex": 1, "spacingMode": "segment_after"}, }, before_signature=before_pattern_segment["objects"][0]["geometrySignature"], # type: ignore[index] before_cache=before_pattern_segment, after_cache=after_pattern_segment, capability_key="pattern.segment_spacing", expected_target=6.5, edited_object_id="pattern:holes", ) _assert( pattern_segment_mismatch.get("ok") is False and pattern_segment_mismatch.get("reason") == "target-mismatch", f"pattern.segment_spacing mismatch should fail from the applied edit result: {pattern_segment_mismatch}", ) shell_thickness_ok = check_scdm_target( { "objectType": "thin_wall", "geometrySignature": {"objectType": "thin_wall", "thickness": 1.6}, "capabilities": [{"key": "shell.thickness", "currentValue": 1.6}], }, capability_key="shell.thickness", expected_target=1.6, ) _assert(shell_thickness_ok.get("ok") is True, f"shell.thickness result should validate target thickness: {shell_thickness_ok}") shell_thickness_mismatch = check_scdm_target( { "objectType": "thin_wall", "geometrySignature": {"objectType": "thin_wall", "thickness": 1.6}, "capabilities": [{"key": "shell.thickness", "currentValue": 1.6}], }, capability_key="shell.thickness", expected_target=2.0, ) _assert(shell_thickness_mismatch.get("ok") is False and shell_thickness_mismatch.get("reason") == "target-mismatch", f"shell.thickness mismatch should fail: {shell_thickness_mismatch}") summary_before = _cache_with_summary( {"bodyCount": 13, "objectCount": 554, "faceCount": 158, "edgeCount": 396}, _hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)), ) summary_after_ok = _cache_with_summary( {"bodyCount": 13, "objectCount": 550, "faceCount": 157, "edgeCount": 390}, _hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5)), ) summary_ok = check_scdm_summary_delta(summary_before, summary_after_ok, capability_key="hole.diameter") _assert(summary_ok.get("ok") is True, f"small summary changes should pass: {summary_ok}") summary_after_bad = _cache_with_summary( {"bodyCount": 13, "objectCount": 80, "faceCount": 20, "edgeCount": 45}, _hole("hole:90", 90, diameter=0.75, center=(0.5, 1.0, 9.5)), ) summary_bad = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=summary_before["objects"][0]["geometrySignature"], # type: ignore[index] before_cache=summary_before, after_cache=summary_after_bad, capability_key="hole.diameter", expected_target=0.75, ) _assert(summary_bad.get("ok") is False and summary_bad.get("reason") == "summary-drift", f"large summary drift should fail: {summary_bad}") summary_fill = check_scdm_summary_delta(summary_before, summary_after_bad, capability_key="feature.fill") _assert(summary_fill.get("ok") is None and summary_fill.get("reason") == "skipped-command-feature", f"fill should skip summary count guard: {summary_fill}") fill_before = _cache( _hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)), _hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5)), ) fill_after = _cache(_hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5))) fill_signature = fill_before["objects"][0]["geometrySignature"] # type: ignore[index] fill_ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=fill_signature, before_cache=fill_before, after_cache=fill_after, capability_key="feature.fill", edited_object_id="hole:85", ) _assert(fill_ok.get("ok") is True, f"feature.fill should pass when the edited feature disappears: {fill_ok}") _assert(fill_ok.get("removalCheck", {}).get("ok") is True, f"feature.fill should record removal evidence: {fill_ok}") fill_still_present = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=fill_signature, before_cache=fill_before, after_cache=fill_before, capability_key="feature.fill", edited_object_id="hole:85", ) _assert( fill_still_present.get("ok") is False and fill_still_present.get("reason") == "feature-still-present", f"feature.fill should fail when the edited feature still matches: {fill_still_present}", ) fill_no_cache = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=fill_signature, capability_key="feature.fill", ) _assert( fill_no_cache.get("ok") is False and fill_no_cache.get("reason") == "removal-check-unavailable", f"feature.fill should require a new cache for removal verification: {fill_no_cache}", ) round_before = _cache(_feature("round:60", "round", 60, center=(1.0, 0.0, 2.0), capability_key="feature.delete_round_or_chamfer")) round_after = _cache(_feature("hole:85", "hole", 85, center=(5.0, 0.0, 2.0), capability_key="hole.diameter")) round_delete_ok = validate_scdm_edit_result( {"ok": True, "output_step": str(output_step)}, before_signature=round_before["objects"][0]["geometrySignature"], # type: ignore[index] before_cache=round_before, after_cache=round_after, capability_key="feature.delete_round_or_chamfer", edited_object_id="round:60", ) _assert(round_delete_ok.get("ok") is True, f"round/chamfer delete should pass when the edited feature disappears: {round_delete_ok}") _assert(round_delete_ok.get("targetCheck", {}).get("reason") == "removed", f"round/chamfer delete should use removal target check: {round_delete_ok}") ambiguous_after = _cache( _hole("hole:100", 100, diameter=0.75, center=(0.5, 1.0, 9.5)), _hole("hole:101", 101, diameter=0.75, center=(0.5, 1.0, 9.5)), ) ambiguous = match_scdm_object_by_signature(before_signature, ambiguous_after, capability_key="hole.diameter") _assert(ambiguous.get("status") == "multiple", f"ambiguous matches should be reported: {ambiguous}") print("scdm result validator ok") return 0 if __name__ == "__main__": raise SystemExit(main())