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
+99 -2
View File
@@ -54,17 +54,109 @@ def query_ast(value: Any) -> dict[str, Any] | list[Any] | str | float | bool | N
return str(value)
_SET_COMBINATORS = {
"qUnion": "union",
"qIntersection": "intersection",
"qSubtraction": "subtraction",
}
_FILTERS = {
"qAdjacent": "adjacent",
"qOwnerBody": "owner_body",
"qBodyType": "body_type",
"qConstructionFilter": "construction",
}
def _query_expression_node(value: Any) -> dict[str, Any]:
"""Return a typed, lossless-enough representation of a source query.
``source_query.ast`` remains the source-level record, including parser line
numbers. This second representation makes set boundaries and filters
explicit so a later resolver can interpret them without re-parsing
FeatureScript text. Unknown calls stay opaque instead of being flattened
into a nearby supported topology query.
"""
if isinstance(value, Call):
name = value.name
args = value.args
if name in _SET_COMBINATORS:
operands = args[0] if len(args) == 1 and isinstance(args[0], list) else args
return {
"node": "set",
"operator": _SET_COMBINATORS[name],
"operands": [_query_expression_node(item) for item in operands],
}
if name in _FILTERS:
return {
"node": "filter",
"filter": _FILTERS[name],
"input": _query_expression_node(args[0]) if args else {"node": "literal", "value": None},
"arguments": [_query_expression_node(item) for item in args[1:]],
}
if name == "makeQuery":
return {
"node": "topology_query",
"owner": _query_expression_node(args[0]) if len(args) > 0 else {"node": "literal", "value": None},
"topology_type": _query_expression_node(args[1]) if len(args) > 1 else {"node": "literal", "value": None},
"entity_type": _query_expression_node(args[2]) if len(args) > 2 else {"node": "literal", "value": None},
"arguments": [_query_expression_node(item) for item in args[3:]],
}
if name == "qCreatedBy":
return {
"node": "created_by",
"owner": _query_expression_node(args[0]) if args else {"node": "literal", "value": None},
"arguments": [_query_expression_node(item) for item in args[1:]],
}
if name in {"sQuery", "sketchEntityQuery"}:
return {
"node": "source_entity",
"sketch": _query_expression_node(args[0]) if len(args) > 0 else {"node": "literal", "value": None},
"entity_type": _query_expression_node(args[1]) if len(args) > 1 else {"node": "literal", "value": None},
"entity": _query_expression_node(args[2]) if len(args) > 2 else {"node": "literal", "value": None},
"arguments": [_query_expression_node(item) for item in args[3:]],
}
if name == "qSketchRegion":
return {
"node": "sketch_region",
"sketch": _query_expression_node(args[0]) if args else {"node": "literal", "value": None},
"arguments": [_query_expression_node(item) for item in args[1:]],
}
return {
"node": "opaque_call",
"name": name,
"arguments": [_query_expression_node(item) for item in args],
}
if isinstance(value, list):
return {"node": "list", "items": [_query_expression_node(item) for item in value]}
if isinstance(value, dict):
return {
"node": "map",
"entries": [
{"key": str(key), "value": _query_expression_node(item)}
for key, item in value.items()
],
}
if value is None or isinstance(value, (str, float, bool, int)):
return {"node": "literal", "value": value}
return {"node": "literal", "value": str(value)}
def query_expr(value: Any) -> dict[str, Any]:
"""Produce the versioned CDSL query-expression contract for one query."""
return {"version": "1.0", "root": _query_expression_node(value)}
def parse_query(value: Any) -> QueryInfo:
info = QueryInfo(ast=query_ast(value))
for call in walk_calls(value):
info.calls.append(call.name)
if call.name in {"qUnion", "qIntersection", "qSubtraction", "qAdjacent"}:
if call.name in {"qUnion", "qIntersection", "qSubtraction"}:
info.query_combinators.append(call.name)
if call.name in {"qBodyType", "qOwnerBody"}:
info.body_scope.append(call.name)
if call.name in {"TDD", "trueDependencyDisambiguation"}:
info.disambiguation.append(call.name)
if call.name in {"qBodyType", "qOwnerBody", "qAdjacent"}:
if call.name in {"qBodyType", "qOwnerBody", "qAdjacent", "qConstructionFilter"}:
info.filters.append(call.name)
if call.name in {"makeQuery", "qCreatedBy"} and call.args:
owner = symbolic_string(call.args[0])
@@ -76,6 +168,11 @@ def parse_query(value: Any) -> QueryInfo:
definition = next((arg for arg in call.args if isinstance(arg, dict)), {})
if isinstance(definition.get("isStart"), str): info.is_start = definition["isStart"].lower() == "true"
elif "isStart" in definition: info.is_start = bool(definition["isStart"])
elif call.name == "qCreatedBy" and len(call.args) > 1 and info.topology_type is None:
# qCreatedBy is itself a typed FeatureScript query. Keep its
# requested kind so consumers can distinguish a datum plane
# from a point without inspecting feature IDs or geometry.
info.kind = str(call.args[1]).lower()
if call.name in {"sQuery", "sketchEntityQuery"} and len(call.args) >= 3:
sketch = symbolic_string(call.args[0]); info.source_sketch = sketch.split(".", 1)[0]
if info.topology_type is None: info.kind = str(call.args[1]).lower()