Files

756 lines
36 KiB
Python
Raw Permalink Normal View History

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}",
)
single_left_signature = dict(before_pattern_segment["objects"][0]["geometrySignature"]) # type: ignore[index]
single_left_signature["movingSide"] = "single_left"
single_left_ok = validate_scdm_edit_result(
{
"ok": True,
"output_step": str(output_step),
"applied": {"segmentSpacing": 6.5, "segmentIndex": 1, "spacingMode": "segment_single_left"},
},
before_signature=single_left_signature,
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(
single_left_ok.get("ok") is True
and single_left_ok.get("targetCheck", {}).get("spacingMode") == "segment_single_left",
f"pattern.segment_spacing should validate move-only-left-instance mode: {single_left_ok}",
)
single_right_signature = dict(before_pattern_segment["objects"][0]["geometrySignature"]) # type: ignore[index]
single_right_signature["movingSide"] = "single_right"
single_right_ok = validate_scdm_edit_result(
{
"ok": True,
"output_step": str(output_step),
"applied": {"segmentSpacing": 6.5, "segmentIndex": 1, "spacingMode": "segment_single_right"},
},
before_signature=single_right_signature,
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(
single_right_ok.get("ok") is True
and single_right_ok.get("targetCheck", {}).get("spacingMode") == "segment_single_right",
f"pattern.segment_spacing should validate move-only-right-instance mode: {single_right_ok}",
)
wrong_mode = validate_scdm_edit_result(
{
"ok": True,
"output_step": str(output_step),
"applied": {"segmentSpacing": 6.5, "segmentIndex": 1, "spacingMode": "segment_after"},
},
before_signature=single_right_signature,
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(
wrong_mode.get("ok") is False and wrong_mode.get("reason") == "pattern-segment-spacing-mode-mismatch",
f"pattern.segment_spacing should fail when SCDM reports a different modeling intent: {wrong_mode}",
)
missing_mode = validate_scdm_edit_result(
{
"ok": True,
"output_step": str(output_step),
"applied": {"segmentSpacing": 6.5, "segmentIndex": 1},
},
before_signature=single_right_signature,
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(
missing_mode.get("ok") is False and missing_mode.get("reason") == "pattern-segment-spacing-mode-missing",
f"pattern.segment_spacing should require SCDM to report a known modeling intent: {missing_mode}",
)
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_repartition = _cache_with_summary(
{"bodyCount": 9, "objectCount": 548, "faceCount": 157, "edgeCount": 390},
_hole("hole:90", 90, diameter=0.9, center=(0.5, 1.0, 9.5)),
)
summary_repartition = check_scdm_summary_delta(summary_before, summary_after_repartition, capability_key="hole.diameter")
_assert(
summary_repartition.get("ok") is None and summary_repartition.get("reason") == "body-count-repartitioned",
f"SCDM body repartition should be a warning, not a hard failure: {summary_repartition}",
)
repartition_ok = 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_repartition,
capability_key="hole.diameter",
expected_target=0.9,
edited_object_id="hole:85",
brep_validator=lambda path: {"ok": path.is_file(), "reason": "ok"},
)
_assert(
repartition_ok.get("ok") is True
and repartition_ok.get("summaryCheck", {}).get("reason") == "body-count-repartitioned"
and repartition_ok.get("validationWarnings"),
f"target-verified SCDM hole diameter edit should survive body repartition warnings: {repartition_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())