feat(cadfs): expand selector provenance coverage

This commit is contained in:
2026-09-14 11:00:14 +08:00
parent 994d06aaea
commit de21ee8a5b
45 changed files with 19837 additions and 842 deletions
+41 -25
View File
@@ -59,7 +59,8 @@ class IntegrationTests(unittest.TestCase):
])
with tempfile.TemporaryDirectory() as tmp:
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
rebuilt = Path(tmp) / "rebuild.step"
outcome = rebuild_candidate(candidate, rebuilt)
self.assertEqual(outcome["status"], "rebuilt")
self.assertEqual(
[item["feature_id"] for item in outcome["result"]["feature_results"]],
@@ -123,7 +124,10 @@ class IntegrationTests(unittest.TestCase):
[4.48, 2.36],
)
self.assertNotIn("result_mode", next(item for item in result.cdsl["features"] if item["id"] == "f_F7")["params"])
self.assertEqual(
next(item for item in result.cdsl["features"] if item["id"] == "f_F7")["params"]["result_mode"],
"new_body",
)
self.assertEqual(next(item for item in result.cdsl["features"] if item["id"] == "f_F9")["params"]["result_mode"], "new_body")
with tempfile.TemporaryDirectory() as tmp:
@@ -140,7 +144,8 @@ class IntegrationTests(unittest.TestCase):
candidate = lower_model(parse_featurescript(feature.read_text(), "00925274"), {}).cdsl
with tempfile.TemporaryDirectory() as tmp:
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
rebuilt = Path(tmp) / "rebuild.step"
outcome = rebuild_candidate(candidate, rebuilt)
self.assertEqual(outcome["status"], "rebuilt")
result = outcome["result"]
@@ -166,9 +171,13 @@ class IntegrationTests(unittest.TestCase):
result = lower_model(parse_featurescript(feature.read_text(), "00007264"), {})
fillet = next(item for item in result.cdsl["features"] if item["id"] == "f_F2")
self.assertEqual(fillet["atomic_id"], "fillet")
self.assertEqual(len(fillet["selectors"]), 1)
query_set = fillet["selectors"][0]
self.assertEqual(query_set["selector_intent"]["query_family"], "QUERY_SET")
self.assertEqual(query_set["selector_intent"]["query_set_contract"], "proven_operand_union")
self.assertTrue(all(
selector["selector_intent"]["query_family"] == "SWEPT_EDGE"
for selector in fillet["selectors"]
operand["selector_intent"]["query_family"] == "SWEPT_EDGE"
for operand in query_set["query_operands"]
))
with tempfile.TemporaryDirectory() as tmp:
@@ -176,6 +185,8 @@ class IntegrationTests(unittest.TestCase):
self.assertEqual(outcome["status"], "rebuilt")
binding = next(item for item in outcome["selector_binding"] if item["feature_id"] == "f_F2")
self.assertEqual(len(binding["resolved"]), 4)
resolution = next(item for item in outcome["result"]["selector_resolution"] if item["feature_id"] == "f_F2")
self.assertEqual(resolution["resolution_mode"], "query_set_union")
vertical_edges = [
relation
for delta in outcome["result"]["topology_deltas"]
@@ -287,11 +298,13 @@ class IntegrationTests(unittest.TestCase):
self.assertNotIn("stable_id", selector)
with tempfile.TemporaryDirectory() as tmp:
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
rebuilt = Path(tmp) / "rebuild.step"
outcome = rebuild_candidate(candidate, rebuilt)
self.assertTrue(rebuilt.exists())
self.assertEqual(outcome["status"], "rebuild_failed")
self.assertEqual(outcome["error"]["message"], "f_F4: selector_query_unsupported during incremental replay")
self.assertEqual(outcome["status"], "runtime_ineligible")
prefix = outcome["last_executable_prefix"]["result"]
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F3")
self.assertEqual([item["feature_id"] for item in prefix["feature_results"]], ["f_F1", "f_F2", "f_F3"])
resolution = next(item for item in prefix["selector_resolution"] if item["feature_id"] == "f_F2")
self.assertEqual(resolution["status"], "resolved")
@@ -359,7 +372,7 @@ class IntegrationTests(unittest.TestCase):
result = lower_model(parse_featurescript(feature.read_text(), "00542223"), {})
sweep = next(item for item in result.cdsl["features"] if item["id"] == "f_F5")
segment = sweep["params"]["path"]["segment"]
self.assertEqual(result.status, "converted_complete")
self.assertEqual(result.status, "converted_partial")
self.assertEqual(sweep["atomic_id"], "sweep_add")
self.assertEqual(sweep["sketch_id"], "sketch_F4")
self.assertEqual(segment["points"], [[-40.0, -50.0], [-20.0, -14.96], [0.0, 5.0]])
@@ -367,6 +380,7 @@ class IntegrationTests(unittest.TestCase):
self.assertEqual(segment["end_tangent"], [52.88, 49.52])
self.assertNotIn("F2", [item.get("feature_id") for item in result.diagnostics])
self.assertNotIn("F5", [item.get("feature_id") for item in result.diagnostics])
self.assertIn("F7", [item.get("feature_id") for item in result.diagnostics])
def test_direct_sketch_wire_qbodytype_path_lowers_and_rebuilds_00896761(self):
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
@@ -390,17 +404,18 @@ class IntegrationTests(unittest.TestCase):
self.assertEqual(outcome["status"], "rebuilt")
self.assertEqual([item["feature_id"] for item in outcome["result"]["feature_results"]], ["f_F2"])
def test_multi_entity_sketch_wire_qbodytype_path_remains_deferred_00786708(self):
def test_multi_entity_sketch_wire_qbodytype_path_lowers_as_a_source_ordered_wire_00786708(self):
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
feature = root / "featurescript_rp/0078/00786708.txt"
if not feature.exists(): self.skipTest("CADFS sample is not installed")
result = lower_model(parse_featurescript(feature.read_text(), "00786708"), {})
self.assertIn({
"code": "unsupported_engine_capability", "feature_id": "F2", "operation": "sweep", "capability": "sweep_path_query",
"message": "current CDSL sweep requires one direct sketch line or B-spline path",
}, result.diagnostics)
self.assertNotIn("f_F2", {item["id"] for item in result.cdsl["features"]})
self.assertEqual(result.status, "converted_complete")
first_sweep = next(item for item in result.cdsl["features"] if item["id"] == "f_F2")
self.assertEqual(
[(item["source_entity_id"], item["type"]) for item in first_sweep["params"]["path"]["segments"]],
[("E1", "arc"), ("E0", "line"), ("E2.MirrorCS", "arc")],
)
later_sweep = next(item for item in result.cdsl["features"] if item["id"] == "f_F5")
self.assertEqual(later_sweep["params"]["path"]["segment"]["source_entity_id"], "E5")
@@ -417,14 +432,13 @@ class IntegrationTests(unittest.TestCase):
with tempfile.TemporaryDirectory() as tmp:
rebuilt = rebuild_cdsl(candidate, Path(tmp) / "pattern.step")
self.assertEqual(rebuilt["solid_count"], 1)
bbox = rebuilt["bbox_mm"]
self.assertAlmostEqual(bbox["min"][0], -52.14101625137758)
self.assertAlmostEqual(bbox["min"][1], -57.500000100000065)
self.assertAlmostEqual(bbox["max"][0], 52.14101625137762)
self.assertAlmostEqual(bbox["max"][1], 37.5000001000001)
self.assertEqual(rebuilt["solid_count"], 2)
self.assertEqual(
[item["feature_id"] for item in rebuilt["feature_results"]],
["f_F1", "f_F3", "f_F5", "f_F6"],
)
def test_fused_body_circular_copy_faces_preserve_shell_prefix_00542223(self):
def test_deferred_pattern_shell_keeps_later_executable_feature_00542223(self):
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
feature = root / "featurescript_rp/0054/00542223.txt"
if not feature.exists(): self.skipTest("CADFS sample is not installed")
@@ -433,9 +447,11 @@ class IntegrationTests(unittest.TestCase):
with tempfile.TemporaryDirectory() as tmp:
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
self.assertEqual(outcome["status"], "rebuild_failed")
self.assertEqual(outcome["error"]["message"], "f_F7: selector_query_unsupported during incremental replay")
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F6")
self.assertEqual(outcome["status"], "rebuilt")
self.assertEqual(
[item["feature_id"] for item in outcome["result"]["feature_results"]],
["f_F1", "f_F3", "f_F5", "f_F6", "f_F10"],
)
def test_face_chamfer_source_query_is_not_geometry_bound_00111611(self):
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
File diff suppressed because it is too large Load Diff
+51 -2
View File
@@ -4,8 +4,8 @@ import unittest
from cadfs_to_cdsl.featurescript_lexer import lex
from cadfs_to_cdsl.featurescript_parser import parse_featurescript
from cadfs_to_cdsl.ir import Call
from cadfs_to_cdsl.lowering import _queries, _source_refs
from cadfs_to_cdsl.query_parser import parse_query
from cadfs_to_cdsl.lowering import _number, _queries, _source_refs
from cadfs_to_cdsl.query_parser import parse_query, query_expr
from cadfs_to_cdsl.units import length_mm
@@ -28,6 +28,14 @@ TRANSFORM_SOURCE = SOURCE.replace(
''',
)
UNKNOWN_FEATURE_SOURCE = SOURCE.replace(
'\n});\n',
'''
assignVariable(context, id + "F2", {"name" : "diameter", "value" : 10 * mm});
});
''',
)
class ParserTests(unittest.TestCase):
def test_lexer_ignores_comments_and_preserves_lines(self):
@@ -47,6 +55,10 @@ class ParserTests(unittest.TestCase):
self.assertEqual(model.features[-1].operation, "transform")
self.assertEqual(model.features[-1].params["transformType"], "TransformType.TRANSLATION_3D")
def test_unknown_direct_context_feature_is_preserved_for_capability_diagnostics(self):
model = parse_featurescript(UNKNOWN_FEATURE_SOURCE, "unknown-feature")
self.assertEqual([(feature.feature_id, feature.operation) for feature in model.features], [("F1", "extrude"), ("F2", "assignVariable")])
def test_query_parser(self):
query = Call("makeQuery", [Call("__binary__", ["id", "+", "F1.opExtrude"]), "CAP_EDGE", "EDGE", {"isStart": False, "x": Call("sQuery", [Call("__binary__", ["id", "+", "F0.wireOp"]), "EDGE", "E0"])}])
value = parse_query(query)
@@ -55,6 +67,43 @@ class ParserTests(unittest.TestCase):
self.assertEqual(value.ast["call"], "makeQuery")
self.assertEqual(value.ast["args"][0]["call"], "__binary__")
def test_signed_parenthesized_scalar_preserves_unit_expression_semantics(self):
model = parse_featurescript(SOURCE.replace(
'"depth":120 * mm', '"depth":-(10 + 2) / 2 * mm', 1,
), "signed-scalar")
depth = model.features[-1].params["depth"]
self.assertEqual((depth.name, depth.args[1]), ("__binary__", "*"))
self.assertEqual(_number(depth), -6.0)
def test_query_expression_preserves_nested_set_and_filter_boundaries(self):
source = Call("qUnion", [[
Call("qConstructionFilter", [
Call("qBodyType", [
Call("qCreatedBy", [Call("__binary__", ["id", "+", "F1"]), "EDGE"]),
"BodyType.WIRE",
]),
"ConstructionObject.NO",
]),
Call("qSubtraction", [
Call("sQuery", [Call("__binary__", ["id", "+", "F0.wireOp"]), "EDGE", "E0"]),
Call("sQuery", [Call("__binary__", ["id", "+", "F0.wireOp"]), "EDGE", "E1"]),
]),
]])
expression = query_expr(source)
self.assertEqual(expression["version"], "1.0")
self.assertEqual(expression["root"]["node"], "set")
self.assertEqual(expression["root"]["operator"], "union")
filtered, subtraction = expression["root"]["operands"]
self.assertEqual((filtered["node"], filtered["filter"]), ("filter", "construction"))
self.assertEqual((filtered["input"]["node"], filtered["input"]["filter"]), ("filter", "body_type"))
self.assertEqual((subtraction["node"], subtraction["operator"]), ("set", "subtraction"))
parsed = parse_query(source)
self.assertEqual(parsed.query_combinators, ["qUnion", "qSubtraction"])
self.assertEqual(
parsed.filters,
["qConstructionFilter", "qBodyType"],
)
def test_source_version_and_standard_library_are_retained(self):
source = '''FeatureScript 1511;
import(path : "onshape/std/geometry.fs", version : "1511.0");
+34 -1
View File
@@ -3,7 +3,9 @@ from __future__ import annotations
import json, tempfile, unittest
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from cadfs_to_cdsl.reports import write_json
from cadfs_to_cdsl.dataset import Sample
from cadfs_to_cdsl.operation_registry import build_operation_registry
from cadfs_to_cdsl.reports import generate_markdown_report, write_json
class ReportTests(unittest.TestCase):
@@ -17,5 +19,36 @@ class ReportTests(unittest.TestCase):
self.assertIn(payload["index"], range(64))
self.assertEqual(list(path.parent.glob(".summary.json.*.tmp")), [])
def test_operation_registry_distinguishes_absent_roadmap_work_items(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
source = root / "00000001.txt"
source.write_text(
'FeatureScript 1511; export const f = defineFeature(function(context, id, definition) {'
' extrude(context, id + "F1", {"depth": 1 * mm}); });',
encoding="utf-8",
)
registry = build_operation_registry([Sample("00000001", files={"featurescript": str(source)})])
self.assertEqual(registry["schema"], "cadfs_to_cdsl.operation_registry.v1")
self.assertEqual(registry["operations"]["extrude"]["feature_count"], 1)
self.assertEqual(registry["operations"]["transform"]["state"], "not_observed_in_current_source")
self.assertEqual(registry["operations"]["thicken"]["feature_count"], 0)
def test_markdown_report_uses_source_registry_instead_of_static_operation_claims(self):
with tempfile.TemporaryDirectory() as temporary:
output = Path(temporary)
write_json(output / "operation_registry.json", {
"operations": {
"transform": {"roadmap_work_item": True, "state": "observed"},
"thicken": {"roadmap_work_item": True, "state": "not_observed_in_current_source"},
},
})
report = generate_markdown_report(output, [])
text = report.read_text(encoding="utf-8")
self.assertIn("thicken", text)
self.assertNotIn("draft, thicken, split", text)
if __name__ == "__main__": unittest.main()
@@ -6,7 +6,13 @@ from pathlib import Path
from cadfs_to_cdsl.dataset import Sample
from cadfs_to_cdsl.reports import read_json
from cadfs_to_cdsl.selector_candidate_demo import _search_candidate_records, run_geometry_probe, strip_provenance_intents
from cadfs_to_cdsl.selector_candidate_demo import (
_query_candidate_groups,
_search_candidate_records,
_strip_selector_search_tokens,
run_geometry_probe,
strip_provenance_intents,
)
def _contains_selector_intent(value: object) -> bool:
@@ -18,6 +24,39 @@ def _contains_selector_intent(value: object) -> bool:
class SelectorCandidateDemoTests(unittest.TestCase):
@staticmethod
def _query_group_candidate(query_family: str, source_entity: str | None = None, source_entities: list[str] | None = None) -> dict:
intent = {
"version": "1.0",
"kind": "edge" if query_family == "SWEPT_EDGE" else "face",
"query_family": query_family,
"source_query": {"ast": {"call": "makeQuery", "args": ["owner", query_family]}},
"derivation_policy": {"allowed": ["boundary"], "multiplicity": "all_fragments" if query_family == "SWEPT_FACE" else "one"},
}
if source_entity is not None:
intent["source_entity"] = {"sketch_id": "S", "entity_id": source_entity}
if source_entities is not None:
intent["source_entities"] = [{"sketch_id": "S", "entity_id": entity_id} for entity_id in source_entities]
return {"kind": intent["kind"], "owner_feature_id": "f_extrude", "selector_intent": intent}
def _query_group_provenance(self, selector: dict) -> dict:
return {
"geometry": {
"sketches": [{
"id": "sketch_S", "source_sketch_id": "S",
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
"profile": {"type": "planar_imprint", "source_entities": [
{"id": "E1", "curve": {"type": "line", "start": [0, 0], "end": [3, 0]}},
{"id": "E2", "curve": {"type": "line", "start": [3, 0], "end": [3, 2]}},
]},
}],
},
"features": [
{"id": "f_extrude", "atomic_id": "extrude_add_blind", "sketch_id": "sketch_S", "params": {"distance_mm": 5}},
{"id": "f_consumer", "selectors": [selector]},
],
}
def test_search_candidates_excludes_records_below_normal_resolution_threshold(self) -> None:
resolution = type("Resolution", (), {
"candidates": (
@@ -34,11 +73,85 @@ class SelectorCandidateDemoTests(unittest.TestCase):
def test_strip_provenance_intents_removes_nested_selector_metadata(self) -> None:
value = {
"selector_intent": {"version": "1.0"},
"output_role": "extrude.end",
"output_role_source": {"owner_feature_id": "f0", "output_role": "extrude.end"},
"params": {"reference": {"selector_intent_version": "1.0"}},
}
self.assertEqual(strip_provenance_intents(value), 2)
self.assertEqual(value, {"params": {"reference": {}}})
def test_query_group_keeps_all_ranked_swept_face_fragments_together(self) -> None:
selector = self._query_group_candidate("SWEPT_FACE", source_entity="E1")
provenance = self._query_group_provenance(selector)
resolution = type("Resolution", (), {"candidates": (
{"record_id": "fragment-a", "score": 1.0, "geometry": {"surface_type": "plane", "plane_normal": [0, -1, 0], "plane_offset_mm": 0}},
{"record_id": "fragment-b", "score": 1.0, "geometry": {"surface_type": "plane", "plane_normal": [0, 1, 0], "plane_offset_mm": 0}},
{"record_id": "wrong-wall", "score": 1.0, "geometry": {"surface_type": "plane", "plane_normal": [1, 0, 0], "plane_offset_mm": 3}},
)})()
descriptor, groups, count = _query_candidate_groups(
provenance, "f_consumer:selectors[0]", selector, resolution, maximum=8,
)
self.assertEqual(count, 1)
self.assertEqual(descriptor["ranking_status"], "source_geometry_ranked")
self.assertEqual(descriptor["multiplicity"], "all_fragments")
self.assertEqual([record["record_id"] for record in groups[0]["records"]], ["fragment-a", "fragment-b"])
self.assertEqual(groups[0]["ranking"]["method"], "all_fragments_source_geometry")
def test_query_group_ranks_the_source_pair_vertical_edge(self) -> None:
selector = self._query_group_candidate("SWEPT_EDGE", source_entities=["E1", "E2"])
provenance = self._query_group_provenance(selector)
resolution = type("Resolution", (), {"candidates": (
{"record_id": "source-vertex", "score": 1.0, "geometry": {"curve_type": "line", "start_mm": [3, 0, 0], "end_mm": [3, 0, 5]}},
{"record_id": "other-vertex", "score": 1.0, "geometry": {"curve_type": "line", "start_mm": [0, 0, 0], "end_mm": [0, 0, 5]}},
)})()
descriptor, groups, count = _query_candidate_groups(
provenance, "f_consumer:selectors[0]", selector, resolution, maximum=8,
)
self.assertEqual(count, 1)
self.assertEqual(descriptor["query_family"], "SWEPT_EDGE")
self.assertEqual(groups[0]["records"][0]["record_id"], "source-vertex")
self.assertEqual(groups[0]["ranking"]["method"], "source_pair_extrusion_vertex")
def test_query_group_ranks_the_line_arc_intersection_edge(self) -> None:
selector = self._query_group_candidate("SWEPT_EDGE", source_entities=["E1", "E2"])
provenance = self._query_group_provenance(selector)
provenance["geometry"]["sketches"][0]["profile"]["source_entities"][0]["curve"] = {
"type": "arc", "start": [-1, 0], "end": [1, 0], "center": [0, 0], "radius_mm": 1, "clockwise": True,
}
provenance["geometry"]["sketches"][0]["profile"]["source_entities"][1]["curve"] = {
"type": "line", "start": [0, -2], "end": [0, 2],
}
resolution = type("Resolution", (), {"candidates": (
{"record_id": "arc-line-intersection", "score": 1.0, "geometry": {"curve_type": "line", "start_mm": [0, 1, 0], "end_mm": [0, 1, 5]}},
{"record_id": "unrelated-vertical", "score": 1.0, "geometry": {"curve_type": "line", "start_mm": [0, -1, 0], "end_mm": [0, -1, 5]}},
)})()
_descriptor, groups, count = _query_candidate_groups(
provenance, "f_consumer:selectors[0]", selector, resolution, maximum=8,
)
self.assertEqual(count, 1)
self.assertEqual(groups[0]["records"][0]["record_id"], "arc-line-intersection")
def test_query_group_ignores_a_candidate_missing_cylinder_axis_evidence(self) -> None:
selector = self._query_group_candidate("SWEPT_FACE", source_entity="E1")
provenance = self._query_group_provenance(selector)
provenance["geometry"]["sketches"][0]["profile"]["source_entities"][0]["curve"] = {
"type": "arc", "start": [-1, 0], "end": [1, 0], "center": [0, 0], "radius_mm": 1, "clockwise": True,
}
resolution = type("Resolution", (), {"candidates": (
{"record_id": "missing-axis", "score": 1.0, "geometry": {"surface_type": "cylinder", "radius_mm": 1}},
)})()
descriptor, groups, count = _query_candidate_groups(
provenance, "f_consumer:selectors[0]", selector, resolution, maximum=8,
)
self.assertEqual(count, 1)
self.assertEqual(descriptor["ranking_status"], "source_geometry_unavailable")
self.assertEqual(groups[0]["records"][0]["record_id"], "missing-axis")
def test_experiment_search_tokens_are_not_persisted(self) -> None:
value = {"selectors": [{"_selector_search_token": "f:selectors[0]"}]}
self.assertEqual(_strip_selector_search_tokens(value), 1)
self.assertEqual(value, {"selectors": [{}]})
def test_geometry_probe_keeps_heuristic_success_separate_from_provenance(self) -> None:
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
feature = root / "featurescript_rp/0000/00002243.txt"