Files
cadSet/SimpleCADAPI/design-docs/cadql-brep-query-language.md

2659 lines
94 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.<namespace>.<key>
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": [<predicate>, <predicate>]}
{"or": [<predicate>, <predicate>]}
{"not": <predicate>}
```
空 `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<T>` 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<T>`,不是复制出来的 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<K>, sources: BindingMap) -> SelectionSet<T>
query_brep_rselection(scope: SelectionSet<K>, sources: BindingMap) -> SelectionSet<T>
make_fillet_rsolid(body: Solid, edges: SelectionSet<Edge>) -> Solid
make_shell_rsolid(body: Solid, faces_to_remove: SelectionSet<Face>) -> Solid
apply_tag_rselection(scope: Shape<K>, targets: SelectionSet<T>, tag: Tag) -> Shape<K>
```
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<K>,
targets: SelectionSet<T>,
tag: Tag,
propagation: LOCAL | DOWNWARD,
) -> Shape<K>
```
该 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。