# CADQL: 基于 BRep Graph 的结构化查询语言设计 ## 文档状态 - 状态:Proposed; transitional scoped semantics implemented - 目标版本:CADQL Query IR 1.0 - 目标项目:SimpleCADAPI 2.x - 主要影响模块:`ql.py`、`core.py`、`topology.py`、`graph.py`、`serializer.py`、各 translator backend - 非目标:在第一阶段引入 GraphQL Server、HTTP API 或第三方 GraphQL runtime ### 当前实现边界 当前代码尚未实现本文定义的完整 CADQL Query IR。已落地的过渡 subset 仅包括现有 `ShapeSelector` 上的 scoped tag predicate、typed tracking predicates、source-preserving `TagBinding` assignment,以及 graph/model schema `2.0` 中的 semantic replay。该 subset 不等于 typed graph-pattern evaluator、完整 Topology Evolution Graph、named-port schema `3.0` 或跨 backend provider parity。 因此,下文 Phase 1-7 仍是 proposed roadmap;已实现的 tagging/tracking subset 应按 `design-docs/cadql-tagging-and-auto-semantics.md` 的 bounded transition 状态理解。 ## 1. 摘要 SimpleCADAPI 需要一套以 BRep topology graph 为基础、同时支持几何属性和 semantic tags 的结构化查询语言。该查询语言必须满足: 1. 查询意图能够进入 canonical operation graph,并可序列化、重放和翻译。 2. 查询结果不依赖未定义的 BRep 枚举顺序。 3. 几何条件具有明确的单位、容差和方向语义。 4. semantic tag 查询能够区分 local、inherited、effective 和 lineage scope。 5. 查询的 cardinality 和歧义策略是正式 contract,而不是调用端的临时检查。 6. Python replay、FreeCAD translator 和未来 translator 使用相同查询语义。 7. 查询意图与一次执行产生的几何 fingerprint/evidence 分离。 本方案借鉴 GraphQL 的 schema、类型验证、variables、fragments、selection set 和结构化错误,但不直接使用 GraphQL 作为核心执行模型。Canonical contract 是一个强类型、JSON 可序列化的 CAD Query IR。GraphQL-like 文本、Python builder、GUI 和 LLM 都只是 Query IR 的前端。 ## 2. 背景与问题 当前 selection 体系由多套机制叠加形成: - `get_edges(index)`、`get_faces(index)` 等 indexed getter。 - `ShapeSelector` 和 QL predicate/order/cardinality。 - `select_edges_by_tag`、`select_faces_by_tag`。 - `make_select_rvertex/redge/rwire/rface/rsolid` graph node。 - `geo_selector` 几何 fingerprint。 - `TopoRef`、legacy enumeration index 和 selector hint fallback。 - feature params 中的 `selected_edge_node_ids`、`selected_face_node_ids`、indices 和 refs。 这些机制目前没有统一的一等数据模型,造成以下问题: - QL query 经常在 record time 被 resolve,然后降级为 geometry-only selection node,原始 semantic intent 丢失。 - `TopoRef.topo_id` 是 implementation-defined,不适合作为 durable topology naming。 - Python replay 和 FreeCAD translator 使用不同的 geo scoring、阈值和 normal 方向规则。 - 对称或重复几何中的同分候选不会可靠地报告歧义。 - selection node、query、projection、cardinality 和 fallback strategy 混在同一组 params 中。 - backend 很难声明自己支持哪些 query predicate 和 traversal relation。 因此,下一代查询系统不能继续在当前 `ql.py` 上零散增加 helper。需要先定义稳定的 Query IR、类型系统和执行语义。 ## 3. 设计决策 ### 3.1 决策一:Canonical contract 是 Query IR,不是文本语法 Canonical operation graph 中保存 JSON Query IR,不保存 Python callable,也不把 GraphQL 字符串作为唯一事实来源。 原因: - JSON IR 更容易做 schema migration 和 strict validation。 - translator 不需要嵌入 GraphQL parser。 - Python builder、GraphQL-like text、GUI 和 LLM 可共享同一个执行器。 - Query IR 可以精确表示 typed graph pattern、单位、容差和 cardinality。 ### 3.2 决策二:GraphQL-like syntax 是推荐文本前端 借鉴 GraphQL: - 类型化 schema。 - 字段和 input object。 - variables。 - fragments。 - validation before execution。 - result projection。 - structured diagnostics。 不照搬 GraphQL: - 不使用 GraphQL resolver 自由定义字段语义。 - 不使用 GraphQL null propagation 表示选择失败。 - 不依赖 GraphQL list 顺序。 - 不把 selection set 与 CAD entity match 混为一谈。 - 不把 mutation/subscription 引入 CADQL 1.0。 ### 3.3 决策三:查询对象是 BRep Topology Graph 必须区分三种图: | 图 | 职责 | | --- | --- | | Operation Graph | 描述模型如何构建,并提供 query scope 和 provenance。 | | BRep Topology Graph | 描述当前 output 中 topology entities 及其 contains/boundary/incident/adjacent 关系。 | | Topology Evolution Graph | 通过 EntityVersion/TopoUseVersion/Change 描述 preserved/modified/generated/deleted、split、merge 和 generation。 | CADQL 在 Current BRep Graph 与 Topology Evolution Graph 的联合视图上执行,可以通过 provenance relation 连接 Operation Graph。 ### 3.4 决策四:Query intent 是主身份,Evidence 是辅助信息 Query intent 描述用户为什么选择实体,例如: ```text 选择带 role.mounting_surface tag、surface type 为 plane、法向朝 +Z 的唯一 face ``` Evidence 描述某次执行如何得到结果,例如: ```text 匹配了 face_7;area=245;normal=[0,0,1];唯一候选;resolver=ocp ``` Replay 必须先重新执行 Query intent。Evidence 用于 drift diagnosis 和显式 fallback,不是 primary selector。 ### 3.5 决策五:违反唯一性默认失败 当 feature query 的 cardinality 要求唯一实体,而匹配结果不唯一时必须失败,不允许静默选择 BRep 列表中的第一个实体。多实体选择通过 `EXACTLY`、`AT_LEAST` 等 cardinality 正式声明。 ### 3.6 决策六:查询定义与 graph binding 分离 Query IR 描述 traversal、predicate、ordering、cardinality 和 projection,不保存具体 operation node ID。查询作用域通过执行时 binding 提供: - Python 中由 `cadql.select(cadql.Face, in_=body)` 的 `in_` 参数建立 runtime binding。 - Operation Graph 中由 query node 的 `scope` typed input port 建立 binding。 - 文本查询中由 variable 建立 binding。 因此,相同 Query IR 可以绑定到参数变化后重建的 operation output,`query_hash` 也不会因为 node ID 改变。持久化对象由 `query` 和 `bindings` 两部分组成。 ### 3.7 决策七:Canonical query 是 typed graph pattern 很多实际选择不是“某条 edge 的长度或中心是多少”,而是“当前拓扑实体在空间结构和建模历史中扮演什么角色”。Boolean、fillet、chamfer、shell、cut 和 pattern 都会产生 split、merge、generated、modified 和 preserved entities,不能为每种 operation 发明一个 selection helper。 Canonical CADQL 因此采用统一的 typed graph pattern: - typed variable 表示当前或历史 topology entity、oriented topology use、change 或 operation。 - topology relation 表示 contains、boundary、incident、adjacent 等当前空间结构。 - lineage relation 表示一个实体经过任意 operation 后如何 split、merge、modify、generate 或 preserve。 - provenance relation 表示 operation、input role 和 source binding。 - semantic predicate 表示用户或系统提供的稳定意图锚点。 - geometry predicate 只作为可选约束,不是统一 identity 模型的基础。 查询最终返回某个 typed variable 的去重集合。现有 `faces().where(...).order_by(...)` 属于 legacy `ql.py`,不是 CADQL 1.0 public syntax,也不是 canonical IR。 这一形式统一表达: - box/cylinder union 后,位于两个来源 surface 交界上的 current edges。 - fillet/chamfer 后,由原 edge 分裂或生成、并与特定来源 faces 相邻的 current edges。 - 多次 feature 后,仍是某个 semantic source query 的 descendants 的 current faces。 - 不知道具体 curve/surface class 时,仅依赖 topology role 和 lineage 的选择。 CADQL 不能在完全没有 semantic tag、source query、spatial relation、lineage 或 geometry distinction 时区分数学上对称且可交换的实体。此时正确结果是集合或 `AmbiguousSelectionError`,而不是伪造稳定身份。 ### 3.8 决策八:Public API 只有一种 query 形态 CADQL 不提供 `from_shape`、`anchor`、`on`、`SurfaceContext` 等相互重叠的 query 入口。所有选择都从同一个构造器开始: ```python cadql.select(ResultType, in_=scope) ``` 其中 public `scope` 可以是 Shape、operation output 或另一个 `SelectionQuery[T]`。已经 resolved 的 `SelectionSet[T]` 不作为 public `in_` 参数;Operation Graph 内部会把 referenced query 的 output 作为 typed binding 连接到下游 query node。所有 query 使用相同的操作并返回相同的结果形态: ```text select -> where -> order_by/take -> expect -> SelectionQuery[T] SelectionQuery[T].resolve() -> SelectionSet[T] ``` 已有 query 不会被“提升”为另一种 Anchor 对象。它本身就是可持久化、可作为后续 relation source binding 的选择意图: ```python mounting_face = ( cadql.select(cadql.Face, in_=before_boolean) .where(cadql.tag("role.mounting_surface", scope="effective")) .expect_one() ) current_faces = ( cadql.select(cadql.Face, in_=after_boolean) .where( cadql.related( cadql.descends_from, from_=cadql.this, to=mounting_face, derivations=("continuation", "fragment"), depth=(1, 64), ) ) .expect_at_least(1) ) ``` 局部 surface/UV 选择也不是另一套 context API。先选 current faces,再把该 query 作为普通 relation binding: ```python outer_coedges = ( cadql.select(cadql.Coedge, in_=after_boolean) .where( cadql.related(cadql.boundary_uses, from_=current_faces, to=cadql.this) & cadql.topology.loop_role.eq("outer") ) .expect_at_least(1) ) ``` `FaceFamily` 只是 `SelectionSet[Face]`,`WorkPlaneContext` 只是绑定到单一 planar chart 的 query validation 状态,二者都不是 public object kind。一个历史 face resolve 为多个 current fragments 时,结果自然是多个 Face;初始 `.expect_one()` 与后续 `.expect_at_least(1)` 是两个独立 query 的 cardinality contract。 ### 3.9 决策九:先定义不可约的选择 case 表面上不同的 CAD 选择应先归约到最少的正交能力。CADQL 只有六种不可互相替代的 match case: | Case | 唯一新增能力 | 代表问题 | | --- | --- | --- | | 类型全集 | 只给出 result type 和 scope | 选择 body 中所有 Face。 | | 属性约束 | 对 candidate property 求 predicate | 选择带 tag、面积大于阈值或法向朝上的 Face。 | | Current topology relation/path | 匹配 snapshot 中的 typed relation 或其有界/传递闭包 | 选择 face boundary Edge、两个 faces 的共享 Edge 或相切 edge chain。 | | Oriented occurrence | 返回 TopoUse,而不是 underlying entity | 选择 face outer loop 的正向 Coedge 或 seam 的某次 use。 | | Evolution relation | 沿 Change graph 匹配历史关系 | 选择原 edge split 后的 current fragments。 | | Multi-source Change pattern | 同时约束一个 Change 的多个 input/output | 选择 box/cylinder 两个 source surfaces 共同生成的交界 edges。 | 以下不是新的 match case: - Semantic、geometry、local UV 和相对空间条件都是属性约束,只是 property namespace 不同。 - Face、Edge、Vertex 只是 result type 不同。 - Boolean、fillet、chamfer 只是 Change producer 不同。 - Union、intersection、difference 是集合代数。 - Ordering、take、group 和 cardinality 是结果代数。 - 保存、命名和 graph binding 是 query 生命周期,不改变 match 语义。 任何新增 public helper 都必须证明自己能表达一个现有六类无法表达的 case;否则只能是 lowering 到这些原语的非 canonical convenience syntax,并且不进入 1.0 public contract。 ### 3.10 典型选择 case corpus 下面每行具有不同的最小表达式或数据需求;同一行中的 operation 名称和 topology type 变化不产生新 case。 | # | 典型需求 | 唯一归约 | 需要的数据 | | --- | --- | --- | --- | | 1 | 选择 scope 中全部 Face | 类型全集 | Current BRep containment。 | | 2 | 选择带 mounting tag 的 Face | 属性约束:semantic | Semantic binding。 | | 3 | 选择半径约等于 5 mm 的圆 Edge | 属性约束:intrinsic geometry | Geometry properties、unit、tolerance。 | | 4 | 选择平行于 datum plane 的 Face | 属性约束:relative geometry | Reference binding、frame。 | | 5 | 选择已选 Face 包含的 Edge | Query scope containment | Current BRep containment。 | | 6 | 选择两个 Face 的共享 Edge | Current relation | `incident_faces`。 | | 7 | 选择与 seed Face 相邻的 Face | Current relation | `adjacent_faces`。 | | 8 | 选择与 seed 连通且保持相切的 Edge chain | Current relation path | `adjacent_edges`、step predicate、depth bound。 | | 9 | 选择 Face outer loop 中的 Edge occurrences | Oriented occurrence + property | `boundary_uses`、`loop_role`。 | | 10 | 区分 periodic Face 上同一 seam Edge 的两次使用 | Oriented occurrence | TopoUse occurrence identity。 | | 11 | 选择 planar Face 局部坐标最右侧的 Coedge | Oriented occurrence + local property | SurfaceChart、stable frame。 | | 12 | 选择 source Edge split 后的 current fragments | Evolution relation | `descends_from`、`FRAGMENT` evidence。 | | 13 | 选择由 source Face 生成的新 boundary Edge | Evolution relation | `generated_from`、`BOUNDARY` evidence。 | | 14 | 选择与某 transition Face 同一 Change 的 Edge | Evolution relation | `co_result_of` witness。 | | 15 | 选择同时由 box/cylinder sources 生成的 interface Edge | Explicit Change pattern | 多 input role、`INTERSECTION` output。 | | 16 | Source Face 已消失时选择消费它的 Change outputs | Explicit Change pattern | Historical input 和 generated outputs。 | | 17 | 选择满足 A 或 B、同时排除 C 的 Edge | Set algebra | Union/difference 和 entity identity。 | | 18 | 从多个候选中选择最高且唯一的 Face | Result ordering | Business order、tie error、take、cardinality。 | | 19 | 每个 hole loop 选择最长且唯一的 Edge | Correlated partition | Loop identity、per-partition order/cardinality。 | | 20 | 数学上完全对称且无区分依据的实体 | Cardinality/ambiguity | 返回集合,或唯一性要求下失败。 | 覆盖规则: - Case 2、3、4 的求值数据不同,但语法和结果行为完全相同,都是 `.where(property_predicate)`。 - Case 6、7、8 的 relation 不同,但语法和结果行为完全相同,都是 `.where(related(...))`。 - Case 12、13、14 的 evolution relation 不同,但仍使用同一个 `related(...)` constructor。 - Case 15、16 都需要显式匹配一个 n-ary Change;input 数量不是新的语言机制。 - Case 17、18、19、20 不增加 candidate identity 机制,只处理已匹配集合。 - Face/Edge/Vertex/Wire/Solid 的替换不产生新 case;它只触发 relation/property type validation。 ## 4. 术语 | 术语 | 定义 | | --- | --- | | Scope | 查询开始的 operation output 或已有 topology entity set;scope binding 不属于 Query IR identity。 | | Match | 根据 predicate 选择 topology entities。 | | Traversal | 沿强类型 topology/lineage relation 移动。 | | Predicate | 对 geometry、topology、semantic 或 provenance property 的条件。 | | Projection | 查询结果需要返回的字段,不决定选中哪些实体。 | | Cardinality | 对匹配结果数量的正式约束。 | | Ambiguity | 多个候选都满足 identity 或 fingerprint,无法唯一判定。 | | Evidence | 某次执行的匹配属性、候选数、path witness 和 selected refs。 | | SelectionQuery | 未执行的 typed Query IR 及其 runtime bindings;可以作为另一个 query 的 source。 | | SelectionSet | 某个 Query IR 的强类型结果集合。 | ## 5. 类型系统 ### 5.1 Topology 类型 目标类型层次: ```text TopoEntity ├── Compound ├── CompSolid ├── Solid ├── Shell ├── Face ├── Wire ├── Edge └── Vertex QueryContext ├── SurfaceChart └── FrameRef TopoUse[T] ├── FaceUse ├── WireUse ├── Coedge oriented use of Edge in a face boundary └── VertexUse ``` CADQL 1.0 的内部 BRep graph 必须完整表示: ```text Compound CompSolid Solid Shell Face Wire Edge Vertex TopoUse SurfaceChart ``` Public query return type 1.0 至少支持 Solid/Face/Wire/Edge/Vertex 和 Coedge。Coedge 是 `TopoUse[Edge]` 的公开类型名,不是新的 BRep entity。Shell/Compound/CompSolid 及其他 TopoUse 可以参与 pattern,并在 provider capability 声明后作为返回类型。SDK 当前 `TopoKind` 已有 Compound,但缺少 Shell/CompSolid;canonical selection operations 也缺少 Compound/Shell/CompSolid。Phase 1 必须补齐 IR type enum,Phase 4 再补齐 public graph outputs。 ### 5.2 Property namespace 属性必须按 namespace 分类,禁止把所有属性放入未定义语义的 dict。 #### Topology properties ```text topology.kind topology.closed topology.edge_count topology.wire_count topology.inner_wire_count topology.incident_face_count topology.adjacent_face_count topology.loop_role topology.loop_identity ``` #### Geometry properties ```text geometry.length geometry.area geometry.volume geometry.center geometry.center.x geometry.center.y geometry.center.z geometry.bbox geometry.normal geometry.tangent geometry.curvature geometry.radius geometry.curve_type geometry.surface_type ``` #### Semantic properties ```text semantic.tags semantic.metadata.. semantic.role semantic.group semantic.material ``` #### Provenance properties ```text provenance.produced_by provenance.operation_type provenance.origin_role provenance.source_node ``` #### Spatial/reference properties ```text geometry.axis.origin geometry.axis.direction geometry.location geometry.support_curve_type geometry.support_surface_type geometry.parameter_span geometry.sweep_angle ``` 这些 property 仅对适用的 entity kind 可用。例如 `geometry.axis` 可用于 cylinder/cone surface 和 circle/ellipse curve;`geometry.sweep_angle` 只适用于具有规范角参数的 trimmed conic。Property registry 必须声明适用类型,不能在不适用时返回任意默认值。 #### Local/chart properties ```text local.u local.v local.u_range.min local.u_range.max local.v_range.min local.v_range.max local.winding local.orientation ``` `local.u/v` 具有 length 或 dimensionless 参数单位,由 SurfaceChart schema 声明;不能在 query 中混合比较不同单位。`local.winding/orientation` 只适用于 Coedge/WireUse,其他 local property 的适用类型由 registry 静态验证。任何 predicate、ordering、partition 或 projection 对 `local.*` property 的引用都必须携带明确的 chart binding。Chart identity 通过 `surface_chart/use_chart` relation 表达,不再提供会自引用 chart binding 的 `local.chart_id` property。Loop role 和 identity 是纯拓扑事实,使用不依赖 chart 的 `topology.loop_role/loop_identity`。 ### 5.3 Property capability 每个 backend/evaluator 必须声明 property capability: ```json { "geometry.area": "supported", "geometry.curvature": "unsupported", "topology.loop_role": "supported" } ``` Query validation 在执行前检查所有 property 和 relation。Unsupported property 必须产生 `UnsupportedQueryCapabilityError`,不能被当作 false predicate。 ## 6. Typed Traversal Relations CADQL relation 是有限枚举,并具有 source/target 类型约束。 ### 6.1 Topology relations | Relation | Source | Target | 语义 | | --- | --- | --- | --- | | `contains_direct` | Compound/CompSolid/Solid/Shell/Face/Wire | next lower entity | 只表示直接 BRep containment。 | | `contains` | TopoEntity | lower-dimensional entity | `contains_direct` 的有界传递 path,必须指定 target type。 | | `incident_faces` | Edge | Face | 共享该 edge 的 faces。 | | `adjacent_faces` | Face | Face | 通过 edge adjacency 相邻。 | | `adjacent_edges` | Edge | Edge | 共享 vertex 的 edges;可用 step predicate 进一步要求 tangent continuity。 | | `boundary_uses` | Face/Wire | Coedge | Face 或 wire boundary 中有方向的 Edge occurrences。 | | `use_entity` | TopoUse[T] | T | Occurrence 引用的同类型 underlying entity,例如 Coedge -> Edge。 | | `use_parent` | TopoUse | TopoEntity | Occurrence 所属 parent entity。 | | `use_chart` | TopoUse | SurfaceChart | Occurrence 所属 face chart。 | | `surface_chart` | Face | SurfaceChart | Face query 绑定的 surface chart。 | | `next_coedge` | Coedge | Coedge | 同一 WireUse 中按 orientation 的下一个 occurrence。 | | `previous_coedge` | Coedge | Coedge | 同一 WireUse 中按 orientation 的上一个 occurrence。 | | `mate_coedge` | Coedge | Coedge | 邻接 FaceUse 上引用同一 underlying Edge 的 occurrence。 | | `starts_at` | Coedge | VertexUse | Coedge 在 parent orientation 下的起始 VertexUse。 | | `ends_at` | Coedge | VertexUse | Coedge 在 parent orientation 下的结束 VertexUse。 | | `belongs_to_loop` | Coedge | WireUse | Coedge 所属的 boundary loop occurrence。 | `boundary_uses(Face/Wire, Coedge)` 是 `FaceUse -> WireUse -> Coedge` occurrence chain 的规范投影。它保留每个 parent occurrence,不按 underlying Edge 去重;因此 seam Edge 在同一 Face 中仍返回两个 Coedges。需要显式遍历中间 occurrence 时使用 `use_parent`/`belongs_to_loop`,不增加另一套 `coedges` relation。 ### 6.2 Lineage relations Lineage 不能只是一组从 child 指向 parent 的模糊 ref。Canonical Topology Evolution Graph 使用四类 node: ```text EntityVersion 某个 operation output snapshot 中的 topology entity TopoUseVersion 某个 snapshot 中带 parent/orientation/occurrence 的 topology use Change 一次 operation 内的一项 topology evolution event Operation Operation Graph 中的建模 operation ``` 基本关系: ```text EntityVersion -[INPUT_TO {input_port, role}]-> Change Change -[OUTPUT {event, derivation}]-> EntityVersion TopoUseVersion -[USE_INPUT_TO {input_port, role}]-> Change Change -[USE_OUTPUT {event, derivation, order_interval}]-> TopoUseVersion TopoUseVersion -[USE_ENTITY]-> EntityVersion TopoUseVersion -[USE_PARENT]-> EntityVersion Change -[PERFORMED_BY]-> Operation EntityVersion -[MEMBER_OF]-> Snapshot ``` `event` 沿用宏观结果分类: ```text PRESERVED MODIFIED GENERATED DELETED ``` `derivation` 使用与具体 feature 名称无关的演化分类: | Derivation | 含义 | | --- | --- | | `CONTINUATION` | 同一拓扑角色继续存在,可能参数或位置改变。 | | `FRAGMENT` | 一个 parent 被 split/trim 后产生同维度片段。 | | `MERGE` | 多个同维度 parents 合并为一个 output。 | | `INTERSECTION` | 由两个或多个 support entities 的相交生成。 | | `BOUNDARY` | 由高一维结果或修改区域产生的新 boundary。 | | `REPLACEMENT` | 旧实体被新的同角色实体替换,但不是可证明的 fragment。 | | `UNKNOWN` | Backend 只知道 parent/child,无法给出更强分类。 | 例如原 edge 经 chamfer/fillet 后被分裂,新的同维 edge 使用 `FRAGMENT`;feature 新建的过渡面 boundary edge 使用 `BOUNDARY`;boolean 两个 surface 的交线使用 `INTERSECTION`。这些分类描述 BRep 演化事实,不编码 `fillet`、`chamfer` 或 `union` 名称。 一个 Change 可以有多个 input 和 output,因此能自然表示 split 和 merge,不需要伪造一对一 durable topology ID。`order_interval` 是可选的 source-use 参数区间;只有 backend 能证明 occurrence ancestry 和 orientation 时才记录。Lineage relation 只能在 model graph 提供相应 change evidence 时使用;缺少 evidence 不能退化成几何猜测。 ### 6.3 Relation quantifiers Predicate 可以对子关系做 `exists`、`all` 和 `count`: ```json { "relation": "incident_faces", "from": "edge", "to": "source_face", "relation_quantifier": {"kind": "at_least", "value": 1} } ``` `source_face` 必须是已声明 variable;其 `descends_from` source binding 由同一个 `and` pattern 中的独立 path constraint 表达。 Count 形式: ```json { "relation": "incident_faces", "relation_quantifier": "count", "where": {"property": "geometry.surface_type", "eq": "plane"}, "cardinality": {"kind": "exactly", "value": 1} } ``` Quantifier 的 `where` 在 relation target type 上静态验证。`all` 对空 relation 返回 false,避免 vacuous truth 导致意外匹配。 Python `related(...)` 的 `from_`/`to` endpoint 可以绑定单个 variable 或 `SelectionQuery[T]`。Python 可以省略默认 endpoint quantifier;normalized IR 必须显式携带它: ```text ANY_MEMBER 至少与 binding 中一个 member 满足 relation;默认值。 ALL_MEMBERS 与 binding 中每个 member 满足 relation;空 binding 返回 false。 EXACTLY(n) 恰好与 n 个 members 满足 relation。 AT_LEAST(n) 至少与 n 个 members 满足 relation。 AT_MOST(n) 至多与 n 个 members 满足 relation。 ``` 默认值永远是 `ANY_MEMBER`,不随 relation 名称、方向或 endpoint cardinality 改变。Python 省略默认值与显式写 `from_quantifier="any_member"`/`to_quantifier="any_member"` 完全等价;Canonical IR 总是显式保存 normalized quantifier。 ### 6.4 Relation validation 以下是合法 traversal: ```text Solid -> contains_direct -> Shell Face -> contains_direct -> Wire Wire -> contains_direct -> Edge Edge -> contains_direct -> Vertex Face -> adjacent_faces -> Face ``` 以下必须在 validation 阶段失败: ```text Vertex -> contains_direct -> Face Edge -> boundary_uses Solid -> incident_faces ``` ### 6.5 BRep graph snapshot construction Provider 对每个 scope output 构建一个只读 BRep graph snapshot: 1. Scope root 本身是 graph node,不只索引它的 children。 2. 每个拓扑实体以 `(kind, entity_token)` 在该 snapshot 内去重。 3. `contains_direct` 保存 direct containment edge;`contains` 是它的 typed path projection,不重复存边。 4. `boundary_uses` 连接 parent 和有方向 occurrence,`use_entity` 再连接 underlying entity;`incident` 和 `adjacent` 从共享 underlying boundary entity 推导。 5. Face adjacency 默认是共享 Edge;仅共享 Vertex 不算 face adjacency。 6. Seam edge 在同一 face 中可能出现两次,但 entity graph node 只有一个;需要 occurrence 信息的查询使用 `TopoUse/Coedge`,不假装 Edge entity 能表达方向不同的两次使用。 7. Snapshot identity 只在一次 scope evaluation 内有效。跨 replay identity 由 Query intent 和 Evidence 处理,不持久化 `entity_token`。 `related(...)` traversal 对 entity set 逐个展开、合并并按 entity identity 去重。需要保留多条 path multiplicity 的 path query 不属于 CADQL 1.0。 ### 6.6 Generic feature-evolution semantics CADQL 不定义 boolean、fillet 或 chamfer 专用 relation。Provider 将每次 operation 的 topology history 规范化为 Change graph: ```text input EntityVersion(s) -> Change -> output EntityVersion(s) ``` Input edge 必须带 role: ```text subject 被延续、切分或替换的实体 target 用户明确选中并驱动 feature 的实体 support 决定新实体位置或边界的支撑实体 tool boolean/tool operand 中的实体 context 参与 change,但没有更强可证明角色的实体 ``` Output edge 必须带 `event` 和 `derivation`。由此可统一表达: - Boolean 交界 edge:同一个 Change 有来自两个 operands 的 support/tool inputs,output derivation 为 `INTERSECTION`。 - 原 edge 被 split:原 edge 是 subject/target input,多个 current edges 是 `FRAGMENT` outputs。 - Fillet/chamfer 新 boundary edges:原 target edge 和 adjacent support faces 是 inputs,新 edges 是 `BOUNDARY` outputs。 - 原 face 被 trim:原 face 是 subject input,current face 是 `FRAGMENT` 或 `CONTINUATION` output。 - 多次 feature 后追踪:沿 Change graph 对 `descends_from` 或更宽的 `depends_on` 做传递 path match。 规范 evolution relation 只有以下五个;没有 `fragments_of`、`generated_by`、`intersects_sources` 等 aliases: | Relation | Source | Target | Depth | 含义 | | --- | --- | --- | --- | --- | | `descends_from` | current Entity/TopoUse | bound historical same-kind Entity/TopoUse | 1..n | Source 是 target 的 continuation/fragment/merge/replacement descendant。 | | `generated_from` | current Entity/TopoUse | bound historical Entity/TopoUse | 1..n | Source 由 target 参与 intersection/boundary generation。 | | `depends_on` | current Entity/TopoUse | bound historical Entity/TopoUse | 1..n | Source 的 causal Change path 中包含 target,不保证同维度或材料连续性。 | | `co_result_of` | current Entity/TopoUse | current Entity/TopoUse | 1 | 两者是同一个 Change 的 outputs。 | | `changed_with` | current Entity/TopoUse | bound historical Entity/TopoUse | 1 | Source 的 Change 同时消费 target。 | CADQL 1.0 不提供 `interface_between(a, b)`、`descendants_of(a)` 等同义 helper。它们分别使用 `change(...)` 和 `related(...)` 表达,避免同一 graph constraint 出现第二种 public 写法。 如果 backend 没有完整 Change input/output evidence,相关 pattern 产生 `UnsupportedQueryCapabilityError`,不能退化为“找最接近的 curve”。 对 box/cylinder union 场景,不应先假定交界 edge 是 circle。Cylinder 与 oblique box plane 的支撑交线通常为 ellipse/conic,trim 后只是其中一段。CADQL 应先按两个来源 bindings 共同参与的 `INTERSECTION` Change 选择 current edges;curve type、sweep angle 或 length 只是可选的二次约束。 ### 6.7 Entity 和 oriented use BRep 中同一个底层 entity 可以在不同 parent 中以不同 orientation 出现。Canonical graph 区分: ```text TopoEntityVersion snapshot 内未定向的拓扑实体 TopoUse entity 在某个 parent boundary 中的一次有方向 occurrence ``` `contains`、`incident_faces` 等 entity-level relation 用于大多数 selection;`boundary_uses`、`next_coedge`、`starts_at` 和 `ends_at` 用于依赖方向或 seam occurrence 的 query。`TopoEntityVersion` 可按 kernel `IsSame` 语义去重,`TopoUse` identity 则包含 parent、orientation 和 occurrence ordinal。 Normal、tangent、edge endpoint order 等 oriented property 必须在 `TopoUse` 上求值,或显式指定相对哪个 parent 求 oriented view。不能在去除 orientation 的 entity 上声称存在唯一方向。 ### 6.8 Query composition 和 relation direction `select(T, in_=scope)` 始终只做两件事:建立 candidate universe,并声明 result type。它不隐式进入 history、不自动加入邻面,也不根据调用链切换语义。 `in_` 只绑定 candidate domain,不 lower 为 graph relation: - Shape 或 operation output:candidate domain 是该 current snapshot。 - `SelectionQuery[P]`:candidate domain 是其 resolved `SelectionSet[P]` 覆盖的 current subgraphs。 - 请求 `TopoEntity` 类型时,subgraph 包含 scope root 本身和沿 `contains_direct` 可达的 descendants。 - 请求 `Coedge` 时,subgraph 包含 scope 中 Face/Wire 的 `boundary_uses` targets。 - 所有 candidate 合并后按 entity/use identity 去重。 - 若目标不是 candidate-domain membership,而是 incident、adjacent、use/entity 或 history 关系,必须写入 `.where(...)`。 Candidate-domain membership 是 evaluator seed 规则,不属于 `related(...)` registry,也不进入 query identity。Query identity 保存 result type;binding identity 保存具体 scope。这样 `select(Solid, in_=solid)` 可以选择 root,`select(Coedge, in_=body)` 也不需要伪造 `contains(body, coedge)`。 所有 graph relation 使用同一个 predicate constructor: ```python cadql.related(relation, from_=source, to=cadql.this) cadql.related(relation, from_=cadql.this, to=target) ``` `cadql.this` 表示当前被测试的 candidate。Relation schema 静态检查 source/target type,因此 relation direction 不靠方法名猜测。一个 `related(...)` predicate 最多允许一个 endpoint 是 `SelectionQuery[T]`,另一个必须是 `cadql.this` 或局部 scalar variable。Set endpoint 默认量词始终是 `ANY_MEMBER`;需要其他行为时按 endpoint 方向使用 `from_quantifier=` 或 `to_quantifier=`。Relation target 自身的 exists/count 语义使用独立的 `relation_quantifier` IR field,不复用 endpoint quantifier 名称。 示例: ```python # Face -> boundary_uses -> Coedge coedges = ( cadql.select(cadql.Coedge, in_=body) .where(cadql.related(cadql.boundary_uses, from_=faces, to=cadql.this)) ) # Coedge -> use_entity -> Edge edges = ( cadql.select(cadql.Edge, in_=body) .where(cadql.related(cadql.use_entity, from_=coedges, to=cadql.this)) ) # Edge -> incident_faces -> Face shared_edges = ( cadql.select(cadql.Edge, in_=body) .where( cadql.related(cadql.incident_faces, from_=cadql.this, to=face_a) & cadql.related(cadql.incident_faces, from_=cadql.this, to=face_b) ) ) ``` Python `cadql.related(...)` 可以省略 `depth`,唯一默认值是 `depth=1`;normalized IR 必须显式保存该值。有界或传递 traversal 使用同一个 constructor,并提供 `depth=(min, max)`。Normalize 阶段把默认/显式 `depth=1` lower 为相同 canonical `relation` constraint,把范围 lower 为 canonical `path` constraint。 Path 的 `step_where` 是可选的 pairwise predicate,显式绑定 `cadql.step.from_`、`cadql.step.to` 和 relation witness。例如 `adjacent_edges` witness 是共享 Vertex: ```python cadql.related( cadql.adjacent_edges, from_=seed_edges, to=cadql.this, depth=(0, 64), step_where=cadql.step.tangent_continuous( angle_tolerance=cadql.deg(0.1), ), ) ``` 普通 `where=` 只允许 target-local predicate,不能表达 pairwise tangency。Provider 不支持 relation witness 或 oriented tangent 时必须报告 capability error。 Canonical path IR 保存同一信息: ```json { "path": { "from": "seed_edge", "relation": "adjacent_edges", "to": "result", "min_depth": 0, "max_depth": 64, "from_quantifier": "any_member", "step_where": { "predicate": "tangent_continuous", "relative_to": "relation_witness", "angle_tolerance": {"value": 0.1, "unit": "deg"} } } } ``` `tangent_continuous` 只适用于 `adjacent_edges` path。`relative_to: relation_witness` 表示在共享 Vertex occurrence 处比较进入/离开 tangent,因此满足 oriented-property 的显式 context 要求。 ### 6.9 Local 2D topology naming 局部 surface 选择的稳定性主要来自 query-selected Face 与其局部拓扑结构,而不是 UV 数值。每个 face chart 暴露: ```text FaceUse -> outer/inner WireUse -> ordered cyclic Coedge sequence -> start/end VertexUse -> mate Coedge on neighboring FaceUse ``` Outer/inner 是 `topology.loop_role` property,不是第二套 relation。局部 traversal 只使用 6.1 已注册的 `next_coedge`、`previous_coedge`、`mate_coedge`、`starts_at`、`ends_at` 和 `belongs_to_loop`。 当原 boundary edge 被 split 时,Topology Evolution Graph 必须把 current coedge fragments 映射到 source coedge query,并保留在 source orientation 下的有序覆盖: ```text source CoedgeUse -> fragment 0 -> fragment 1 -> fragment 2 ``` 该顺序来自 `TopoUseVersion USE_OUTPUT.order_interval`,不是 current backend edge enumeration。若 provider 只能证明 underlying edge fragments 属于同一 source query、无法证明 use ancestry/orientation/order,则仍可返回无序 fragment set,但任何 `next_coedge` 或 ordered-range query 必须产生 `UnsupportedQueryCapabilityError`。 常见无具体几何 query: ```text selected face 的 outer loop edges selected face 某个有稳定区分依据的 inner loop current descendants selected coedge 被 split 后的全部 current fragments 与 selected face 相邻、且由同一个 Change 新生成的 faces selected face boundary 上由 tool source 参与生成的 edges ``` “第 2 个 inner loop”只有在 loop 本身有 semantic tag 或稳定 ordering key 时才可持久化。Raw current loop enumeration 仍不是 durable identity。 ### 6.10 Surface chart 只是显式 predicate binding CADQL 不创建 SurfaceContext/WorkPlaneContext 对象。Local property 必须通过 `in_chart=` 显式绑定到一个 Face query: ```python right_side = ( cadql.select(cadql.Coedge, in_=body) .where( cadql.related(cadql.boundary_uses, from_=mounting_face, to=cadql.this) & cadql.local.u.approximately( cadql.local.u_range.max, in_chart=mounting_face, ) ) .expect_one() ) ``` 规则: - `mounting_face` 必须静态为 `SelectionQuery[Face]`。 - 若 local predicate 需要单一 chart,binding query 必须具有 `ONE` cardinality。 - 多个 faces 可按 chart 分组后分别执行 local predicate,但不同 chart 的 UV 不得直接比较或全局排序。 - Planar frame 来自 source Face query 的 operation/frame binding,不从 current trimmed bbox 重新猜测。 - Chart 只沿可证明的 `CONTINUATION`/`FRAGMENT` use history 继承。`MERGE`/`REPLACEMENT` 默认不继承,必须用新的 Face query 和显式 frame 重新定义 local predicate。 邻接和 Change neighborhood 不再是 context expansion mode。它们分别写成 `related(incident_faces, ...)`、`related(evolution_relation, ...)` 或 multi-source `change(...)` constraints。因此候选范围不会因一个隐式 expansion 改变。 ## 7. Predicate Algebra ### 7.1 Boolean predicates ```text {"and": [, ]} {"or": [, ]} {"not": } ``` 空 `and`、空 `or` 在 strict mode 下非法,避免产生不直观的全选或空选。 ### 7.2 Scalar comparison ```text eq ne lt lte gt gte between approximately ``` 浮点 property 禁止默认 exact equality。对 length/area/volume/radius 使用 `approximately` 或范围谓词。 ### 7.3 Vector and direction predicates ```text parallel_to same_direction_as opposite_direction_to perpendicular_to angle_to ``` 语义定义: - `parallel_to` 允许同向和反向。 - `same_direction_as` 只允许同向。 - `opposite_direction_to` 只允许反向。 - 所有方向 predicate 必须指定或继承 angle tolerance。 - curved face 的 normal 查询必须指定 sampling policy;CADQL 1.0 默认仅允许 planar face 的 stable normal predicate。 ### 7.4 String和enum predicates ```text eq ne in exists ``` `geometry.surface_type`、`geometry.curve_type` 使用 enum,不使用 backend-specific Python class name。 ### 7.5 Tag predicates Tag 查询格式: ```json { "property": "semantic.tags", "tag": { "scope": "effective", "op": "eq", "value": "role.mounting_surface" } } ``` Tag scope: | Scope | 含义 | | --- | --- | | `local` | 直接挂在 entity 上。 | | `inherited` | 从 topology parent 或 model entity 传播。 | | `effective` | local 与 inherited 的并集。 | | `lineage` | 从 topology lineage ancestor 传播。 | Python `cadql.tag(value, scope="effective")` 的默认 scope 是 `effective`,builder normalization 始终把默认值显式写入 IR。其他 scope 必须显式传参。 当前 SDK 仅保存扁平化 `_tags: set[str]`,传播后无法判断一个 tag 是 local 还是 inherited。因此迁移前只有 `effective` 可被可靠执行;请求 `local`、`inherited` 或 `lineage` 必须得到 `UnsupportedQueryCapabilityError`,不能把 effective tags 冒充为其他 scope。 Phase 4 必须增加保留来源的 canonical tag binding: ```json { "tag": "role.mounting_surface", "assignment_node": "node_tag_mounting_surface", "scope_binding": "body", "target": { "kind": "selection_query", "query_hash": "sha256:...", "binding_hash": "sha256:..." }, "attachment": "local", "propagation": "downward" } ``` `effective` 是 local bindings 与适用 inherited bindings 的计算结果,不再通过复制字符串丢失来源。Lineage tags 根据 tag bindings 和 Topology Evolution Graph 在 query-time 计算。 匹配模式: ```text eq in prefix pattern exists ``` `pattern` 只支持 dot-token wildcard,例如 `role.*` 和 `anchor.datum.*`。CADQL 1.0 不支持任意 regex。 ### 7.6 Metadata predicates Metadata 查询必须指定 namespace: ```json { "property": "semantic.metadata.manufacturing.finish", "eq": "ground" } ``` 未指定 namespace 的自由 key 查询在 strict mode 下非法。 ## 8. Units 和 Tolerance ### 8.1 Quantity 所有有量纲 value 使用: ```json { "value": 100.0, "unit": "mm2" } ``` CADQL 1.0 基础单位: ```text length: mm, cm, m, in area: mm2, cm2, m2, in2 volume: mm3, cm3, m3, in3 angle: deg, rad ``` Query IR 规范化阶段转换为 canonical units: ```text length -> mm area -> mm2 volume -> mm3 angle -> rad ``` ### 8.2 Tolerance QueryOptions 提供默认 tolerance profile: ```json { "linear_abs": {"value": 1e-6, "unit": "mm"}, "linear_rel": 1e-9, "angular_abs": {"value": 1e-6, "unit": "rad"}, "area_abs": {"value": 1e-9, "unit": "mm2"}, "area_rel": 1e-8, "volume_abs": {"value": 1e-12, "unit": "mm3"}, "volume_rel": 1e-8 } ``` Predicate 可以覆盖默认 tolerance。Scalar approximate equality 统一使用: ```text abs(a - b) <= max(abs_tolerance, rel_tolerance * max(abs(a), abs(b))) ``` Ordering key 的业务同分也使用对应量纲的同一 tolerance,而不是 exact float equality。NaN、infinity 或 property unavailable 在 strict query 中产生 `PropertyUnavailableError`,不能参与排序。Backend 不能自行改变 tolerance 语义,只能声明数值能力低于请求精度并拒绝执行。 ### 8.3 Coordinate frame `geometry.center`、`geometry.bbox`、`geometry.normal`、`geometry.tangent` 以及 query 中的 point/vector 都必须在同一显式 frame 中解释。QueryOptions 默认使用 world frame: ```json { "frame": {"kind": "world"} } ``` 可选 frame: ```text world operation_context frame_ref ``` `operation_context` 使用 scope producer 记录的 coordinate-system snapshot;`frame_ref` 引用 model 的 Frame Graph。Provider 必须先把属性转换到 query frame 再求值。Face entity 本身没有唯一 oriented normal;normal/tangent predicate 必须通过 `relative_to=` 绑定 parent Solid/Shell shape、query 或明确的 FaceUse。Provider 在该 parent occurrence 中计算 oriented normal,再转换到 query frame。 ## 9. Cardinality 和 Ordering ### 9.1 Cardinality 正式类型: ```text ANY ONE OPTIONAL_ONE EXACTLY(n) AT_LEAST(n) AT_MOST(n) BETWEEN(min, max) ``` Feature selection 不允许使用隐式 `ANY`。例如 fillet 的 edge query 必须明确至少一条 edge,remove-face shell query 必须明确预期数量或范围。 Cardinality 只验证完整 pattern、order 和 take 求值后的结果,不执行排序、截断或“选最佳候选”。`.expect_one()` 对两个匹配实体必须失败,即使 query 存在 `order_by`。 Validation outcome: | 条件 | Error | | --- | --- | | `actual == 0` 且 minimum 大于 0 | `NoMatchError` | | `0 < actual < minimum` | `SelectionCardinalityError` | | `actual > maximum` | `AmbiguousSelectionError` | | top-N boundary 出现业务排序同分 | `AmbiguousSelectionError` | ### 9.2 Ordering `take` 必须在 `order_by` 后使用。CADQL 1.0 不提供 `first`、`last`、`offset` 或 `selection_item(index)`;这些写法要么重复 `order_by(...).take(...)`,要么重新引入不稳定 index。 合法示例: ```json { "order_by": [ {"property": "geometry.center.z", "direction": "desc"}, {"property": "geometry.center.x", "direction": "asc"}, {"property": "geometry.area", "direction": "desc"} ] } ``` 禁止使用 backend topology enumeration index 作为默认稳定排序。内部可用 deterministic final tie-breaker,但如果业务排序键全部相同且 query 要求唯一项,必须报告 ambiguity。 `take` 只能在显式 `order_by` 后使用。它默认采用 `boundary_ties: error`:如果第 N 项与第 N+1 项的全部业务排序键相同,则产生 `AmbiguousSelectionError`。内部 entity token 只能保证 deterministic materialization,不能用来打破业务并列。 ### 9.3 Set semantics 和 ambiguity Query 的自然结果始终是去重集合。Cardinality 决定该集合是否可接受: - `EXACTLY(4)` 返回四个实体是成功,不是 ambiguity。 - `ONE` 返回两个实体时是 `AmbiguousSelectionError`。 - `AT_LEAST(1)` 返回多个实体是成功。 - `take(1)` 在业务排序边界同分时是 `AmbiguousSelectionError`。 因此 CADQL 1.0 不增加独立的 `ALLOW_SET`/`ERROR` policy。`PICK_FIRST` 也不进入 canonical contract。Python 临时探索 API 可以提供显式 unsafe helper,但不得写入 canonical model graph。 ### 9.4 Result algebra Match 结束后,所有 query 共用同一结果代数: ```text distinct union / intersection / difference order_by take expect partition_by order_each_by take_each expect_each_one / expect_each_exactly ``` 规则: - 集合运算两侧必须是兼容的 `SelectionQuery[T]`,结果仍是 `SelectionQuery[T]`。 - 去重依据是 current entity identity 或 TopoUse identity,不是 geometry fingerprint。 - `order_by` 只建立业务顺序;`take` 才缩小集合。 - `expect` 只验证最终集合,不选择候选。 - 每个 operation 执行后类型都不改变,resolve 后统一返回 `SelectionSet[T]`。 `group_by()` 不进入 CADQL 1.0 public API,因为它会引入第二种 public result shape。需要“每个 group 选择一个”的 case 使用 CADQL 1.0 correlated partition stage: ```python longest_edge_per_hole = ( cadql.select(cadql.Coedge, in_=mounting_face) .where(cadql.topology.loop_role.eq("inner")) .partition_by(cadql.topology.loop_identity) .order_each_by(cadql.length.desc()) .take_each(1, boundary_ties="error") .expect_each_one() ) ``` 该 query 的 resolved result 仍是扁平 `SelectionSet[Coedge]`;partition key 和每组 cardinality 进入 evidence。若 feature 消费 underlying Edge,再用一个普通 `select(Edge).where(related(use_entity, ...))` query。Partition stages 只改变内部求值方式,不产生 `SelectionGroup` public result。 ## 10. Query IR 1.0 ### 10.1 顶层结构 ```json { "schema_version": "1.0", "variables": { "result": {"type": "face", "domain": "current"} }, "match": { "node": "result", "where": { "property": "semantic.tags", "tag": { "scope": "effective", "op": "eq", "value": "role.mounting_surface" } } }, "return": "result", "cardinality": {"kind": "one"}, "projection": ["ref"], "options": { "frame": {"kind": "world"} } } ``` ### 10.2 Variables 和 binding Variable domain: ```text current 当前 scope snapshot 中的 entity/use history scope 可达的历史 EntityVersion change Topology Evolution Graph 中的 Change operation Operation Graph node bound_source 外部 binding 提供的 entity set 或 operation output ``` `current` domain 已经表示由 `scope` binding 建立的 candidate domain;IR 不再额外生成 `contains(scope, result)` constraint。`bound_source` variable 必须通过 `from_binding` 关联 named binding,relation/path endpoint 只引用已声明 variable,不直接引用 binding name。 Query IR 外部的 runtime binding: ```json { "bindings": { "scope": { "kind": "operation_output", "node_id": "node_result", "output_slot": 0 }, "box_source": { "kind": "operation_output", "node_id": "node_box", "output_slot": 0 }, "cylinder_source": { "kind": "operation_output", "node_id": "node_cylinder", "output_slot": 0 } } } ``` Graph node 内使用 named input bindings,避免 query params 重复持有 node ID: ```json { "bindings": { "scope": {"kind": "input", "port": "scope"}, "box_source": {"kind": "input", "port": "box_source"}, "cylinder_source": {"kind": "input", "port": "cylinder_source"} } } ``` Binding source 可以是 Shape output、SelectionSet output 或 operation reference。后者用于 source operand、feature target 和 semantic source query。Binding 是 query execution identity 的一部分,但不进入 normalized pattern hash。 后续 binding 可增加: ```text selection_output assembly_component product_part explicit_entity_refs semantic_query ``` Explicit refs 只能作为 legacy/evidence binding,不能被描述为重建出的 semantic intent。 ### 10.3 Pattern constraints Canonical constraints: ```text node variable 的 property/semantic predicate relation 两个 variable 之间的一条 typed relation path 两个 variable 之间有界或传递的 typed relation path change EntityVersion/Change/Operation 的 evolution pattern and/or/not 组合 constraints exists/count 局部 variable quantifier ``` Box/cylinder union 后选择交界 edges 的完整例子: ```json { "schema_version": "1.0", "variables": { "edge": {"type": "edge", "domain": "current"}, "change": {"type": "change", "domain": "change"}, "box_face": {"type": "face", "domain": "bound_source", "from_binding": "box_source"}, "cylinder_face": {"type": "face", "domain": "bound_source", "from_binding": "cylinder_source"}, "box_current_face": {"type": "face", "domain": "current"}, "cylinder_current_face": {"type": "face", "domain": "current"} }, "match": { "and": [ { "change": { "variable": "change", "inputs": [ { "variable": "box_face", "input_port": "subjects", "role": "support", "endpoint_quantifier": "any_member" }, { "variable": "cylinder_face", "input_port": "tools", "role": "tool", "endpoint_quantifier": "any_member" } ], "outputs": [ { "variable": "edge", "derivation": ["intersection"] } ] } }, { "relation": "incident_faces", "from": "edge", "to": "box_current_face", "relation_quantifier": {"kind": "at_least", "value": 1} }, { "path": { "from": "box_current_face", "relation": "descends_from", "to": "box_face", "min_depth": 1, "max_depth": 64, "to_quantifier": "any_member" } }, { "relation": "incident_faces", "from": "edge", "to": "cylinder_current_face", "relation_quantifier": {"kind": "at_least", "value": 1} }, { "path": { "from": "cylinder_current_face", "relation": "descends_from", "to": "cylinder_face", "min_depth": 1, "max_depth": 64, "to_quantifier": "any_member" } } ] }, "return": "edge", "cardinality": {"kind": "exactly", "value": 3}, "projection": ["ref", "lineage"] } ``` 该 query 不引用 circle、ellipse、length、center 或 backend topology index。它选择的是三条曲线在联合 topology/evolution graph 中的角色。 原 edge 经任意后续 feature 分裂后,选择其 current fragments: ```json { "schema_version": "1.0", "variables": { "edge": {"type": "edge", "domain": "current"}, "source_edge": {"type": "edge", "domain": "bound_source", "from_binding": "source_edge"} }, "match": { "path": { "from": "edge", "relation": "descends_from", "to": "source_edge", "derivation": ["continuation", "fragment"], "min_depth": 1, "max_depth": 64, "to_quantifier": "any_member" } }, "return": "edge", "cardinality": {"kind": "at_least", "value": 1}, "projection": ["ref", "lineage"] } ``` 同样的 pattern 适用于 boolean split、fillet、chamfer、shell 或后续 trim;query 不检查 operation name。 ### 10.4 Public builder lowering Public builder 只有一个入口。`select/where/order_by/take/expect` normalize 后 lower 到 canonical variables + constraints: ```text select(Face, in_=body).where(P) => bind body as scope seed candidate domain Face(current), including an applicable root node predicate P(result) ``` 另一个 query 作为 `in_` 时建立其 current subgraph candidate domain: ```text select(Edge, in_=face_query) => bind face_query SelectionSet[Face] as scope seed Edge(current) from each selected Face subgraph ``` 非 containment 关系不通过改变入口表达: ```text select(Face, in_=body).where( related(adjacent_faces, from_=seed_faces, to=this) ) => bind seed_faces relation adjacent_faces(seed, result) ``` Cardinality 是顶层 postcondition,不是可插入中间的 operation。多个各自需要 cardinality contract 的 query 通过内部 `SelectionSet` graph binding 组合;public Python composition 仍传入 `SelectionQuery[T]`,不要求用户提前 `resolve()`。 ### 10.5 Projection Projection 只影响 query result/evidence,不影响 match: ```json [ "ref", "semantic.tags", "geometry.area", "geometry.center", { "property": "geometry.normal", "relative_to_binding": "body" } ] ``` Oriented property projection 必须使用结构化 item,并携带 parent-relative binding;裸字符串 `geometry.normal`/`geometry.tangent` 不合法。 Canonical feature graph 至少要求 projection 包含 `ref`。 ### 10.6 Query-to-query binding lowering Query composition 不引入 Anchor 或 Context payload。每个被引用的 query 都是一个独立 Query IR object;引用方通过 named input port 绑定其 `SelectionSet` output。 ```python source_face = ( cadql.select(cadql.Face, in_=before_boolean) .where(cadql.tag("role.mounting_surface", scope="effective")) .expect_one() ) current_faces = ( cadql.select(cadql.Face, in_=after_boolean) .where( cadql.related( cadql.descends_from, from_=cadql.this, to=source_face, derivations=("continuation", "fragment"), depth=(1, 64), ) ) .expect_at_least(1) ) ``` Lowering: ```text query source_face -> SelectionSet[Face] query current_faces input source_face: SelectionSet[Face] F: Face(current) S: Face(bound_source) F path(descends_from, derivation in {CONTINUATION, FRAGMENT}) S return distinct F ``` 局部 boundary query 同样只增加普通 bindings 和 relations: ```python coedges = ( cadql.select(cadql.Coedge, in_=after_boolean) .where(cadql.related(cadql.boundary_uses, from_=current_faces, to=cadql.this)) ) ``` 若 query 使用 `local.*` property,IR 额外声明 `SurfaceChart` variable:Face query 通过 `surface_chart` 绑定 chart,Coedge 通过 `use_chart` 绑定同一个 chart。Chart/frame evidence 属于该 query binding,不属于新的持久化 object kind。`MERGE`/`REPLACEMENT` 默认不传播 chart;validation 要求调用方提供新的 chart/frame binding。 ## 11. GraphQL-like Text Syntax 推荐文本前端示例: ```graphql query MountingFace($body: NodeRef!) { brep(node: $body) { matches: match( type: FACE where: { and: [ { semantic: { tag: { scope: EFFECTIVE matches: "role.mounting_surface" } } } { geometry: { surfaceType: { eq: PLANE } normal: { sameDirectionAs: [0, 0, 1] angleTolerance: { value: 0.1, unit: DEG } relativeTo: $body } } } ] } expect: ONE ) { ref semantic { tags } geometry { area center } } } } ``` 语法约束: - `match(type: FACE)` 负责 entity matching;文档保持为合法 GraphQL document subset,而不是发明 `match Face` 语法。 - `{ ref geometry semantic }` 是 projection selection set。 - `where` 编译为 predicate AST。 - `expect` 编译为 cardinality。 - `orderBy` 编译为 deterministic order stage。 - GraphQL fragments 只复用 projection;predicate 复用使用 typed input variables,因为标准 GraphQL fragment 不能展开 input object。 - directives 仅用于 debug/evidence 等非选择语义,不能改变 entity identity。 CADQL 1.0 parser 可后置实现。第一阶段以 Python builder 和 JSON IR 为主。 ## 12. Python Builder API 目标 API: ```python from simplecadapi import cadql mounting_face = ( cadql.select(cadql.Face, in_=body) .where( cadql.tag("role.mounting_surface", scope="effective") & cadql.surface_type.eq("plane") & cadql.normal.same_direction_as( (0.0, 0.0, 1.0), angle_tolerance=cadql.deg(0.1), relative_to=body, ) ) .expect_one() ) ``` 执行: ```python result = mounting_face.resolve() face = result.one() ``` 直接用于 feature: ```python result = shell_rsolid( body, faces_to_remove=mounting_face, thickness=2.0, ) ``` Active `GraphSession` 中,feature 必须记录 Query IR,不允许只记录 resolved concrete face。 ### 12.1 所有 query 返回同一种结果 `resolve()` 返回: ```python SelectionSet[Face] ``` 它包含: ```text entities refs evidence diagnostics query_hash ``` `SelectionSet[T]` 是所有 query 的唯一 resolved result。它携带 entities、refs 和 execution evidence;不存在针对 Anchor、Context、history query 或 property query 的其他 result class。禁止通过普通 list slicing 隐式改变 canonical query semantics。 ### 12.2 六种 match case 的统一表达 以下片段假定 `face_a`、`face_b`、`seed_edges`、`source_edge`、`box_faces` 和 `cylinder_faces` 是前面通过同一个 `cadql.select(...)` API 定义的 typed `SelectionQuery[T]` bindings。 类型全集: ```python faces = cadql.select(cadql.Face, in_=body).expect_at_least(1) ``` 属性约束,无论 property 来自 semantic、geometry、spatial 还是 local namespace,都使用 `.where(...)`: ```python mounting_faces = ( cadql.select(cadql.Face, in_=body) .where( cadql.tag("role.mounting_surface", scope="effective") & cadql.surface_type.eq("plane") ) .expect_at_least(1) ) ``` Current topology relation,单步形式: ```python shared_edges = ( cadql.select(cadql.Edge, in_=body) .where( cadql.related(cadql.incident_faces, from_=cadql.this, to=face_a) & cadql.related(cadql.incident_faces, from_=cadql.this, to=face_b) ) .expect_at_least(1) ) ``` 同一个 current topology relation case 的 path 形式: ```python tangent_chain = ( cadql.select(cadql.Edge, in_=body) .where( cadql.related( cadql.adjacent_edges, from_=seed_edges, to=cadql.this, step_where=cadql.step.tangent_continuous( angle_tolerance=cadql.deg(0.1), ), depth=(0, 64), ) ) .expect_at_least(1) ) ``` Oriented occurrence: ```python outer_coedges = ( cadql.select(cadql.Coedge, in_=body) .where( cadql.related(cadql.boundary_uses, from_=mounting_faces, to=cadql.this) & cadql.topology.loop_role.eq("outer") ) .expect_at_least(1) ) ``` Evolution relation: ```python current_fragments = ( cadql.select(cadql.Edge, in_=result) .where( cadql.related( cadql.descends_from, from_=cadql.this, to=source_edge, derivations=("continuation", "fragment"), depth=(1, 64), ) ) .expect_at_least(1) ) ``` Multi-source Change pattern: Box/cylinder union 后,不依赖 curve 类型选择交界 edges: ```python interface_edges = ( cadql.select(cadql.Edge, in_=result) .where( cadql.change( inputs=( cadql.change_input( box_faces, input_port="subjects", role="support", endpoint_quantifier="any_member", ), cadql.change_input( cylinder_faces, input_port="tools", role="tool", endpoint_quantifier="any_member", ), ), output=cadql.this, derivation="intersection", ) ) .expect_exactly(3) ) ``` `cadql.change(...)` 直接对应 canonical Change constraint。每个 `change_input(...)` 必须声明 input port、role 和 `endpoint_quantifier`;`any_member` 表示 source query 中至少一个 Face 是该 Change input,不表示整个 Face set 被当作一个 graph node。它不检查 producer operation 是否名为 union,也不检查 edge 是 circle 或 ellipse。 选择由 source edge 参与生成、但不一定与 source 同维度连续的 current boundaries,仍是普通 evolution relation: ```python generated_boundaries = ( cadql.select(cadql.Edge, in_=result) .where( cadql.related( cadql.generated_from, from_=cadql.this, to=source_edge, derivations=("boundary", "intersection"), depth=(1, 64), ) ) .expect_at_least(1) ) ``` `descends_from` 是较强的材料/拓扑延续关系;`generated_from` 和 `depends_on` 是更宽的因果关系。调用者必须选择符合意图的关系,不能把所有 parent refs 都称作 descendants。 ### 12.3 Query composition,不创建 Anchor 或 Context 先定义 source face query: ```python mounting_surface = ( cadql.select(cadql.Face, in_=body) .where(cadql.tag("role.mounting_surface", scope="effective")) .expect_one() ) ``` 在 topology-changing operation 后,显式定义 current descendants query: ```python current_mounting_faces = ( cadql.select(cadql.Face, in_=result) .where( cadql.related( cadql.descends_from, from_=cadql.this, to=mounting_surface, derivations=("continuation", "fragment"), depth=(1, 64), ) ) .expect_at_least(1) ) ``` 再通过普通 current relations 选择 boundary occurrences 和 underlying entities: ```python outer_coedges = ( cadql.select(cadql.Coedge, in_=result) .where( cadql.related( cadql.boundary_uses, from_=current_mounting_faces, to=cadql.this, ) & cadql.topology.loop_role.eq("outer") ) .expect_at_least(1) ) outer_edges = ( cadql.select(cadql.Edge, in_=result) .where(cadql.related(cadql.use_entity, from_=outer_coedges, to=cadql.this)) .expect_at_least(1) ) ``` 这里 lower 为: ```text current face descends_from mounting_surface query result AND current face boundary_uses coedge AND coedge references returned edge ``` 在 topology change 前的唯一 planar Face chart 中使用稳定局部坐标,仍然是相同 query 形态: ```python right_side = ( cadql.select(cadql.Coedge, in_=body) .where( cadql.related( cadql.boundary_uses, from_=mounting_surface, to=cadql.this, ) & cadql.local.u.approximately( cadql.local.u_range.max, in_chart=mounting_surface, ) ) .expect_one() ) ``` 如果 source face 被 split,`current_mounting_faces.resolve()` 自然返回 `SelectionSet[Face]`。如果业务要求恰好三个 fragments,就在这个 query 上写 `.expect_exactly(3)`;不需要 `FaceFamily` result type 或 `expect_faces_exactly` 专用方法。跨多个 fragments 使用 local UV 时,Coedge query 必须先 `.partition_by(cadql.use_chart)`,并在每个 partition 内绑定对应的 `ONE` Face chart;不能把 `AT_LEAST(1)` Face query 当作单一 chart。 Box/cylinder 交界的三个 box faces 不共面,也仍是普通 Face query: ```python box_faces = ( cadql.select(cadql.Face, in_=box) .expect_at_least(1) ) cylinder_faces = ( cadql.select(cadql.Face, in_=cylinder) .expect_at_least(1) ) interface_edges = ( cadql.select(cadql.Edge, in_=result) .where( cadql.change( inputs=( cadql.change_input( box_faces, input_port="subjects", role="support", endpoint_quantifier="any_member", ), cadql.change_input( cylinder_faces, input_port="tools", role="tool", endpoint_quantifier="any_member", ), ), output=cadql.this, derivation="intersection", ) ) .expect_exactly(3) ) ``` 每个 face 有自己的 SurfaceChart。上例没有跨 chart 比较 UV,只使用 Change source bindings,因此仍是 geometry-independent pattern。 ### 12.4 Indexed getter 的未来行为 `get_faces(0)` 等旧 API 暂时保留,但标记为 convenience/legacy selection。它不能被诚实地转换为 CADQL 1.0 semantic pattern,GraphSession 在迁移期继续记录 legacy materialized selection: ```text legacy_selection( basis=backend_enumeration, index=0, evidence=fingerprint ) ``` 该结构是 model schema 的 legacy extension,不属于 Query IR 1.0。文档和 examples 逐步迁移到 semantic/topology/lineage CADQL。 ## 13. Operation Graph 集成 ### 13.1 新 canonical operation 建议增加: ```text query_brep_rselection ``` 只定义这一个 canonical query operation。Result entity type 是 Query IR 和 typed output 的参数,不再按 Face/Edge/Vertex 拆分 `query_brep_rfaces`、`query_brep_redges` 等平行 operations。 Node 结构: ```json { "op": "query_brep_rselection", "inputs": [ {"port": "scope", "node_id": "node_result", "output_slot": 0}, {"port": "box_source", "node_id": "node_box", "output_slot": 0}, {"port": "cylinder_source", "node_id": "node_cylinder", "output_slot": 0} ], "params": { "query_object": "query_box_cylinder_interface", "query_hash": "sha256:..." }, "outputs": [ {"port": "result", "type": "selection_set", "entity_type": "edge"} ] } ``` `query_box_cylinder_interface` 是 model 顶层 `query_objects` 中的完整 normalized Query IR,即 10.3 的完整 pattern,包括两个 current incident-face variables 和 source lineage constraints。Graph node 不保存语义缩水的第二份 query 副本。 该 node 输出 `SelectionSet`,不是复制出来的 shape object。 这里有一个 typed SelectionSet output,而不是按匹配实体数量生成多个 graph outputs。实际 cardinality 位于 Query IR。 ### 13.2 Operation Graph 类型桥接 当前 `OperationNode` 只有无名称的 `inputs` 和 `output_count`,不能表达 SelectionSet 与 Shape 的类型区别。引入 CADQL 时,model schema 必须增加: ```json { "inputs": [ {"port": "body", "node_id": "node_box", "output_slot": 0}, {"port": "edges", "node_id": "node_query", "output_slot": 0} ], "outputs": [ {"port": "result", "type": "solid"} ] } ``` Operation registry 定义静态 signature,例如: ```text query_brep_rselection(scope: Shape, sources: BindingMap) -> SelectionSet query_brep_rselection(scope: SelectionSet, sources: BindingMap) -> SelectionSet make_fillet_rsolid(body: Solid, edges: SelectionSet) -> Solid make_shell_rsolid(body: Solid, faces_to_remove: SelectionSet) -> Solid apply_tag_rselection(scope: Shape, targets: SelectionSet, tag: Tag) -> Shape ``` Importer 只能在 output slot 可恢复时把 2.0 graph 的 positional inputs 按 operation signature 升级为 named ports;无法恢复 slot 的多输出引用必须保留为 legacy binding 并报告 migration diagnostic,不能猜测。Exporter 在新 model schema 中不再用 params 中的 `selected_*_node_ids` 表达数据流。 该变更需要将 model/graph schema 从 `2.0` 升级为 `3.0`;Query IR 仍独立使用 `1.0`。Package 版本与 schema 版本不绑定。 ### 13.3 Feature 消费 SelectionSet 例如 shell graph: ```text body -----------------------> shell \ / -> query_brep_rselection -- ``` Feature params 不再新增并行的: ```text selected_faces selected_face_node_ids selected_face_indices selector_hint selection_query ``` 新 graph 只通过 typed input 消费 selection set。Legacy importer 可以将旧字段升级成 Query IR + evidence/fallback。 ### 13.4 Semantic tag persistence Semantic query 要可 replay,tag assignment 必须进入 Operation Graph,而不能只修改 Python wrapper 的 `_tags`。新增 canonical alias operation: ```text apply_tag_rselection( scope: Shape, targets: SelectionSet, tag: Tag, propagation: LOCAL | DOWNWARD, ) -> Shape ``` 该 operation 不改变 geometry,返回具有新 semantic state 的 shape view。Root shape tagging 被 lower 为选择 scope root 的 SelectionSet。自动 tagging 也生成相同的 canonical `TagBinding`,但可作为 producer operation 的 semantic output metadata 存储,避免创建大量可见 node。 TagBinding 保存 assignment node、target query/ref、attachment 和 propagation policy。执行 CADQL 时,provider 以当前 scope producer 为版本边界计算 local/inherited/effective tags。下游 geometry operation 必须通过 topology delta 传播 tag binding;没有 lineage evidence 时不得猜测 lineage tag。 Legacy 2.0 payload 中只有扁平 tags,导入时统一标为 `effective_legacy`。它们只参与 `scope: effective` 查询并产生 migration warning,不能伪造 local/inherited 来源。 ### 13.5 是否保留 `make_select_*` 迁移期保留: ```text make_select_rvertex make_select_redge make_select_rwire make_select_rface make_select_rsolid ``` 它们被定义为 legacy materialized selection node。新代码不再为 query 的每个 match 自动生成一个 `make_select_*` node。 迁移稳定后只保留 explicit one-entity materialization 作为 legacy graph adapter;CADQL 不增加 `selection_item_rentity`,避免通过另一名称恢复 index selection。 ## 14. Query Execution ### 14.1 执行阶段 统一执行器流程: ```text parse/build -> normalize -> type validate -> capability validate -> compile execution plan -> resolve named bindings -> build current BRep snapshot -> load reachable Topology Evolution Graph -> seed candidates for typed variables -> join topology/evolution/provenance constraints -> evaluate semantic and optional geometry predicates -> project and distinct the return variable -> order -> take -> cardinality validation -> build refs/evidence -> return SelectionSet[T] ``` ### 14.2 Property provider 执行器不直接散落调用 OCP/FreeCAD API。定义 backend-neutral provider protocol: ```python class BRepQueryProvider(Protocol): def resolve_binding(self, binding: QueryBinding) -> object: ... def snapshot(self, scope: object) -> BRepSnapshot: ... def evolution_graph(self, scope: object) -> TopologyEvolutionGraph: ... def related(self, entity: object, relation: Relation) -> Sequence[object]: ... def property(self, entity: object, property_id: str) -> object: ... def tags(self, entity: object, scope: TagScope) -> Sequence[str]: ... def evidence_ref(self, entity: object) -> TopoRef: ... def entity_token(self, entity: object) -> object: ... def use_token(self, use: object) -> object: ... ``` `entity_token` 只在一次 provider execution 内用于未定向 entity identity、`distinct` 和 deterministic materialization。OCP provider 应使用 topology identity (`IsSame` 等价语义),不能使用 Python wrapper identity。`use_token` 还包含 parent、orientation 和 occurrence。两种 token 都不持久化,也不能作为业务 tie-breaker;`TopoRef` 只进入 evidence/fallback,不被命名为 durable stable identity。 实现: ```text OCPQueryProvider FreeCADQueryProvider ``` 两者运行同一 Query IR conformance tests。 ### 14.3 Determinism 执行器必须保证: - `distinct` 的 equality semantics 明确。 - order keys 逐项稳定比较。 - 缺失 property 的排序行为明确,默认 validation error。 - query hash 由 normalized IR 生成,与 dict insertion order 无关。 - 同一 provider、相同 BRep 和相同 Query IR 返回相同 ordered refs。 ### 14.4 History capability boundary Provider 对每个 scope 声明 history level: ```text NONE 只有当前 BRep snapshot,例如无 sidecar 的 STEP 导入。 PARTIAL 有部分 operation/topology parent evidence。 COMPLETE 查询可达范围内的 Change graph 满足 canonical completeness checks。 ``` Current topology/semantic/geometry pattern 在 `NONE` 下仍可执行。任何引用 history/change domain 或 evolution path 的 query 必须在 validation 阶段计算所需 evidence coverage: - `NONE`:current topology/semantic/geometry query 和同一 snapshot 的 query-to-query relation 可以执行;只有跨 snapshot evolution path 产生 `UnsupportedQueryCapabilityError`。 - `PARTIAL`:只有 query 所需 operation/path 均有完整 evidence 时才能执行,否则失败。 - `COMPLETE`:按正常 graph matcher 执行。 缺失 history 不是 predicate false,也不能自动切换到 geometry fingerprint。用户可以显式提供 semantic tag sidecar 或选择 current-space query,但系统不能声称恢复了不存在的建模历史。 ## 15. Evidence 和 Drift Detection ### 15.1 Evidence schema ```json { "query_hash": "sha256:...", "binding_hash": "sha256:...", "execution_hash": "sha256:...", "provider": "ocp", "provider_version": "7.9.3", "selected": [ { "ref": { "graph_id": "graph_1", "node_id": "node_box", "output_slot": 0, "kind": "FACE", "topo_id": "face_7" }, "properties": { "geometry.surface_type": "plane", "geometry.area": {"value": 245.0, "unit": "mm2"}, "geometry.normal": { "value": [0, 0, 1], "relative_to_binding": "body" }, "semantic.tags": ["role.mounting_surface"] }, "fingerprint": { "bbox": {}, "center": [0, 0, 10] } } ], "candidate_count": 1, "runner_up": null, "diagnostics": [] } ``` Hash 定义: - `query_hash`:normalized typed graph pattern、cardinality、ordering、projection 和 options,不含具体 node IDs。 - `binding_hash`:所有 named bindings 的 graph/node/output/selection identity 的 canonical hash。 - `execution_hash`:`query_hash + binding_hash + provider semantic version + scope content fingerprint`。 Evidence 中的 `TopoRef.topo_id` 和 geometry fingerprint 只描述一次 execution 的结果,不是 durable query identity。 ### 15.2 Replay behavior Replay: 1. 重新执行 Query IR。 2. 验证 cardinality 和 ordering boundary ties。 3. 比较 query result 与旧 evidence。 4. `drift_mode=diagnose` 时,如果 execution identity 改变但 pattern 仍满足,记录 `selection_drift` diagnostic。 5. `drift_mode=strict` 时,同一情况产生 `SelectionDriftError`。 6. 如果 query 不再满足,不自动按旧 fingerprint 选择,除非 legacy query options 显式允许 fallback。 ### 15.3 Fallback policy ```text NONE EVIDENCE_FINGERPRINT LEGACY_INDEX ``` Canonical feature query 默认 `NONE`。Legacy model migration 可使用 `EVIDENCE_FINGERPRINT` 或 `LEGACY_INDEX`,并生成 warning。 ## 16. Error Model 统一错误: | Error | 条件 | | --- | --- | | `QuerySyntaxError` | 文本或 JSON IR 语法不合法。 | | `QueryValidationError` | 类型、relation、property 或 unit 不合法。 | | `UnsupportedQueryCapabilityError` | provider/backend 不支持所需能力。 | | `QueryScopeError` | source node/output 不存在或类型错误。 | | `SelectionCardinalityError` | 非零结果仍低于 cardinality minimum。 | | `NoMatchError` | `SelectionCardinalityError` 的特例:结果为零且 minimum 大于零。 | | `AmbiguousSelectionError` | 结果超过 cardinality maximum,或 top-N boundary 无法唯一确定。 | | `PropertyUnavailableError` | 实体上无法稳定计算请求属性。 | | `SelectionDriftError` | strict replay 中结果相对 evidence 发生不允许的漂移。 | | `QueryComplexityError` | Pattern variable、constraint、path 或 candidate 数量超过限制。 | | `LegacySelectionError` | Legacy index/fingerprint selector 缺少 basis 或无法解析。 | 所有错误必须包含的公共 payload: ```text query_location message error_code repair_suggestions ``` 可用时附加 `query_hash`、`binding_hash`、scope、expected/actual cardinality、candidate summaries、unsupported capability 和 source location。Syntax error 不要求提供尚不能计算的 hash 或 candidates。 ## 17. Translator Contract ### 17.1 Backend capability Translator `capabilities.py` 增加: ```json { "query_ir_versions": ["1.0"], "query_entity_kinds": [ "compound", "compsolid", "solid", "shell", "face", "wire", "edge", "vertex", "topo_use", "coedge" ], "query_domains": ["current", "history", "change", "operation", "bound_source"], "query_relations": [ "contains_direct", "contains", "incident_faces", "adjacent_faces", "adjacent_edges", "boundary_uses", "use_entity", "use_parent", "use_chart", "surface_chart", "next_coedge", "previous_coedge", "mate_coedge", "starts_at", "ends_at", "belongs_to_loop", "descends_from", "generated_from", "depends_on", "co_result_of", "changed_with" ], "history_level": "complete", "use_history": "ordered", "evolution_derivations": [ "continuation", "fragment", "merge", "intersection", "boundary", "replacement" ], "query_properties": [ "geometry.length", "geometry.area", "geometry.volume", "geometry.center", "geometry.normal", "geometry.curve_type", "geometry.surface_type", "topology.loop_role", "topology.loop_identity", "local.u_range.min", "local.u_range.max", "semantic.tags" ] } ``` ### 17.2 FreeCAD translator FreeCAD 不再维护独立 selector scoring contract,而是实现 `FreeCADQueryProvider`。 `query_brep_rselection` runtime: 1. 从 `GRAPH_NODES[source_node_id]` 取得 shape。 2. 使用统一 Query IR evaluator。 3. 返回 `GRAPH_SELECTIONS[node_id]` 中的 typed selection result。 4. 默认不为每个 selection 创建 visible `Part::Feature`。 5. 如果需要 materialized helper object,放入 construction group 并隐藏。 6. fillet/chamfer/shell 直接将 SelectionSet refs 转换为 FreeCAD `EdgeN/FaceN`。 ### 17.3 Backend parity 同一 fixture 必须在 OCP 和 FreeCAD provider 上满足: - result cardinality 一致。 - semantic tags 匹配一致。 - geometry predicate tolerance 一致。 - normal orientation semantics 一致。 - ambiguity outcome 一致。 - Change input roles 和 output derivations 一致。 - `descends_from/generated_from/depends_on` path result 一致。 不要求 backend 的 raw topology index 一致。 ## 18. Security 和复杂度限制 CADQL 不是任意 Python 执行环境。 禁止: - Python lambda/callable predicate 进入 canonical IR。 - 任意 module/class introspection。 - 任意 regex。 - unbounded recursive traversal。 - backend-specific property path 注入。 QueryOptions 限制: ```text max_pattern_variables max_pattern_constraints max_path_changes max_candidate_count max_projection_fields max_fragment_expansion execution_timeout ``` 超限产生 `QueryComplexityError`。 ## 19. Versioning Query IR 独立版本: ```text query.schema_version = 1.0 ``` 它不与 model schema version 强绑定。Model schema 声明支持的 Query IR version 范围。 Version policy: - 新增 optional property/predicate:minor-compatible。 - 改变 predicate、normal、tag scope、cardinality 语义:major change。 - Provider capability 可以小于完整 schema,但必须显式声明。 ## 20. 迁移方案 ### Phase 0:Characterization 目标:锁住当前 behavior 和问题。 工作: - 收集当前 QL、indexed getter、tag selection、`make_select_*` fixtures。 - 增加对称 geometry、nested selection、Compound、pattern、fillet/chamfer/shell tests。 - 增加 box/cylinder boolean interface、edge split、face trim、split/merge lineage characterization fixtures。 - 审计每个 topology-changing operation 当前能提供的 input/output parent mapping;区分 complete、partial 和 absent evidence。 - 增加 OCP replay 与 FreeCAD translator parity tests。 - 不修改 public API。 退出标准: - 当前每种 selection path 都有 model JSON fixture。 - 已知 ambiguity 和 backend mismatch 有明确 failing test。 ### Phase 1:Typed Graph Pattern IR 新增模块建议: ```text src/simplecadapi/cadql/ ├── __init__.py ├── types.py ├── predicates.py ├── relations.py ├── ir.py ├── validation.py ├── normalize.py ├── errors.py └── provider.py ``` 实现: - Query IR dataclasses 和 JSON schema。 - Variable、topology relation、evolution path 和 pattern constraint registry。 - Property/relation applicability registry。 - Unit/tolerance normalization。 - Cardinality 和 set semantics。 - Query hash。 - 不实现 GraphQL parser。 退出标准: - JSON IR round-trip 稳定。 - invalid variable/relation/path/property/unit 在执行前失败。 - normalized IR hash 稳定。 ### Phase 2:Topology Evolution Evidence 目标:为 geometry-independent history query 建立可靠事实源。 实现: - 将当前 `TopoDelta` 扩展为显式 Change inputs/outputs。 - 增加 `TopoUseVersion`、`USE_ENTITY/USE_PARENT` 和可选 ordered use-fragment evidence。 - 每个 input 记录 entity ref、input port 和 role。 - 每个 output 记录 entity ref、event 和 derivation。 - 为 boolean、fillet、chamfer、shell、cut、transform 和 pattern 建立 topology history adapter。 - 区分 `TopoEntityVersion` 与 oriented `TopoUse`。 - 序列化完整 parent refs,不只保存宏观 generated/modified lists。 - Capability 按 operation 和 derivation 分类声明 complete/partial/unsupported。 退出标准: - split、merge、intersection、boundary 和 continuation fixtures 可重建 Change graph。 - Seam/coedge fixture 能区分 underlying entity ancestry 与 occurrence ancestry;不支持 use ordering 时 capability 明确为 partial。 - 同一 fixture 的 OCP record-time evidence 可 JSON round-trip。 - 证据不完整时 lineage query 在执行前失败,不做 geometry fallback。 ### Phase 3:OCP Pattern Evaluator 和 Python Builder 新增: ```text cadql/builder.py cadql/evaluator.py cadql/providers/ocp.py ``` 实现: - Solid/Face/Wire/Edge/Vertex topology pattern。 - Current/history/change domains 和 typed joins。 - `descends_from`、`generated_from`、`depends_on` 和 Change constraints。 - 基础 geometry properties。 - semantic tags。 - pattern/order/cardinality/evidence。 - Python fluent builder。 Feature API 开始接受 `SelectionQuery[T]`/`SelectionSet[T]`,但旧 list/ShapeSelector 继续兼容。 退出标准: - 新 builder 能表达 boolean interface 和任意 feature 后的 edge fragments,且不需要 geometry predicate。 - 新 builder 可替代 fillet/chamfer/shell 的主要 QL examples。 ### Phase 4:Operation Graph 和 Semantic Integration 实现: - 新增 `query_brep_rselection` canonical op。 - Graph/model schema 3.0 增加 named input ports 和 typed outputs。 - Feature graph 通过 typed selection input 消费 query result。 - Serializer/replay 支持 Query IR 和 Evidence。 - `apply_tag_rselection` 和 source-preserving TagBinding。 - Legacy selected params 导入升级。 退出标准: - 新 graph 不再依赖 `selected_*_indices` 作为 primary contract。 - QL semantic intent 在 model JSON 中可见。 - active GraphSession 能记录完整 Query IR、bindings 和 TagBinding。 ### Phase 5:FreeCAD Provider 实现: - `FreeCADQueryProvider`。 - Generated runtime graph-pattern evaluator。 - FreeCAD topology history 到 canonical Change graph 的 adapter。 - detail feature SelectionSet 转换。 - selection helper 默认 internal/hidden。 - OCP/FreeCAD conformance suite。 退出标准: - 所有共享 query fixture 在两个 provider 中 outcome 一致。 - Boolean interface 和 feature fragment lineage outcome 一致。 - 不再维护独立的 ad hoc FreeCAD geo selector scorer。 ### Phase 6:GraphQL-like Parser 在 IR 和 evaluator 稳定后实现: - parser。 - variables。 - fragments。 - schema introspection。 - formatter。 - query explain/debug tools。 GraphQL parser 不是前五阶段的 blocker。 ### Phase 7:Deprecation 逐步 deprecate: - 当前 `ShapeSelector` 作为 canonical graph representation。 - `selection_query`、`selected_*_node_ids`、indices 多轨并存。 - indexed getter 作为推荐 replayable selection。 - backend-specific geo selector scoring。 旧 model JSON 继续通过 migration adapter 导入。 ## 21. 测试方案 ### 21.1 IR tests - JSON round-trip。 - schema version。 - normalized hash。 - property/relation type validation。 - unit conversion。 - tolerance override。 - cardinality validation。 ### 21.2 Geometry tests - box 最大/最小平面。 - cylinder planar/cylindrical faces。 - circular edge radius。 - face normal same/opposite/parallel semantics。 - inner/outer wire。 - nested face-to-edge traversal。 - planar chart frame 在 face split 后保持一致。 - periodic surface chart 的 seam coedges 保留 occurrence。 ### 21.3 Topology evolution/composition tests - box/cylinder union 交界 edge 只通过两个 source bindings 和 `INTERSECTION` 选择。 - 原 edge 经 boolean/fillet/chamfer 后的 current fragments。 - 原 face trim/split 后的 query 返回多个 current Face。 - `select(T, in_=query)` 只做 containment,不隐式加入 incident 或 history candidates。 - 显式 `incident_faces` relation 能选到 fillet/chamfer transition faces。 - 显式 Change constraint 能选到同一 Change 的 boundaries。 - coedge fragment ordering 有 evidence 时可用,无 evidence 时明确拒绝 ordered query。 - 多个不共面 faces 各自维护 chart,禁止直接跨 chart 比较 UV。 - 无 history 的导入 shape 可执行同一 snapshot 的 query composition。 - 无 history 的 source query 跨 topology-changing snapshot evolution 明确失败。 - Source face 完全被 feature 消耗时,Change constraint 仍可从历史 input 到达 generated outputs。 - `MERGE/REPLACEMENT` 默认不继承 source chart;重新定义 Face query 和 frame 后才允许 local query。 ### 21.4 Semantic tests - local tag。 - inherited/effective tag。 - lineage tag。 - wildcard token pattern。 - metadata namespace。 ### 21.5 Ambiguity tests - 对称 box 的相同 edge。 - duplicated holes。 - radial pattern 中相同 faces。 - 同 area/normal/center 的候选。 - order keys 同分。 预期:需要唯一实体时产生 `AmbiguousSelectionError`。 ### 21.6 Replay tests - 参数变化后 query 仍匹配同一语义实体。 - topology id 改变但 semantic query 仍成立。 - query result drift evidence。 - strict/no fallback。 - legacy fingerprint fallback warning。 ### 21.7 Translator conformance - OCP 和 FreeCAD cardinality 一致。 - OCP 和 FreeCAD property normalization 一致。 - normal orientation 一致。 - tag scope 一致。 - ambiguous query 均失败。 - fillet/chamfer/shell subelement mapping 正确。 ## 22. API 示例 ### 22.1 最大平面 ```python top_face = ( cadql.select(cadql.Face, in_=body) .where(cadql.surface_type.eq("plane")) .order_by(cadql.center.z.desc(), cadql.area.desc()) .take(1, boundary_ties="error") .expect_one() ) ``` 如果多个面 center.z 和 area 均相同,查询失败,而不是取第一个。 ### 22.2 Semantic mounting faces ```python mounting_faces = ( cadql.select(cadql.Face, in_=body) .where( cadql.tag("role.mounting_surface", scope="effective") & cadql.surface_type.eq("plane") ) .expect_at_least(1) ) ``` ### 22.3 Face boundary 中的 circular edges ```python mounting_surface = ( cadql.select(cadql.Face, in_=body) .where(cadql.tag("role.mounting_surface", scope="effective")) .expect_one() ) holes = ( cadql.select(cadql.Edge, in_=mounting_surface) .where( cadql.curve_type.eq("circle") & cadql.radius.approximately(cadql.mm(2.5), rel=1e-6) ) .expect_exactly(4) ) ``` ### 22.4 Feature consumption ```python rounded = fillet_rsolid( body, edges=( cadql.select(cadql.Edge, in_=body) .where(cadql.tag("edge.outer.vertical")) .expect_exactly(4) ), radius=2.0, ) ``` ### 22.5 Corner case: source face 被 split 成多个 current faces 假设 boolean 或后续 trim 将原来的一个 mounting face 切成三个不连续的 face fragments。Source query 要求唯一;current query 独立要求三个结果: ```python mounting_surface = ( cadql.select(cadql.Face, in_=before_boolean) .where(cadql.tag("role.mounting_surface", scope="effective")) .expect_one() ) current_mounting_faces = ( cadql.select(cadql.Face, in_=after_boolean) .where( cadql.related( cadql.descends_from, from_=cadql.this, to=mounting_surface, derivations=("continuation", "fragment"), depth=(1, 64), ) ) .expect_exactly(3) ) # Query all current boundary occurrences across the three fragments. outer_coedges = ( cadql.select(cadql.Coedge, in_=after_boolean) .where( cadql.related( cadql.boundary_uses, from_=current_mounting_faces, to=cadql.this, ) & cadql.topology.loop_role.eq("outer") ) .expect_at_least(3) ) # Most detail features consume underlying 3D edges, not oriented coedges. fragment_edges = ( cadql.select(cadql.Edge, in_=after_boolean) .where(cadql.related(cadql.use_entity, from_=outer_coedges, to=cadql.this)) .expect_at_least(3) ) ``` Lowering 的关键部分: ```text F: Face(current) U: TopoUse(current) E: Edge(current) F descends_from mounting_surface query result through derivation in {CONTINUATION, FRAGMENT} F boundary_uses U U use_entity E return distinct E ``` 这里不能沿用 source query 的 `.expect_one()` 检查 current results。两个 query 有两个独立 cardinality contract。若参数变化后只产生两个 fragments,current query 的 `.expect_exactly(3)` 必须产生 `SelectionCardinalityError`,而不是按旧 topology index 补选第三个 face。 ### 22.6 Corner case: source face 被 fillet/chamfer 完全消耗 假设一个很窄的 face 被 fillet 或 chamfer 完全消耗,最终结果中不存在任何 `CONTINUATION/FRAGMENT/REPLACEMENT` face。Descendant query 应返回空集;generated-output query 直接从消费 source face 的 Change 开始,不需要 context expansion mode: ```python narrow_surface = ( cadql.select(cadql.Face, in_=before_fillet) .where(cadql.tag("role.narrow_transition", scope="effective")) .expect_one() ) transition_faces = ( cadql.select(cadql.Face, in_=after_fillet) .where( cadql.related( cadql.generated_from, from_=cadql.this, to=narrow_surface, derivations=("boundary",), depth=1, ) ) .expect_at_least(1) ) transition_edges = ( cadql.select(cadql.Edge, in_=after_fillet) .where( cadql.related( cadql.depends_on, from_=cadql.this, to=narrow_surface, depth=1, ) & cadql.related( cadql.co_result_of, from_=cadql.this, to=transition_faces, ) ) .expect_at_least(1) ) ``` 对应语义: ```text source current descendants may be empty Change consumes historical narrow_surface query result Change outputs transition_faces with BOUNDARY derivation transition_edges are outputs of the same Change and depend on the source ``` 该查询不检查 operation 名称是否为 fillet/chamfer,也不假定 transition edge 是 line、circle 或 spline。若 backend 只有 current BRep、没有消费 source face 的 Change evidence,必须产生 `UnsupportedQueryCapabilityError`;不能退化为按距离或面积猜测 transition faces。 ## 23. Open Questions 以下问题在 Phase 1 前需要做 ADR 决策: 1. SelectionSet 是否允许作为 public Python sequence,还是只暴露显式 `.all()`、`.one()`。 2. Curved face normal/curvature sampling policy 在哪个版本加入。 3. Evidence 是否默认进入 model JSON,还是只对 feature-consumed query 持久化。 4. Legacy index fallback 的废弃周期。 5. GraphQL-like parser 是自行实现最小 grammar,还是复用 GraphQL parser 后转换 AST。 6. Periodic surface chart 的 canonical seam 和 UV unwrap policy。 7. Source Face query 的 frame 在非刚性/拓扑替换 operation 后允许 continuation,还是必须显式提供新 frame。 ## 24. 推荐结论 采用以下方向: 1. 新建 `simplecadapi.cadql`,不继续把当前 `ql.py` 扩展为长期 canonical contract。 2. Query IR 1.0 是核心规范;GraphQL-like text 是后置前端。 3. Canonical query 是 Current BRep Graph 与 Topology Evolution Graph 上的 typed graph pattern,不是 operation-specific selector。 4. Public API 只有 `cadql.select(ResultType, in_=scope)` 一个 query 入口;Shape、operation output 和已有 query 都通过同一个 `in_` binding 进入。 5. 所有选择归约为六类 match capability;所有 query 使用相同的 `where/order_by/take/expect` 行为并产生 `SelectionQuery[T]`,`.resolve()` 统一返回 `SelectionSet[T]`。 6. Geometry predicate 是可选约束;topology role、source binding、semantic tag 和 evolution path 可以独立完成选择。 7. 完整 Change input/output/role/derivation evidence 是 history query 的前置条件,缺失时明确报 capability error。 8. Unit、tolerance、oriented TopoUse、ordering 和 cardinality 必须进入 contract。 9. Query intent 与 Evidence/fingerprint 分离;`TopoRef` 只作 evidence/fallback。 10. 新 graph 使用 typed SelectionSet node,不再把每个 query match 自动展开为 geometry-only `make_select_*`。 11. OCP replay 和所有 translator 运行同一 Query IR、Change Graph、TopoUse 和 surface-chart conformance suite。 12. GraphQL-like parser 只有在 Query IR 和 evaluator 稳定后才实现。 该方案的核心不是设计更漂亮的 filter API,而是建立一个可以跨参数变化、跨 replay、跨 CAD backend 保留用户选择意图的 topology query contract。