feat: 接入 SCDM 优先编辑闭环并支持阵列局部间距

接入 SCDM probe/edit/cache/校验链路,增强孔组、阵列、关系式和参数表交互。

支持阵列相邻段间距、移动意图切换、结果回滚校验,并补充对应回归脚本。
This commit is contained in:
2026-08-19 18:02:47 +08:00
parent a3eb7e2476
commit 4e7877e05c
21 changed files with 5646 additions and 491 deletions
+487
View File
@@ -44,6 +44,29 @@ def _hole(object_id: str, face_id: int, *, diameter: float, center: tuple[float,
}
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,
@@ -128,6 +151,419 @@ def main() -> int:
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)),
@@ -154,6 +590,57 @@ def main() -> int:
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)),