994d06aaea
- 新增 selector_candidate_demo,移除 provenance intent 后枚举候选 selector - 对候选分支执行有界重建与严格 STEP 比较 - 仅在候选遍历完整且唯一 strict 通过时生成 selector 映射记录 - 增加 selector 候选搜索、预算限制和记录生成的测试 - 保持生产 selector resolver 不受 Demo 逻辑影响 - 更新 CADFS 能力台账,记录 IMPRINT 派生 profile 的 lineage selector 缺口
211 lines
10 KiB
Python
211 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from cadfs_to_cdsl.featurescript_parser import parse_featurescript
|
|
from cadfs_to_cdsl.lowering import lower_model
|
|
from cadfs_to_cdsl.rebuild import rebuild_candidate
|
|
from cadfs_to_cdsl.selector_binding import _score, bind_candidate_selectors
|
|
|
|
|
|
class SelectorBindingTests(unittest.TestCase):
|
|
def test_face_normal_match_is_orientation_independent(self) -> None:
|
|
score = _score(
|
|
{"normal": [0.0, 0.0, 1.0], "plane_offset_mm": 12.0},
|
|
{"normal": [0.0, 0.0, -1.0], "plane_offset_mm": 12.0},
|
|
)
|
|
self.assertEqual(score, 1.0)
|
|
|
|
def test_rotational_face_axis_direction_is_orientation_independent(self) -> None:
|
|
score = _score(
|
|
{"axis_direction": [0.0, 0.0, 1.0]},
|
|
{"axis_direction": [0.0, 0.0, -1.0]},
|
|
)
|
|
self.assertEqual(score, 1.0)
|
|
|
|
def test_axis_origin_distinguishes_parallel_cylinders(self) -> None:
|
|
score = _score(
|
|
{"axis_origin_mm": [0.0, 25.4, 33.37]},
|
|
{"axis_origin_mm": [33.38, 25.4, 0.0]},
|
|
)
|
|
self.assertEqual(score, 0.0)
|
|
|
|
def test_empty_snapshot_score_is_not_treated_as_a_match(self) -> None:
|
|
self.assertEqual(_score({}, {"normal": [0.0, 0.0, 1.0]}), 0.0)
|
|
|
|
def test_geometry_free_context_selector_uses_unique_active_record_when_owner_is_stale(self) -> None:
|
|
cdsl = {
|
|
"features": [
|
|
{"id": "f_source", "atomic_id": "reference_plane"},
|
|
{
|
|
"id": "f_mirror",
|
|
"atomic_id": "pattern_mirror",
|
|
"params": {},
|
|
"selectors": [{"kind": "plane", "owner_feature_id": "f_source"}],
|
|
},
|
|
],
|
|
}
|
|
report = {
|
|
"feature_results": [],
|
|
"topology_records": [{
|
|
"kind": "plane",
|
|
"record_id": "context:plane:1",
|
|
"owner_feature_ids": ["f_live_plane"],
|
|
"geometry": {},
|
|
}],
|
|
}
|
|
|
|
with patch("engine.cdsl_engine.runtime.rebuild_cdsl", return_value=report):
|
|
bound, _ = bind_candidate_selectors(cdsl)
|
|
|
|
selector = bound["features"][1]["selectors"][0]
|
|
self.assertEqual(selector["owner_feature_id"], "f_live_plane")
|
|
self.assertEqual(selector["stable_id"], "context:plane:1")
|
|
self.assertEqual(bound["features"][1]["params"]["mirror_plane"], selector)
|
|
|
|
def test_geometry_free_instance_selector_does_not_fall_back_to_active_record(self) -> None:
|
|
cdsl = {
|
|
"features": [
|
|
{"id": "f_source", "atomic_id": "reference_plane"},
|
|
{
|
|
"id": "f_mirror",
|
|
"atomic_id": "pattern_mirror",
|
|
"params": {},
|
|
"selectors": [{
|
|
"kind": "plane",
|
|
"owner_feature_id": "f_source",
|
|
"owner_match_required": True,
|
|
}],
|
|
},
|
|
],
|
|
}
|
|
report = {
|
|
"feature_results": [],
|
|
"topology_records": [{
|
|
"kind": "plane",
|
|
"record_id": "context:plane:1",
|
|
"owner_feature_ids": ["f_live_plane"],
|
|
"geometry": {},
|
|
}],
|
|
}
|
|
|
|
with patch("engine.cdsl_engine.runtime.rebuild_cdsl", return_value=report):
|
|
with self.assertRaisesRegex(ValueError, "f_mirror: selector_not_found after prefix rebuild"):
|
|
bind_candidate_selectors(cdsl)
|
|
|
|
def test_swept_face_area_lower_bound_rejects_coplanar_fragment(self) -> None:
|
|
expected = {"normal": [0.0, 1.0, 0.0], "plane_offset_mm": 54.69, "minimum_area_mm2": 285.0}
|
|
self.assertIsNone(_score(expected, {"normal": [0.0, 1.0, 0.0], "plane_offset_mm": 54.69, "area_mm2": 0.64}))
|
|
self.assertEqual(_score(expected, {"normal": [0.0, 1.0, 0.0], "plane_offset_mm": 54.69, "area_mm2": 463.7}), 1.0)
|
|
|
|
def test_reversed_cap_normal_reverses_its_plane_offset(self) -> None:
|
|
score = _score(
|
|
{"normal": [0.0, 0.0, 1.0], "plane_offset_mm": 10.0},
|
|
{
|
|
"normal": [0.0, 0.0, 1.0],
|
|
"plane_normal": [0.0, 0.0, -1.0],
|
|
"plane_offset_mm": -10.0,
|
|
},
|
|
)
|
|
|
|
self.assertEqual(score, 1.0)
|
|
|
|
def test_cap_face_output_role_is_validated_without_snapshot_rebinding(self) -> None:
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0001/00016195.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
candidate = lower_model(parse_featurescript(feature.read_text(), "00016195"), {})
|
|
|
|
bound, evidence = bind_candidate_selectors(candidate.cdsl)
|
|
|
|
selector = next(item for item in bound["features"] if item["id"] == "f_F3")["selectors"][0]
|
|
self.assertEqual({key: selector[key] for key in (
|
|
"kind", "owner_feature_id", "output_role", "source", "confidence",
|
|
)}, {
|
|
"kind": "face", "owner_feature_id": "f_F1", "output_role": "extrude.end",
|
|
"source": "runtime_snapshot", "confidence": 1.0,
|
|
})
|
|
self.assertEqual(selector["selector_intent"]["query_family"], "CAP_FACE")
|
|
self.assertEqual(selector["selector_intent"]["evidence"], "operation_role")
|
|
self.assertNotIn("stable_id", selector)
|
|
self.assertNotIn("snapshot_id", selector)
|
|
self.assertNotIn("geometry", selector)
|
|
binding = next(item for item in evidence if item["feature_id"] == "f_F3")
|
|
self.assertEqual(len(binding["resolved"]), 1)
|
|
self.assertEqual(binding["resolved"][0]["output_roles"], ["extrude.end"])
|
|
|
|
def test_rebuild_candidate_uses_one_incremental_replay(self) -> None:
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0001/00016195.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
candidate = lower_model(parse_featurescript(feature.read_text(), "00016195"), {})
|
|
|
|
# The production path must bind and export from one live session. If
|
|
# it called the old compatibility binder, this patched entry point
|
|
# would fail while rebuilding selector-bearing prefixes.
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
with patch(
|
|
"engine.cdsl_engine.runtime.rebuild_cdsl",
|
|
side_effect=AssertionError("production rebuild invoked legacy prefix replay"),
|
|
):
|
|
outcome = rebuild_candidate(candidate.cdsl, Path(directory) / "00016195.step")
|
|
|
|
self.assertEqual(outcome["status"], "rebuilt")
|
|
self.assertEqual(
|
|
[item["feature_id"] for item in outcome["result"]["feature_results"]],
|
|
[item["id"] for item in candidate.cdsl["features"]],
|
|
)
|
|
self.assertTrue(outcome["selector_binding"])
|
|
|
|
def test_shell_offset_face_preserves_true_dependency_but_rejects_unproven_cap_selector(self) -> None:
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0010/00107631.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
candidate = lower_model(parse_featurescript(feature.read_text(), "00107631"), {})
|
|
self.assertEqual(candidate.status, "converted_complete")
|
|
|
|
# F2 consumes its immediate direct CAP_FACE output role. F3 retains
|
|
# its exact OFFSET_FACE true-dependency intent, but its separate
|
|
# start-cap removal selector is deferred and must not rebind
|
|
# geometrically.
|
|
prefix = deepcopy(candidate.cdsl)
|
|
prefix["features"] = prefix["features"][:3]
|
|
first, offset = prefix["features"][-1]["selectors"]
|
|
self.assertEqual(first["selector_intent"]["query_family"], "CAP_FACE")
|
|
self.assertEqual(first["selector_intent"]["derivation_policy"]["multiplicity"], "none")
|
|
self.assertEqual({key: offset[key] for key in (
|
|
"kind", "owner_feature_id", "output_role", "output_role_source", "source", "confidence",
|
|
)}, {
|
|
"kind": "face", "owner_feature_id": "f_F2", "output_role": "shell.offset_face",
|
|
"output_role_source": {"owner_feature_id": "f_F1", "output_role": "extrude.start"},
|
|
"source": "runtime_snapshot", "confidence": 1.0,
|
|
})
|
|
self.assertEqual(offset["selector_intent"]["query_family"], "OFFSET_FACE")
|
|
self.assertEqual(offset["selector_intent"]["disambiguation"]["type"], "true_dependency")
|
|
with self.assertRaisesRegex(ValueError, "f_F3: selector_query_unsupported after prefix rebuild"):
|
|
bind_candidate_selectors(prefix)
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
outcome = rebuild_candidate(candidate.cdsl, Path(directory) / "00107631.step")
|
|
self.assertEqual(outcome["status"], "rebuild_failed")
|
|
self.assertEqual(outcome["error"]["message"], "f_F3: selector_query_unsupported during incremental replay")
|
|
self.assertEqual(outcome["last_executable_prefix"]["failed_feature_id"], "f_F3")
|
|
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F2")
|
|
|
|
def test_intersection_vertex_preserves_outer_copy_provenance_without_geometry_binding(self) -> None:
|
|
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
|
feature = root / "featurescript_rp/0042/00423838.txt"
|
|
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
|
candidate = lower_model(parse_featurescript(feature.read_text(), "00423838"), {})
|
|
reference = next(item for item in candidate.cdsl["features"] if item["id"] == "f_F7")["params"]["end_condition"]["reference"]
|
|
self.assertTrue(all(item["owner_match_required"] for item in reference["intersection_of"][:2]))
|
|
self.assertEqual(reference["selector_intent"]["query_family"], "INTERSECT")
|
|
copy_components = reference["intersection_of"][:2]
|
|
self.assertEqual([item["selector_intent"]["query_family"] for item in copy_components], ["COPY", "COPY"])
|
|
with self.assertRaisesRegex(ValueError, "f_F7: selector_query_unsupported after prefix rebuild"):
|
|
bind_candidate_selectors(candidate.cdsl)
|