2026-08-19 10:28:09 +08:00
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 )},
],
}
2026-08-19 18:02:47 +08:00
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 )},
],
}
2026-08-19 10:28:09 +08:00
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; \n END-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 } " )
2026-08-19 18:02:47 +08:00
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 } " )
2026-08-19 10:28:09 +08:00
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 } " )
2026-08-19 18:02:47 +08:00
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 } " )
2026-08-19 10:28:09 +08:00
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 ())