feat(cadfs): expand reconstruction capability coverage

This commit is contained in:
2026-09-09 10:11:46 +08:00
parent 5bd1011335
commit 2f89d7930c
21 changed files with 8373 additions and 418 deletions
@@ -0,0 +1,514 @@
# CADFS 全量能力闭环目标
## 目标
将 CADFS FeatureScript 的完整建模历史通用地转换为 CDSL,再由 CDSL engine
重建 STEP。目标不是让少数代表模型通过,也不是为某个样本拟合最终 STEP;目标是
补齐当前 CADFS 全量语料实际出现的所有建模语义,使每个可比较模型都能以完整
feature history 生成工程相似的 STEP。
工程验收使用 `comparison.json.rp.passed == true`:双向 surface max/p99 与 bbox
不超过 `0.02 mm`、体积和面积相对误差不超过 `0.005`、实体数相等。`strict` 的
`0.01 mm` / `1e-5` 阈值保留为高精度诊断,不能因严格失败而隐藏已工程相似的模型,
也不能因生成 STEP 就把几何错误的模型标为成功。
本文件的范围是
`output-history/20260907-235209` 的 9,347 条历史快照。后续重新扫描全量数据时,
必须以新快照的真实样本数和能力矩阵更新本文件及本地能力台账。
## 不可妥协的实现原则
这是绘图引擎和通用 converter,不是样本修复脚本。每项能力必须由 FeatureScript
语义、CDSL contract 和内核拓扑共同定义,并对同类输入普遍成立。
- 禁止以 `sample_id`、文件路径、特定尺寸、特定坐标、特定 feature ID 或 gold STEP
测量值驱动行为。不得为让单条样本通过而植入分支、偏移量、默认 profile 或 selector。
- 不得读取原 STEP 来补齐 FeatureScript 未提供的参数、反推 CDSL 尺寸或决定建模策略。
原 STEP 只用于最终比较、诊断和已验证的 source exception 证据。
- 不得把未知语义降级成任意盲拉伸、默认 union、任意当前 body、任意相近 face/edge
或静默跳过。无法唯一表达或绑定时必须保留可执行前缀、输出明确诊断,并记录能力
缺口。
- 优先扩展显式 CDSL schema、typed runtime state、body graph 和 kernel-level topology
delta;不得以 parsing/lowering 层的临时重写替代应由 runtime/adapter 承担的几何语义。
- 允许针对几何类别采用受限算法,但适用前提必须由通用的、可验证的几何条件表达,
并有正向、反向和边界测试。条件不满足时必须拒绝并诊断,不得猜测。
- 若已有实现反复依赖样本化补丁、不能表达已出现的通用语义或受内核 API 结构性限制,
必须评估替代方案,不得沿错误方向继续累积补丁。替换需要可复现根因、成熟实现或最小
原型的对照、contract/迁移影响评估和回归计划;单个样本或偶发内核失败不足以触发重写。
- 每项能力必须同时具备:FeatureScript lowering、CDSL schema/semantic validation、
runtime/adapter 实现、selector/body 语义、单元测试、多个语料回归和比较工件;缺少
任一层只能标记为“部分完成”。
## 全量基线
历史快照的主要操作和当前能力缺口如下。数量是出现次数或受该缺口阻塞的模型数,
只用于排序,不代表一个操作只有一种语义。
| FeatureScript 操作 | 历史出现次数 | 当前主要缺口数 |
| --- | ---: | ---: |
| `newSketch` | 20,857 | 草图/工作平面诊断 7,043 |
| `extrude` | 18,168 | 3,600 |
| `fillet` | 5,210 | 1,904 |
| `revolve` | 2,087 | 662 |
| `chamfer` | 1,818 | 588 |
| `cPlane` | 1,620 | 168 |
| `hole` | 1,203 | 582 |
| `shell` | 729 | 696 |
| `mirror` | 483 | 241 |
| `transform` | 468 | 252 |
| `sweep` | 378 | 326 |
| `loft` | 377 | 107 |
| `circularPattern` | 252 | 139 |
| `booleanBodies` | 228 | 187 |
已发现的派生 profile / topology 语义必须单独覆盖,不能合并为笼统的 `extrude`
或 `loft` 支持:
| 语义 | 历史缺口数 |
| --- | ---: |
| `extrude_profile_topology:intersect` | 383 |
| `extrude_profile_topology:cap_face` | 201 |
| `extrude_profile_topology:cap_edge` | 134 |
| `extrude_profile_topology:swept_face` | 99 |
| `extrude_extent:up_to_surface` | 100 |
| `loft_profile_topology:cap_face` | 30 |
| `extrude_profile_topology:offset_face` | 18 |
| `extrude_profile_topology:swept_edge` | 16 |
| `loft_profile_topology:swept_face` | 9 |
| `extrude_extent:up_to_vertex` | 7 |
| `extrude_surface_or_mixed` | 4 |
| `extrude_extent:up_to_body` | 2 |
| `extrude_profile_topology:mid_cap_edge` | 2 |
| `loft_profile_topology:cap_edge` / `cap_vertex` | 1 / 1 |
历史报告还记录了 1,335 个未支持操作诊断。当前已知的 P2 操作包括 `draft`、
`thicken`、`split`、`moveFace`、`deleteFace`、`replaceFace`、完整 `transform`、
`derive`、`import` 和 `bend_add`。重新扫描时必须从诊断原文生成完整操作清单;
未列出的新操作不能被静默归入已有能力。
## 覆盖模型
每一项能力的回归选择必须覆盖下面五个维度,而不是只覆盖操作名称:
1. 操作和参数:operation mode、实体/曲面模式、extent、方向、offset、draft、
角度、数量、终止条件和 result mode。
2. 几何输入:直线、圆弧、圆、椭圆、B-spline、开口/闭合 wire、多 region、孔洞、
退化和自交拒绝路径。
3. 拓扑来源:原草图、`CAP_FACE`、`CAP_EDGE`、`SWEPT_FACE`、`SWEPT_EDGE`、
`OFFSET_FACE`、`INTERSECT`、`MID_CAP_EDGE`、`COPY` 和 boolean 后继。
4. body 生命周期:`NEW`、`ADD`、`REMOVE`、`INTERSECT`、keep tools、copy、
transform、delete、mirror、linear/circular/nested pattern、多实体输出。
5. 比较状态:strict、RP、实际几何拒绝、conversion defer、runtime ineligible、
rebuild failure、comparison timeout 与 source exception。
维护三个固定层级的回归集:
| 层级 | 用途 | 通过条件 |
| --- | --- | --- |
| 原子语义矩阵 | 每个 operation/参数/拓扑/body tuple 的最小合成模型 | CDSL contract、可执行性和精确几何断言。 |
| 核心 17 | 跨能力的稳定烟雾回归 | 完整 history、工程相似或有归因明确的 source/comparison 证据。 |
| 扩展 100+ | 每个已出现 tuple 至少多个真实语料样本 | RP 工程相似率、失败诊断和能力覆盖均可追溯。 |
| 全量 9,347 | 完整发布前的能力验证 | 每条都有完整分类;支持的完整 history 均工程相似,剩余项仅能是有证据的 source exception 或未实现 capability。 |
扩展集必须从能力矩阵自动选择并保留历史失败样本。修复能力后应扩大相应 tuple 的
样本数;不得因当前失败而从 manifest 中移除样本,也不得以新的贪心选择替换已有
反例。
## 实施路线
### 开源实现策略
每个新的内核级能力在自研前,先检查官方 OCCT/OCP API、项目现有依赖和成熟开源实现。
本地首选参考是 `/Users/lk/Downloads/SimpleCADAPI-master 4`:其
`topology/tracking.py` 保留 OCC builder 的 `Modified`、`Generated`、`IsDeleted` 和
section-edge 历史,`ocp_booleans.py` 展示了保留 history 的 boolean 与 same-domain
cleanup,其他 kernel/operators/tests 覆盖 transform、shell、sweep、loft、fillet 和
chamfer。这些实现对 P0 topology delta、body provenance 和 P2 feature adapter 具有
直接参考价值。
采用外部方案时必须确认许可证、OCP/OCCT 版本和异常语义,并将算法适配到本项目的
CDSL contract、body graph 和 selector resolver。SimpleCADAPI 强制单一 `Solid` 的
boolean helper 不能直接使用,因为 CADFS 需要独立 body、copy、keep-tools 和 pattern
instance 生命周期。每项采用或拒绝外部方案的决定、理由和回归证据都记录在本地能力
台账;不得不经评估地复制代码,或为复用外部 API 破坏现有 contract。
### P0:通用几何状态和拓扑基础
1. 建立显式 body graph:feature 输出 body ID、result mode、copy、transform、delete、
keep tools、pattern instance 和多 body 聚合语义。当前可用的受限增量是以 source
feature body 为输入的 rigid translation/rotation 与受限 non-rigid uniform-scale
`transform_bodies` / `delete_bodies` contract;它不能
代替 fused body 或 pattern instance 的完整 provenance,后两者在唯一性未证明时
必须诊断而不能退化为 current body。capability preflight 已镜像 runtime 的显式
body-member 生命周期:fused/dress-up 输出、boolean 消耗、delete 和 pattern replay
会撤销旧 member 的可选资格;只有 `new_body`、`keep_tools`、明确 transform 输出,或
经 sole-body lineage 已证明的 current successor,才能被后续 body query 选中。受限的
`pattern_circular` 现在可对所有 source 都是明确可选 member 的 `NEW` additive body,或
一个 direct `SWEPT_BODY` 经该 sole-body successor 保留的 current member,以
`pattern_feature_id` / `source_feature_id` / `instance_index` 记录 direct COPY
member;后续 `transform_bodies` 可精确引用该 COPY 而无需写入 runtime ID。CADFS 的
direct `SWEPT_BODY` transform 现可将 `TRANSLATION_3D`、rotation,以及单一线性
sketch/`CAP_EDGE` 或有 physical cap frame 的 direct `CAP_FACE` direction 的非负
`TRANSLATION_DISTANCE` lower 为同一刚体 contract。系统 datum plane、已显式 lower 的
reference plane,以及能由 direct planar source 唯一构成 frame 的 `SWEPT_FACE` 也可提供该
direction;圆柱/曲面 `SWEPT_FACE`、`SWEPT_EDGE`、OFFSET/COPY face 和没有可证明 frame 的
plane query 继续诊断而不猜测。`SCALE_UNIFORMLY` 在 factor 有限且为
正数、scale point 是 Origin、direct sketch vertex(含 circle center)或 direct
`CAP_VERTEX` 时 lower 为显式 `uniform_scale` body transform,不烘焙回草图。`TRANSLATION_ENTITY` 在 direct source
仍是可选独立 body 时接受一个线性
sketch/`CAP_EDGE` 的端点差或两个 sketch/`CAP_VERTEX` 的有序点差;move 的 bake path
另要求 source 是紧邻的 `NEW` body。对于 exact single-source `makeCopy` transform,
后续 `COPY(instanceName=1)` body/line/vertex 可递归验证其 `derivedFrom` 链并引用已生成
body member;对于前序 direct multi-source `makeCopy`,CDSL 以
`transform_copy_refs(transform_feature_id, source_feature_id)` 表示被查询的唯一直接 source,
runtime 分别物化各 source 的 transformed member,aggregate 只用于 STEP 导出且不伪造
topology owner/delta。schema、semantic validation、capability preflight 和 runtime 都要求
owner 是 preceding multi-source `make_copy:true` transform,且 source 是该 transform 的
直接选择项;single-source、source 外、fused、pattern 和无源限定的 aggregate reference
一律拒绝。无位移的 direct `TransformType.COPY` 以显式 zero translation 与
`make_copy:true` 产生一个独立 identity body member,绝不将其写成 non-copy successor
alias。CAP edge/vertex 的 source-local in-plane coordinates 保持 profile frame,
仅映射到 selected physical cap origin。face、
generic face、swept/offset、pattern 或未证明 COPY/curve direction、多个 direction、已吸收 body 与其它非刚体
transform 仍必须明确诊断。对于已证明只有一个 CADFS body 的受限 lineage,direct
`SWEPT_BODY` 可从初始 `new_body` 经 ordinary fusing add、单 body fillet/chamfer/shell
与 exact non-copy transform 续接到当前 CDSL member;cut 保留所选 member,因而不虚构
一个 cut successor。直接 circular ADD 且 source 唯一等于该 current member 时保留这份
证明,并以该成员的实际 B-rep 物化每个 COPY instance;第二个 `new_body`、`make_copy`
transform、boolean、其它 pattern、delete 或任何未列出的 body atomic 都清空它。它绝不以
aggregate/current body 兜底,
也不为 linear/nested pattern、被排除 instance 或无明确 source member 建立 ownership。
唯一新增的
mirror 例外是:直接 `pattern_mirror` 的每个 source 都仍为可选的
`result_mode: new_body` member 时,runtime 以实际 B-rep mirror 物化其唯一的
`instance_index: 1` COPY member;CDSL semantic validation、capability preflight 与
runtime 都要求这一完整条件。普通 additive/cut/dress-up 后继、已吸收 source、linear
或 nested mirror 仍走 feature replay,不能借该例外暴露 aggregate member。
本轮 327 条 transform shard 中 `00120430`、`00496729`、`00749755` 因该 provenance
rule 完整 lower;只有 source 在 mirror 前未被后续 mutation 吸收的 `00749755` 可执行到
RP `rebuilt_approximate`,另两条保留 `runtime_ineligible` 和已有前缀/诊断,绝不以
converter 完整状态冒充 runtime 或相似通过。新增的 multi-source COPY path 令 `00699847`
的 F3 两源 copy 和 F4 的 source-qualified F3/F1、F3/F2 queries 完整 lower,强制
pipeline 为 `rebuilt_strict`;`00950564` 证明 F1 的 direct `SWEPT_BODY` 在 F3/F5/F7/F9
ordinary add 和 F10/F11 chamfer 后精确 lower 为 F12 的 `source_feature_ids:["f_F11"]`,
不引用 current aggregate。`output/single-body-successor-20260909` 的完整 history 为
`converted_complete`、runtime eligible、8/8 feature executed、`rebuilt_approximate`;
RP 通过(bbox delta `0`,体积/面积相对误差 `0.00011913` / `0.00016132`,实体数 `1`)。
strict 仍因 max/p99 surface `0.01 mm` 与体积/面积阈值失败而保留为诊断。单元测试还覆盖
copy、boolean、pattern 与 delete 均清空 lineage;没有能唯一表示的 nested source-member
chain 仍属未完成 contract,继续保留明确诊断。
2. 在 adapter 层记录每次建模操作的 `preserved`、`modified`、`generated`、`deleted`
topology delta 及 CAP/SWEPT/boolean 等输出角色;runtime 以该记录完成后继 selector
绑定,不以“当前形状中最相近元素”猜测。当前增量已覆盖 OCC 刚体
`transform_bodies`(含 OCP `gp_Trsf.SetScale` 的 uniform scale)、单实体
`boolean_bodies` 的 union/subtract/intersect 以及 direct
single-body fillet/chamfer,以及单一 selected solid 的 `shell`:adapter 返回 opaque
kernel relation,registry 只在 source/result snapshot 的 exact relation 唯一时传递
owner 和 successor,并在 rebuild 输出 `topology_deltas`。shell 沿既有
`BRepOffsetAPI_MakeThickSolidByJoin` builder 的 `Modified`、`Generated`、`IsDeleted`
历史记录 delta;未返回 `IsDeleted` 的内核输入不会被虚构为 deleted。该受限 shell path
还记录可由 source subshape 和 kernel event 证明的 `shell.body_face`、
`shell.offset_face`、`shell.closing_descendant` 和 `shell.wall` evidence role。当前
`shell.offset_face` 只有一个受限 lowering consumer:紧邻 shell 后续的 shell face query,
且 FeatureScript 以 `TDD`/`trueDependencyDisambiguation` 明确指向前序 direct `new_body`
blind-prism 的 `extrude.start` 或 `extrude.end`。CDSL 以
`output_role_source(owner_feature_id, output_role)` 保留此关系;registry 只接受 exact
kernel-history relation 的唯一 active snapshot,不能以相同 geometry、bare offset role 或
任意嵌套 CAP query 替代。它仍不是可 lower 的通用 CAP/SWEPT/OFFSET query。多个 Compound 成员时只有被选中实体的同一
builder 可以产生 delta,其他未改成员继续依赖 body graph 的精确等价。多实体 boolean、
非 direct-builder dress-up fallback、复杂 sweep/loft history 及 CAP/SWEPT/section
query roles 仍属未完成范围。受限的单闭合无内环 profile、无 guide/transition 的 direct
solid sweep 在 initial 或 `new_body` final member 上,会记录
`BRepOffsetAPI_MakePipeShell.FirstShape/LastShape` 的 `sweep.start`/`sweep.end`
evidence;受限的 initial closed-wire solid loft 会记录
`BRepOffsetAPI_ThruSections.FirstShape/LastShape` 的 `loft.start`/`loft.end` evidence。
后续 fuse 会重建 topology,因此不复用已经失效的 sweep/loft builder history。当前
CDSL `selectorRef.output_role` 现公开 direct-prism `extrude.start`/`extrude.end` 与四个
sweep/loft direct-builder cap role;registry 只会在
relation 的唯一 result snapshot 上写入 `TopologyRecord.output_roles`,resolver 要求
owner、active body snapshot 与唯一 role 命中,并拒绝 stable ID、geometry、snapshot ID
混用。CADFS lowering 现对独立 `new_body` blind prism 的 CAP_FACE,以及满足全部条件的
`LocOpe_DPrism` drafted extrusion 生成该 contract:单一闭合外环、无内环、单向 blind
extent,且拉伸方向与实际端盖法向同向。`extrude_from_face` 直接拉伸该 B-rep 面,不能把
CAP_FACE 还原为原始草图。registry 只会让唯一 OCC `modified`/`preserved` relation 跨
boolean 删除成员后的 Compound member-index 改变继续传递 owner/output role;几何回退仍按
member index 隔离,绝不以相似性跨成员继承。该路径不覆盖 drafted 内环、fused/multi-region/
multi-extent/loft/sweep/pattern 或一般 CAP_FACE,故 `CAP_FACE`/`SWEPT_FACE` lowering 仍为
部分完成。`00016195` 的 F1 -> fillet -> F3 和 `00835610` 的 drafted F3 -> F4 subtract ->
F5 都是正例;后者为 `converted_complete` / `rebuilt_approximate`,9 个 feature、3 个实体,
RP 通过(bbox delta `0.0008 mm`、体积相对误差 `0.00020848`、面积相对误差
`0.00021742`)。`00268467` 的 drafted 内环则明确保留 capability diagnostic 和可执行
前缀,不伪造可绑定 selector。
3. 建立统一 selector resolver:owner body、source feature、output role、几何签名、
snapshot 和唯一性证明。找不到或多解必须稳定诊断。
4. 完成草图 region/wire 模型:多轮廓、内环、开口 reference geometry、B-spline/ellipse、
profile query、显式 construction 和退化检测。
非周期 `skFitSpline` 的两点受限变体现已贯通:仅当端点不同、同时给出两端导数,且
CDSL 显式携带严格递增的参数域时,lowering 才以真实的 two-point B-spline 输出;它
绝不退化为直线。schema、semantic validation、sketch solver 与 adapter 共同拒绝缺失
导数/参数、周期两点和重合端点。solver 以参数域上的 cubic Hermite 采样检查闭环面积,
adapter 仍把端点切线与参数交给 OCP 插值,而非近似多段直线。历史快照中 453 个此类
entity 都带两端导数;当前 10 条历史反例矩阵的 9 个可生成候选保留了 18 条该曲线。
该增量只完成此一 profile contract,不覆盖由 `INTERSECT`、`CAP/SWEPT` 等拓扑查询
派生的 profile,也不把后续 dress-up 或内核失败误记为 spline 成功。
### P1:派生 profile 与拉伸终止
1. 用 P0 topology registry 通用重放 `CAP_FACE`、`CAP_EDGE`、`SWEPT_FACE`、
`SWEPT_EDGE`、`OFFSET_FACE`、`INTERSECT`、`MID_CAP_EDGE`,将其物化为可追溯的
profile/wire,而不是复用原草图或写入样本特例。现有 direct-prism,以及单闭合无内环、
法向同向的 `LocOpe_DPrism` draft CAP_FACE,是受限例外:它们以 builder-proven output
role 直接消费物理 B-rep 面,未试图物化或重建 profile。
`OFFSET_FACE` 现有一个独立的受限 profile 物化路径:只有 inward shell 直接消费一个
`new_body` blind additive prism,shell 恰移除该 prism 的一个 CAP,且 OFFSET query 恰指向
其未修改、单闭合凸线性 profile 中的一条非 construction line,lowering 才根据 source
profile 的相邻边 offset 交点、shell thickness、实际 start/end cap frame 构造四边内壁。
它不复用整张 source sketch,也不从 STEP 反推 trim;draft、cut/fused/multi-body source、
多 removal cap、曲线/凹/多 contour profile、多个 source edge 和非正交 span 都保留
`extrude_profile_topology:offset_face` 诊断。`00789939` 的 F15 是严格通过的完整 history
正例;`00588094` 的非直接 provenance 则保持 partial conversion 和五 feature executable
prefix,后续 F15 的 output-role binding failure 仍单独报告。`output/offset-face-linear-wall-20260909-r3`
记录前者 strict/RP 通过;当前 26 条含 `OFFSET_FACE` history 的受控 shard
`output/offset-face-profile-shard-20260909-r1` 为 1 strict、17 rejected、7 rebuild failure、
1 deferred-no-executable-feature,仍有 17 条明确的 `extrude_profile_topology:offset_face`
gap。`output/core-17-offset-face-linear-wall-20260909-r2` 为 4 strict、3 RP、9 rejected、1
comparison timeout,故这仍是派生 profile 的部分完成项而非 general OFFSET replay。
对 CAP_EDGE,现有的受限 outer-profile 规则还会递归展开仅由 `qUnion` 构成的关联嵌套,
再要求恰有一个原始闭合 IMPRINT 外 region 和一个唯一 CAP_EDGE-derived IMPRINT region,
并由两个明确 face-side 判断完整外 region 或外环加孔。该展开只消除 FeatureScript 局部
alias 引入的集合包装,不改变 query provenance,也不放宽 owner、曲线或 side 的唯一性。
`00950564` 的 F3/F5 是这一语义的真实证据:两个 CAP_EDGE profile 都完整 lower,F1--F12
history 也因上述单 body successor rule 完整执行并 RP 通过。它不覆盖一般 CAP_EDGE、CAP
edge 物理重放、COPY/boolean/pattern 后继或未证明的 region 选择。双向 blind extrusion
同样沿用单向 CAP 顺序:`isStart:true` 是主方向的反侧端盖,`isStart:false` 是主方向端盖;
因而 second depth 趋近于零时保持同一 contract,不按 primary/reverse 字段名称猜测。
`00247322` 的 F4 以 `z=40.3 mm` 的反侧 cap 附着 F6,F5 后该 F4 owner 仍有唯一
continuation,F7 的 CAP_EDGE hole profile 可执行。后续 F9 选择已被 F5 消耗的 F4 member,
故保持 `body_source_unavailable`,不烘焙 transform 回 F4 草图或替换为 aggregate body。
刷新的 `output/cap-edge-two-sided-frame-matrix-20260909` 三样本矩阵为 1 个
`rebuilt_approximate`(`00950564`)、1 个 `rebuilt_rejected`(`00803284`)和 1 个
`runtime_ineligible`(`00247322`,但 F1--F7 prefix 可执行)。共享 lowering 的固定核心
17 工件 `output/core-17-two-sided-cap-frame-final-20260909` 分类为 3 strict、3 RP、9 rejected、
1 rebuild failure、1 comparison timeout,与此前共享基线一致。受控全量 24 条 shard
`output/full-shard-two-sided-cap-frame-20260909` 记录 1 strict、2 RP、10 rejected、6 rebuild
failure、3 runtime-ineligible、1 timeout 和 1 无可执行 feature;其 conversion diagnostics
仍归因为已有的 `cPlane`、CAP_EDGE、fillet/chamfer、hole、revolve 与 shell selector 缺口,
没有将它们重分类为双向 CAP frame 成功或失败。
`INTERSECT` 的第一条通用受限路径现以 CDSL `planar_imprint` 保存同一草图的原始
analytic entities、IMPRINT 面侧、可选 INTERSECT vertex order 与 fragment side;solver
仅转换这些精确曲线,adapter 用 OCP `BOPAlgo_Splitter` 切分有界 support face 后选取实际
B-rep region。没有唯一 fragment、选中无界 support-boundary region、construction/source
alias、split failure 或不完整 query 都稳定拒绝,不把 B-spline/arc 采样成多边形,也不复用
整张草图。该路径覆盖 bounded line/arc/circle/ellipse/B-spline arrangement 的 shared
contract,但尚未覆盖不同 sketch、surface/topology producer、trim/copy/pattern 后继或完整
FeatureScript IMPRINT query grammar,因此仍是 P1 部分完成项。工件
`output/planar-imprint-intersect-20260909-runtime` 是 CAP_FACE 扩展前的历史工件:
`00082324` 与当时的 `00835610` 都保留可执行 STEP 但 `rp.passed == false`,`00071859` 和
`00093912` 分别保留既有 `TopoDS::Solid` 和 revolution-segmentation runtime failure。
当前 `output/drafted-cap-face-20260909` 已证明 `00835610` 在 drafted CAP_FACE continuation
与该 IMPRINT 后续均执行时 RP 通过;这不改变 `planar_imprint` 的其它失败分类,也不构成
该能力的完整覆盖。共享核心 17 回归
`output/core-17-planar-imprint-20260909` 分类保持 3 strict、2 RP、10 rejected、1
rebuild failure、1 comparison timeout。
对同一逻辑 circle source,solver 现在保留一条带 workplane frame 的 OCC circle edge,而不将
它预拆为四条 contour arc;fragment 的 directed successor/predecessor 由 splitter image 在
精确交点处的端点和原 curve 同向切向解析,避免周期参数接缝的数值排序歧义。无 fragment
的 source 代表全体 splitter descendants:只有其给定 face side 的每一个 B-rep face 都有界时,
adapter 才保留完整 face 集;任一 support-boundary face、非唯一 side 或 fragment 都拒绝,
不按 image 顺序/长度选择或静默省略。`output/intersect-circle-imprint-matrix-20260909-r2`
记录 `00001313` F1/F3 完整执行但 RP 拒绝(bbox delta `0.023571 mm`)、`00004246`
的 F1/F3 executable prefix 和三个仍有归因的失败。`00040869` 的 line/ellipse
`OD(0)` pairs 以 OCP exact distance 均为 `0.004240908 mm`,没有 splitter image 或
section edge,故 `output/intersect-circle-imprint-source-gap-20260909-r1` 保留 F1 prefix
并报告 source/anchor 无实际交点,绝不以容差补点;核心 17
`output/core-17-intersect-circle-imprint-20260909-r2` 为 4 strict、3 RP、9 rejected、1
comparison timeout。故这仍只是同草图有界 exact-curve arrangement 的 P1 扩展,不能宣称
general INTERSECT replay 已完成。
2. `UP_TO_NEXT` 已 lower 为 CDSL `through_next`,并有一个受限的 current-body
contract:所有 profile sample ray 都在当前 body 命中时,保留 profile 到首个正向
命中面之前的外部材料;只有部分 ray 命中时,以实际命中 face 做有限体层裁剪;没有
命中时明确拒绝,不退化为盲拉伸。`00192744` 的 F5/F10/F14 已由该 contract 完整
执行,但其独立 body 与 source STEP 的融合差异、后续 multi-body transform 和 source
history/STEP 不一致仍单独分类。这是部分完成,不代表复杂 profile、多 body target、
source body lifecycle 或第二方向终止已支持。
3. 在上述受限路径之外,完成 `up_to_surface`、`up_to_next`、`up_to_body`、
`up_to_vertex`、two-sided、through-all、surface/mixed 的有限交集语义和多 body
target 选择。
4. 仅在所有 profile rays、target body 和 offset 均能由通用几何验证时执行;否则保留
前缀并说明哪一个终止条件无法证明。
### P2:实体特征的完整语义
1. `extrude`、`revolve`、`loft`、`sweep`:补齐实体/曲面模式、复杂 profile、导轨、
section correspondence、twist/transition、path/axis selector 和结果 body。现有 direct
initial/new-body simple sweep 与 initial simple loft 现可将 builder-proven cap evidence
作为受限 CDSL output-role selector 供 downstream feature 消费;同一 contract 还支持
independent blind prism CAP_FACE 的 `extrude_from_face`。这些路径仍不能代替完整的
derived-topology selector 语义,CADFS lowering 也尚未以此表示一般 CAP/SWEPT query。
2. `shell`:单一 selected solid 的 direct-builder topology delta 与有限 output-role
evidence 已覆盖。CADFS lowering 现额外接受一个受限的 removal selector:直接 blind 或
two-sided linear extrusion 的立即后继 shell,可从同一未修改 source profile 中唯一的、非
construction `line` 原始实体生成平面 `SWEPT_FACE` placeholder,再由 prefix snapshot
唯一绑定。该规则要求 owner、profile、start/end frame、blind extent 和单 body
lifecycle 都可证明;revolve/sweep/loft、圆或其它非线性 source、changed profile、copy/
pattern/boolean/dress-up continuation 和多 target body 均继续诊断,不能回退到当前 body
或相近面。CADFS `oppositeDirection` 已显式 lower 为 `inward:false`,由既有 OCC
`MakeThickSolidByJoin` 的有符号 offset 执行;这只复用已验证的 removal selector 和单一
selected solid contract,不会将 OCC feasibility failure 伪装成 inward shell 成功。仍需任意
稳定 removal face、多 body target、其它 join/offset 参数语义和更广的 kernel failure
诊断回归。作为进一步的窄例外,紧邻 shell 的 `OFFSET_FACE` removal query 可在其
`disambiguationData` 中只有一个 direct `TDD` CAP source,且 source 是前序 direct
`new_body` blind-prism cap 时 lower 为 `shell.offset_face` 加 source-qualified output role。
没有 `TDD`、多个 source、draft/fused/COPY/pattern/boolean source、非邻接 owner 或无
active unique snapshot 都稳定诊断。`shell.parts` 现有一个独立且同样受限的 body-source
lifecycle contract:只有 parts query 可唯一解析为仍存活的 direct `SWEPT_BODY` independent
member 时,lowering 才写入 CDSL `target_feature_id`;capability preflight 和 runtime 都要求
该 ID 的 exact B-rep solid 仍在 active member graph 中,不能以 `session.body`、current
aggregate 或几何近似替代。`00107631` F3 中 F1 可证明地继续为 `f_F2`,因而是正例;
`00542223` F7 在 fuse/pattern 后不再有独立 member;runtime 仅在 pattern 前后都是一个
可执行实体且 source 唯一时保留 direct source 的 sole-body alias,因此 parts-scoped shell
可以验证为当前实体,而不把 aggregate 伪装成一个独立 member。此路径尚未覆盖 multi-part /
multi-body parts query、COPY、boolean 或 pattern instance 的通用 successor;这些情形必须
保留可执行前缀或既有运行路径,不能宣称 explicit target-body semantics。实现参考
SimpleCADAPI(Apache-2.0)`IsSame` member identity 语义,未复用代码;其单 Solid boolean
假设未被采用,因为它与 CADFS 的独立 body 生命周期不兼容。当前验证为:受影响
lowering/integration/selector/runtime suite `218 passed`(3 条既有 workplane warning);
`output/core-17-shell-parts-20260909` 为 3 strict、3 RP、9 rejected、1 rebuild failure、1
comparison timeout。`output/shell-parts-matrix-20260909` 的 21 实样本中,10 个 shell 写入
explicit `target_feature_id`,结果为 13 rebuilt-rejected、7 rebuild-failed、1 runtime-ineligible。
`00107631` 的 F3 在这项 target contract 下通过 converter/preflight。其普通、非 instance-locked
`CAP_FACE` selector 只有在 active owner candidates 均不满足完整 geometry signature 时,才可在
当前 active member 中选择唯一的 threshold-qualified face;这不能替代 `output_role` relation,
也不适用于 `owner_match_required` COPY provenance。因而 F3 的两张 removal faces 分别严格绑定
为 `f_F2:face:1` 和由 `(f_F1, extrude.start)` 证明的 `f_F2:face:5`。该 feature 随后由 OCC
报告 invalid thick-solid,而不是 selector-not-found;系统保留 F2 executable STEP prefix,不会
修改厚度、重选面或伪造 shell 成功。`00542223` 的完整 history 现可 rebuild,但仍因几何比较
rejected;这不是 RP 通过声明。`output/core-17-fused-sole-body-pattern-provenance-20260909-r2`
的新代码工件为 3 strict、3 RP、10 rejected、1 comparison timeout;五样本
`output/fused-sole-body-pattern-provenance-20260909-r2` 中 `00542223` 与 `00423838` 均为
rebuilt/rejected,`00293508` 保持 RP,`00253824` 仍在无关 `planar_imprint` 路径失败并保留
F3 prefix。这些结果不能作为 complete shell 或 RP 相似通过的证据。
无 geometry 的普通 context selector 同时保留既有的唯一 active same-kind record 回退;它只在
source owner 没有 active record、context 内候选唯一时成立。`owner_match_required` 的
COPY/instance selector 没有此路径,零 owner candidates 仍是 `selector_not_found`。该边界的
原子正反测试与完整 CADFS 测试均通过(11 个 selector test、118 个 CADFS test)。刷新后的
`output/core-17-selector-context-full-20260909` 仍为 3 strict、3 RP、9 rejected、1 rebuild
failure、1 comparison timeout;`output/shell-parts-selector-context-fallback-matrix-20260909` 的
21 条分类仍为 13 rebuilt-rejected、7 rebuild-failed、1 runtime-ineligible。这是 selector
绑定范围修正,不增加 shell 的已完成语义,也不改变 OCC invalid-shape 的保留前缀诊断。
3. `fillet`、`chamfer`、`hole`:完整 selector、tangent propagation、参数变体、
feasibility 检查和 dress-up 后继。当前 fillet/chamfer lowering 对 direct blind/
mid-plane extrusion 的唯一源端点,以及 direct two-section loft 的每 section 一对唯一
源端点,可生成可绑定的 endpoint-bbox `SWEPT_EDGE` selector;前者额外证明为直线,
后者不声称曲线类型,因为内核可将其物化为 B-spline。独立 `NEW` 的 full solid
revolve 也可在 profile 与 axis 都是同一原始草图的直接来源、original-set 唯一指向一
个非轴端点时,按 axis projection 生成 `circle_center_mm` / `radius_mm` 圆边签名;
adapter 只从实际 OCC circle edge 记录该签名。一个 `IMPRINT` profile 仅在 materialized
workplane 和完整 profile 与原草图完全相等时可使用同一来源证明;任何实际 region 选择
都不继承该 contract。该受限路径不覆盖 sweep、surface/partial/fused revolve、
multi-section loft、generated/trimmed/transformed profile 或 boolean/pattern 后继;任一
端点、axis 或 owner 不唯一时必须保留前缀并诊断。内核不能完成时不能伪造较小半径或不同
孔型。
4. `booleanBodies`、`mirror`、`circularPattern`:显式 source/target bodies、实例输出、
nested pattern、remove/intersect/keep-tools 和 transform 后继。当前 direct circular
`NEW` COPY -> rigid transform,以及 direct mirror 的 `NEW` source -> `instance 1`
COPY -> transform 是受限增量;不代表 linear/nested pattern、镜像 aggregate source 或
fused instance 已有完整 output body lifecycle。mirror 的 `mirrorPlane` 另有一个受限
`SWEPT_FACE` contract:唯一 direct full solid `revolve_add` 的原始 source line 与 revolve
axis 可由既有 `_query_plane` 证明为同一物理平面时,lowering 物化显式
`reference_plane` 后交给现有 `pattern_mirror` 执行。它不从 STEP 或 current body 推断
plane;partial revolve、非 revolve、curve/derived face 或任一未解析 frame 都保留原有
mirror-plane diagnostic。`00000385` 的 F5 是正例(XY plane,F5 executed);
`00023074` 保留拒绝边界。五样本
`output/mirror-revolve-plane-matrix-20260909` 为 2 RP、3 rejected,且只移除了
`00000385` F5 的 plane defer,F2 chamfer 与 F7 fillet 仍如实诊断。`booleanBodies`
现在也有一个独立、受限的 COPY body contract:direct `SWEPT_BODY` 和已存活的
mirror/circular `COPY(SWEPT_BODY)` 分别 lower 为 feature ID 和
`{pattern_feature_id, source_feature_id, instance_index}`,runtime/body graph 仅从该
tuple 解析对应独立 body member,绝不以 producer aggregate 或 current body 代替。
direct `SWEPT_BODY` 若唯一解析到上述 fused sole-body successor,也会以 successor 而非
creator 作为 circular source;`00253824` 的 F1 -> F3 cut -> F4 add -> F5 circular 的
source 因而是 `f_F4`,其 F6 targetless UNION 保留 `f_F4` 与 F5/F4/instance-1,2 的
明确 ownership。该完整样本目前在无关的 F4 `planar_imprint` runtime selection 失败时
保留 F3 executable STEP,不能作为 pattern/boolean RP 通过证据。合成 fused-body
circular COPY -> boolean runtime 回归完整执行。多 body、boolean 后继、nested/linear
pattern、被排除 instance 和 CAP/face topology ownership 仍未覆盖。
targetless FeatureScript `UNION` 只在 `keepTools:false` 且至少有两个 explicit members
时按 query 顺序确定一个 target;`00293508` 的 direct F1/F3 UNION 以 F1 target、F3 tool
完整执行并 RP 通过。targetless subtract/intersect、keep-tools、无效或已经吸收的 instance、
以及 multi-source transform COPY 仍保持拒绝。`00000385` 的 F6 因而以 `f_F1` 为 target、
F5/F1/instance-1 为 tool 完整 lower 并执行,F2/F7 保持其原有 diagnostics。四样本
`output/boolean-pattern-copy-provenance-matrix-20260909` 为 3 rejected、
1 runtime-ineligible;其中其它 sweep、keep-tools/subtract、shell/hole 和 selector 缺口均
未被重分类。新的 `output/core-17-boolean-pattern-copy-provenance-20260909` 仍为 3 strict、
3 RP、9 rejected、1 rebuild failure、1 comparison timeout。direct circular ADD 现将每个
transformed COPY 的面、边、顶点沿 exact OCC transform/fuse history 传入 final active snapshot;
只有一个 source 和一个 final result 的 relation 才传递 `f_pattern.cN.f_source` owner,
split/merge/no-history 仍不声明 owner。`00542223` 的 F7 removal faces 和 `00423838` 的
`COPY(CAP_FACE)` 因而都在 active snapshot 中绑定为真实 instance owner;后二者的最终 STEP
仍分别 rejected,不能将 selector 可执行性描述为几何相似通过。direct-new wedge 的原子回归同时
验证三个 copy owner 都出现在最终 fused body。nested/linear pattern、boolean 后继、被排除
instance 和没有 exact history 的 CAP/face ownership 仍未覆盖。
CADFS 的静态数值表达式还允许无歧义的 `round(N)`,但仅当 `N` 已是有限整数,因而该
表达式可被证明为恒等;`00003011` 的 `round(8)` pattern 由此完整 lower。非整数
`round`、变量和其它函数不会在 converter 中猜测 FeatureScript 的数值语义。这只是
lowering 前置条件,不能作为 COPY owner、pattern instance lifecycle 或 RP 相似通过的
证据。
5. `cPlane`:所有历史出现的 plane constructor、曲线/曲面 attachment、方向手性和
退化输入。`CPlaneType.OFFSET` 必须将 CADFS `oppositeDirection` 编译为 source plane
normal 上的负 signed offset,而不是忽略该 flag 或翻转 plane frame;后者会镜像同一草图的
local coordinates。该 shared lowering contract 已覆盖 272 条实际带该 flag 的 history。
`00777619` 将 Right plane 的 `76.2 mm` offset 修正为 `x=-76.2 mm`;因其 profile 只有部分
射线到达 selected `SWEPT_BODY`,`up_to_body` 仍以非均匀 target 诊断保留 F2 prefix,不能
被这一 plane 修复伪装成成功。Right/Front/Top 的三个真实 matrix 例与 24 条分层 shard
`output/cplane-offset-opposite-shard-20260909` 分别验证 frame 和独立分类:3 strict、1 RP、
13 rejected、7 rebuild failure。共享 core-17
`output/core-17-cplane-offset-opposite-20260909` 分类保持 3 strict、3 RP、9 rejected、1
rebuild failure、1 comparison timeout。它仍只是 cPlane 的一项通用 frame contract,不覆盖
曲面/曲线 attachment、退化输入或完整 derived-topology plane semantics。
### P3:尚未支持的 FeatureScript 操作
按全量诊断计数建立 operation registry,逐项为 `draft`、`thicken`、`split`、
`moveFace`、`deleteFace`、`replaceFace`、完整 `transform`、`derive`、`import`、
`bend_add` 建立 schema、lowering、runtime、adapter 和回归。外部资产或生成器缺失时,
必须将 capability 标为不可执行并提供部署诊断,不能在 import 时影响无关模型。
### P4:比较、来源和性能
1. 保留 strict 与 RP 两层几何比较,并为被精确量快速拒绝的边缘模型提供可控的
离线 surface 诊断预算;comparison timeout 是基础设施问题,不能混入几何失败。
2. 输出 source/rebuild 多视图、局部差异、body 体积/面积和最近 feature 的关联证据。
截图只用于人工复核,不能替代多视角 B-rep 比较。
3. 识别 source STEP 非闭合、FeatureScript literal 与 export 精度不一致、缺失资产等
source exception。它们必须保留原始导入/检查证据,不能修饰 CDSL 以伪造通过。
4. 使用 capability shard、缓存中间工件和可恢复 worker 隔离完成全量回归;任何 failure
都必须保留最后一个重新绑定 selector 并以 strict 模式实际执行成功的前缀 STEP、GLB
和诊断。不得把未绑定候选或当前 body fallback 伪装成可执行工件。
## 每项能力的完成定义
一项能力仅在满足以下条件时可标记完成:
1. 有版本化的 CDSL 表达,且 schema/semantic validation 拒绝不完整或歧义输入。
2. lowering 仅根据 FeatureScript source 产生该表达,并保留 source feature、selector
和 body provenance。仅有 runtime CDSL contract 而未由 lowering 产生的能力必须标为
部分完成。
3. runtime/adapter 按通用算法执行,记录结果 body 和 topology delta,不依赖样本信息。
4. 原子语义矩阵包含正向、边界和拒绝测试;至少多个真实语料样本覆盖不同几何和
生命周期组合。
5. 受影响核心集、扩展集和全量 shard 有可复现结果,工程相似通过率、失败数和剩余
exception 均更新到本地台账。
6. 代码审查确认没有 sample-specific 分支、gold STEP 参数回填、隐式默认尺寸或为
通过比较而降低 RP 阈值的行为。
## 执行节奏
每次改动依次执行:受影响原子测试 -> 受影响真实样本 -> 核心 17 -> 扩展能力矩阵 ->
对应全量 shard。完成一个 capability family 后,更新
`ENGINE_CAPABILITY_GAPS_PROGRESS.local.md`,记录新增的通用 contract、剩余边界、
样本列表、比较证据和下一优先级;该本地台账不提交 Git。
全量目标完成时,报告必须按“工程相似”“严格一致”“source exception”“未实现能力”
和“基础设施失败”分别给出分母、样本 ID、工件和原因。任何仍可执行的模型都必须
保留输出,不能因未满足最终几何目标而丢弃。
+29 -24
View File
@@ -3,11 +3,12 @@
## 目标
将 CADFS FeatureScript 历史稳定转换为 CDSL,由 engine 重建 STEP,并与
原始 STEP 做严格几何比较。近期交付门槛是 17 个代表性样本全部严格通过;
原始 STEP 做工程几何比较。近期交付门槛是 17 个代表性样本全部达到工程相似;
最终交付门槛是当前 CADFS 全量语料中的每个可比较模型都完成同一条链路。
这里的“重建成功”仅指 `comparison.json` 的 `strict.passed == true`,不是
仅生成 `rebuild.step`,也不是只通过 RP 宽松阈值。
这里的“重建成功”指 `comparison.json` 的 `rp.passed == true`,不是仅生成
`rebuild.step`。`strict.passed` 保留为高精度诊断指标:它用于发现源历史精度、
内核拓扑和局部几何差异,不再作为工程可用模型的唯一验收门槛。
本文件记录目标、阶段门槛和验收口径。实现进度、能力边界和每次代码变更
后的证据记录在本地台账
@@ -22,33 +23,34 @@
FeatureScript 操作、草图实体、已 lower 的 engine atomic 和未支持能力变体。
- 本地能力台账记录该池已有 `17/17 rebuilt` 的可执行 STEP 证据;当前 checked-in
manifest 保留的是历史选择时的 `3` 个 engine baseline / `17` 个 conversion
样本分类。因此,开始严格回归前必须重新生成并核对 manifest,不能将旧分类
样本分类。因此,开始工程回归前必须重新生成并核对 manifest,不能将旧分类
当作当前通过结果。
- 当前全量报告中仍有 `rebuild_failed`、`rebuild_timeout`、`comparison_timeout` 和
`rebuilt_rejected`。这些都是待消除的问题,不可因保留了可执行 STEP 而视为完成。
`comparison.json.decision = rejected`。这些都是待消除的问题;`approximate_pass`
是已重建且工程相似的模型,仍应保留 strict 差异证据供后续能力完善。
## 验收口径
一个样本必须同时具备以下工件和结果,才计入严格重建成功:
一个样本必须同时具备以下工件和结果,才计入工程重建成功:
1. `candidate.cdsl.json` 通过 schema 和语义验证,且没有以跳过后续几何换取成功。
2. `bound.cdsl.json`(若样本需要 selector 绑定)可复现产生,绑定证据可追溯到
feature、owner 和 topology snapshot。
3. `rebuild.step` 由完整的 CDSL feature history 生成;中间失败不能被最终可执行
前缀掩盖。
4. `comparison.json` 的 `decision` 为 `strict_pass`,并同时满足:
`surface_max <= 0.01 mm`、`surface_p99 <= 0.01 mm`、
`bbox_max_delta <= 0.01 mm`、体积和表面积相对误差均不超过 `1e-5`,以及实体数相等。
4. `comparison.json` 的 `decision` 为 `strict_pass` 或 `approximate_pass`,并满足
RP 工程阈值:`surface_max <= 0.02 mm`、`surface_p99 <= 0.02 mm`、
`bbox_max_delta <= 0.02 mm`、体积和表面积相对误差均不超过 `0.005`,以及实体数相等。
5. `status.json`、转换诊断、重建报告和比较报告保存于该样本目录;可执行但不相似
的模型必须保留,而不能在失败时删除。
`approximate_pass` 仅用于定位接近结果,不能通过本计划的 17 样本或全量目标。
缺失原始 STEP、源数据损坏或外部资产不可取得时,必须单独列为 source exception,
附原始证据;不得伪装成 engine 或 converter 已完成。
严格比较失败不自动表示 engine 或 converter 失败。缺失原始 STEP、源数据损坏或
外部资产不可取得时,必须单独列为 source exception,附原始证据;不得伪装成
engine 或 converter 已完成。
## 第一阶段:17 个代表样本
阶段完成条件:下面 17 个样本全部以 `--compare-mode strict` 重新执行并严格通过,
阶段完成条件:下面 17 个样本全部以 `--compare-mode rp` 重新执行并达到工程相似,
同时 manifest 的覆盖标签与当前输出一致。每次修复只针对实际失败根因扩展能力;
一个样本可能由多个能力共同阻塞。
@@ -60,7 +62,7 @@
| `00111611` | extrude、revolve、fillet、chamfer | 复杂草图、回转轴、圆角和倒角的拓扑稳定性。 |
| `00129362` | cPlane、extrude、revolve | 可复用基准面 frame、回转 add/cut。 |
| `00159804` | circularPattern、through-all add | 环形阵列 replay、`line_angle` 和 through-all 加料。 |
| `00192744` | circularPattern、extrude | 已执行 `curve_point` 附着 frame、`up_to_next` 和 pattern copy deletion;仍缺多 body transform 与 source/STEP 一致性证据。 |
| `00192744` | circularPattern、extrude、transform | 已执行 `curve_point` 附着 frame、`up_to_next`、pattern copy deletion,以及 direct circular `COPY(BODY)` 的 multi-body transform;仍缺完整 pattern/body lifecycle 与 source/STEP 一致性证据。 |
| `00212904` | shell、two-sided cut | shell、`three_point`、双向切除终止条件。 |
| `00287955` | loft、cPlane、revolve | 多 profile loft、`cap_edge`、`mid_plane`。 |
| `00423838` | circularPattern、extrude | `up_to_vertex` 与 two-sided cut 的非盲终止。 |
@@ -76,7 +78,7 @@
```bash
PYTHONPATH=backend:. python -m cadfs_to_cdsl regression \
--tier all --stage pipeline --compare-mode strict --timeout-seconds 60
--tier all --stage pipeline --compare-mode rp --timeout-seconds 60
```
命令结束后应检查 17 个 `comparison.json`,而不是只检查 CLI 的进程退出码。
@@ -84,7 +86,7 @@ PYTHONPATH=backend:. python -m cadfs_to_cdsl regression \
## 能力完成目标
能力的完成定义为同一能力的 CADFS lowering、CDSL contract、runtime/adapter、
selector 或 body 语义(需要时)、严格比较回归均具备。仅实现其中一层时只能记为
selector 或 body 语义(需要时)、工程比较回归均具备。仅实现其中一层时只能记为
“部分完成”。优先级依据全量影响数和 17 样本依赖关系确定。
### P0:使特征历史成为完整实体
@@ -139,7 +141,7 @@ SimpleCADAPI 强制将 boolean 结果收敛为单个 `Solid`,不满足 CADFS
## Source Exception Evidence
严格重建失败必须先按下表归类。只有“已验证 source exception”能够从全量
工程重建失败必须先按下表归类。只有“已验证 source exception”能够从全量
成功分母移除;候选项仍是未完成样本,必须保留 source、CDSL、STEP 和比较工件。
| 分类 | 样本 | 当前证据 | 处理要求 |
@@ -148,13 +150,15 @@ SimpleCADAPI 强制将 boolean 结果收敛为单个 `Solid`,不满足 CADFS
| engine / converter 缺陷(受限路径已修复) | `00710855` | F5 的 CADFS IMPRINT cut 已由错误圆盘改为 `r=19..25.5 mm` 环。对于 F10,runtime 仅在显式同轴 surface shell 在 selected plane 提供内边界证据时,才以源 `width=10 mm` 构造受限圆锥倒角;完整 history 已可执行。 | 将该受限模式扩展为一般 surface/solid split、topology delta 和后继 selector;不得缩小源倒角宽度。 |
| source / STEP 精度候选 | `00710855` | 完整 history 的 surface 最大差为 `0.0035 mm`,面积、bbox 与实体数满足 strict;但 source F8 E14 是 `r=22 mm`,gold STEP 对应圆柱是 `r=21.9965 mm`,导致体积相对误差 `7.998e-5` 超过 strict。 | 保存 source literal 与 STEP 测量证据;未获得 source export 版本证明前不得回填 gold 半径,也不得从验收分母移除。 |
| source / STEP 差异候选 | `00035682` | FeatureScript 圆柱/孔径 literal 的精度低于原 STEP 的测量值;现有拓扑类型和数量相同,但严格体积/面积不一致。 | 保存原始精度证据;复核 source 导出版本后才能定为 exception。 |
| source / STEP 差异候选 | `00192744` | F3/F5 的 frame 与 `UP_TO_NEXT` 已修复,F7/F12 的 `COPY(BODY)` 删除也已 replay,完整 history 可执行且 bbox 对齐;但 source F15 将 F1 移动约 `-10000 mm`,gold STEP 仍位于约 `y=[-500,500]`。按 source 缺省 `NEW` 保留 8 个实体,gold 只有 1 个,体积相对误差仍为 `1.001e-3`。 | 保持严格失败并完成通用 multi-body transform/body lifecycle;不以 gold STEP 反推、伪造 transform 参数或把 source `NEW` 改成 `ADD`。 |
| source / STEP 差异候选 | `00192744` | F3/F5 frame、`UP_TO_NEXT`、F7/F12 `COPY(BODY)` delete 及 F15 的 direct circular COPY multi-body transform 均已完整执行,零 runtime diagnostic。F15 将 F1/F5/F6 COPY 移动约 `-10000 mm` 后,未被选择的 source `NEW` members 留在原处;当前 rebuild 有 22 个实体、gold STEP 只有 1 个,面积误差约 `3.3%`,RP 失败。 | 保留 executable STEP 和 comparison;继续完成 fused/add、nested、linear/mirror COPY 与 boolean/dress-up 后继的完整 body lifecycle。不得从 gold STEP 回推 union、transform 参数或把 source `NEW` 改为 `ADD`。 |
| source / STEP 差异候选 | `00212904` | 原 STEP 出现 source FeatureScript history 未表达的正交 cut strip。 | 保留对比工件;除非 source 能提供缺失 feature,不得在 converter 伪造几何。 |
| source / STEP 精度候选 | `00287955` | 完整 history 已执行并达到 RP。source STEP 的 F1 顶面为 `z=8.128 mm`,FeatureScript 只有 `8.13 mm`;F3、F5/F6 和 F14 的后续面偏差累计到 `0.002-0.008 mm`,最终体积相对误差为 `1.793e-4`。 | 保留 CDSL 与比较工件;不得根据 source STEP 回填未在 FeatureScript 中出现的精确尺寸。 |
| source / STEP 拓扑候选 | `00542223` | 开放 B-spline F2 已作为 reference 草图完整 lower,完整 history 无诊断并重建为 `1` 个实体、`49775.5119 mm3`。source STEP 虽声明 `MANIFOLD_SOLID_BREP` / `CLOSED_SHELL`,OCC 却导入为 `TopAbs_SHELL`、`0 solids`,并报告 `BRepCheck_NotClosed`。 | 保留严格失败;需要可信 source STEP 或中间 B-rep 后,才能验证完整 shell/mixed-body 语义。 |
| source / STEP 精度候选 | `00835610` | 完整 history 可执行且 RP 通过,严格最大表面差约 `0.014 mm`;FeatureScript 值与 STEP 存在约 `0.014 mm` 的 literal 差异。 | 不能从 gold STEP 回填尺寸;继续标为严格失败,等待 source 版本证据。 |
## 全量完成门槛
在 17 样本严格通过后,按 capability family 对全量语料滚动运行,并按失败原因选择
在 17 样本工程通过后,按 capability family 对全量语料滚动运行,并按失败原因选择
新的最小代表样本。最终报告必须以扫描快照中的全部样本为分母,同时满足:
1. 每个有效源样本生成语义有效、可执行的 CDSL;`parse_failed`、
@@ -162,8 +166,8 @@ SimpleCADAPI 强制将 boolean 结果收敛为单个 `Solid`,不满足 CADFS
feature 而导致的 `converted_partial` 均为零。
2. 每个拥有原始 STEP 的样本生成完整 `rebuild.step`;`rebuild_failed` 和
`rebuild_timeout` 均为零。
3. 每个可比较样本严格通过比较;`comparison_timeout`、`rebuilt_rejected` 和仅
`rebuilt_approximate` 均不计入完成。
3. 每个可比较样本以 RP 工程阈值通过比较;`comparison_timeout`、
`comparison.json.decision = rejected` 和仅有可执行 STEP 的样本均不计入完成。
4. 任何合法 source exception 都有单独清单、原始证据和可复现原因;它不进入成功
分母,也不得吞并为“未分类失败”。
5. 生成全量 capability-gap、比较汇总和逐样本证据,确保任一退化都可定位到
@@ -181,15 +185,16 @@ SimpleCADAPI 强制将 boolean 结果收敛为单个 `Solid`,不满足 CADFS
问题。证据不足时不得猜测分类。
3. 只在责任层修复:转换问题修改 CADFS lowering;执行问题扩展 engine;两者都涉及
时分开提交证据和测试。保留最新可执行 STEP/GLB 和失败诊断。
4. 每项代码改动至少重跑受影响样本、17 样本严格回归和相应单元测试;能力状态与
4. 每项代码改动至少重跑受影响样本、17 样本工程回归和相应单元测试;能力状态与
证据同步到本地能力台账。全量统计变化后重新生成 `regression/manifest.json`。
5. 不以关闭诊断、跳过 feature、降低 strict 阈值或用未说明的默认尺寸换取通过。
5. 不以关闭诊断、跳过 feature、用未说明的默认尺寸或放宽 RP 阈值换取通过;严格
指标的变化必须作为诊断证据单独说明。
## 阶段交付物
| 阶段 | 必须交付 | 通过条件 |
| --- | --- | --- |
| R0:证据稳定 | 刷新的 regression manifest、17 份完整工件 | 每个样本可重复运行并可定位当前失败。 |
| R1:代表样本 | 17 个严格比较报告、能力台账更新 | `17/17 strict_pass`。 |
| R1:代表样本 | 17 个工程比较报告、能力台账更新 | `17/17 rp.passed`。 |
| R2:能力扩展 | 按能力 family 的 converter/engine/selector 实现及回归 | 对应缺口不再造成该 family 的失败。 |
| R3:全量验证 | 全量 CDSL、STEP、比较结果和汇总报告 | 满足“全量完成门槛”的五项条件。 |
+9 -1
View File
@@ -24,7 +24,15 @@ def _assess(report: dict[str, Any], limits: dict[str, float]) -> dict[str, Any]:
def compare_steps(gold_step: Path, rebuilt_step: Path, *, surface_tessellation_mm: float = 0.05) -> dict[str, Any]:
from onshape_to_cdsl.compare import strict_compare
# ``onshape_to_cdsl`` is a sibling src-layout package. CADFS commands
# intentionally require only ``PYTHONPATH=backend:.`` so they must work
# from a source checkout as well as an installed package.
try:
from onshape_to_cdsl.compare import strict_compare
except ModuleNotFoundError as error:
if error.name != "onshape_to_cdsl.compare":
raise
from onshape_to_cdsl.src.onshape_to_cdsl.compare import strict_compare
raw = strict_compare(
gold_step,
rebuilt_step,
+2205 -171
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -117,6 +117,12 @@ def rebuild_one(sample: Sample, output: Path, *, force: bool = False, timeout_se
if outcome == "completed":
result = read_json(worker_result); worker_result.unlink(missing_ok=True)
bound_cdsl = result.pop("bound_cdsl", None)
prefix = result.get("last_executable_prefix")
if isinstance(prefix, dict):
prefix_bound_cdsl = prefix.pop("bound_cdsl", None)
if bound_cdsl is None and prefix_bound_cdsl is not None:
bound_cdsl = prefix_bound_cdsl
prefix["bound_feature_count"] = prefix.get("feature_count")
if bound_cdsl is not None: write_json(directory / "bound.cdsl.json", bound_cdsl)
else:
step_path.unlink(missing_ok=True)
@@ -144,6 +150,10 @@ def compare_one(sample: Sample, output: Path, *, force: bool = False, compare_mo
message = f"comparison exceeded {timeout_seconds:g} seconds" if outcome == "timeout" else "comparison worker exited without a result"
status["comparison_error"] = {"type": error_type, "message": message}; write_json(status_path, status); return status
else: comparison = read_json(comparison_path)
# A forced retry can recover from a transient worker timeout. The final
# status must describe the successful comparison rather than retaining a
# stale infrastructure failure beside an accepted result.
status.pop("comparison_error", None)
status["comparison_decision"] = comparison["decision"]
accepted = comparison[compare_mode]["passed"]
status["status"] = "rebuilt_strict" if comparison["strict"]["passed"] else "rebuilt_approximate" if accepted else "rebuilt_rejected"
+49 -1
View File
@@ -1,9 +1,52 @@
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
import re
from typing import Any
def _last_executable_prefix(
cdsl: dict[str, Any],
failed_feature_id: str | None,
output: Path,
) -> dict[str, Any] | None:
"""Bind and export the longest verified prefix before a failed feature."""
from .selector_binding import bind_candidate_selectors
from engine.cdsl_engine.runtime import rebuild_cdsl
features = list(cdsl.get("features") or [])
failed_index = next(
(index for index, feature in enumerate(features) if feature.get("id") == failed_feature_id),
len(features),
)
for feature_count in range(failed_index, 0, -1):
prefix = deepcopy(cdsl)
prefix["features"] = features[:feature_count]
try:
bound_prefix, _binding = bind_candidate_selectors(prefix)
result = rebuild_cdsl(bound_prefix, output, strict=True)
except Exception:
continue
return {
"failed_feature_id": failed_feature_id,
"feature_count": feature_count,
"last_feature_id": str(features[feature_count - 1].get("id") or ""),
"bound_cdsl": bound_prefix,
"result": result,
}
return None
def _failed_feature_id(error: Exception) -> str | None:
diagnostic = getattr(error, "diagnostic", None)
feature_id = getattr(diagnostic, "feature_id", None)
if isinstance(feature_id, str) and feature_id:
return feature_id
match = re.match(r"([^:\s]+): ", str(error))
return match.group(1) if match else None
def rebuild_candidate(cdsl: dict[str, Any], output: Path) -> dict[str, Any]:
from engine.cdsl_engine.runtime import analyze_cdsl, rebuild_cdsl
from .selector_binding import bind_candidate_selectors
@@ -11,6 +54,7 @@ def rebuild_candidate(cdsl: dict[str, Any], output: Path) -> dict[str, Any]:
analysis_dict = analysis.as_dict() if hasattr(analysis, "as_dict") else {"runtime_eligible": analysis.runtime_eligible}
if not analysis.runtime_eligible:
return {"status": "runtime_ineligible", "analysis": analysis_dict}
bound: dict[str, Any] | None = None
try:
bound, binding = bind_candidate_selectors(cdsl)
result = rebuild_cdsl(bound, output, strict=True)
@@ -18,4 +62,8 @@ def rebuild_candidate(cdsl: dict[str, Any], output: Path) -> dict[str, Any]:
except Exception as exc:
detail = {"type": type(exc).__name__, "message": str(exc)}
if hasattr(exc, "selector_resolutions"): detail["selector_resolutions"] = exc.selector_resolutions
return {"status": "rebuild_failed", "analysis": analysis_dict, "error": detail}
prefix = _last_executable_prefix(cdsl, _failed_feature_id(exc), output)
result = {"status": "rebuild_failed", "analysis": analysis_dict, "error": detail}
if bound is not None: result["bound_cdsl"] = bound
if prefix is not None: result["last_executable_prefix"] = prefix
return result
+73 -4
View File
@@ -10,7 +10,7 @@ from typing import Any
def _score(expected: dict[str, Any], actual: dict[str, Any]) -> float | None:
scores: list[float] = []
reversed_plane_normal = False
for key in ("center_mm", "start_mm", "end_mm", "normal", "axis_origin_mm", "axis_direction"):
for key in ("center_mm", "circle_center_mm", "start_mm", "end_mm", "normal", "axis_origin_mm", "axis_direction"):
if key in expected:
# plane_offset_mm 与平面方程绑定,必须使用记录平面方程时采用的
# plane_normal。face 的局部采样 normal 在 OCC 中可能与其相反。
@@ -101,6 +101,7 @@ def _bound_selector(placeholder: dict[str, Any], records: list[dict[str, Any]])
owners = record.get("owner_feature_ids") or [record.get("feature_id")]
selector = {"kind": placeholder["kind"], "owner_feature_id": str(owners[0]), "stable_id": record["record_id"], "snapshot_id": record["record_id"], "source": "runtime_snapshot", "confidence": 1.0, "geometry": record.get("geometry") or {}}
if placeholder.get("binding_feature_id") is not None: selector["binding_feature_id"] = placeholder["binding_feature_id"]
if placeholder.get("owner_match_required"): selector["owner_match_required"] = True
bound.append(selector)
if placeholder.get("match_mode") == "all":
selector = dict(placeholder); selector["matched_selectors"] = bound
@@ -150,23 +151,76 @@ def bind_candidate_selectors(cdsl: dict[str, Any]) -> tuple[dict[str, Any], list
resolved = []
for placeholder in targets:
records = prefix_records(placeholder.get("binding_feature_id"), placeholder.get("owner_feature_id"))
output_role = str(placeholder.get("output_role") or "").strip()
if output_role:
# Builder output roles are not geometry placeholders. They
# remain in the bound CDSL so runtime can resolve the
# current active B-rep face through exact kernel history.
# Replacing one with stable_id/geometry would mix evidence
# and make a stale snapshot appear durable.
owner = placeholder.get("owner_feature_id")
role_source = placeholder.get("output_role_source")
source_owner = role_source.get("owner_feature_id") if isinstance(role_source, dict) else None
source_role = role_source.get("output_role") if isinstance(role_source, dict) else None
candidates = [
record for record in records
if record.get("kind") == placeholder.get("kind")
and owner in (record.get("owner_feature_ids") or [record.get("feature_id")])
and output_role in (record.get("output_roles") or [])
and (
role_source is None
or any(
item.get("output_role") == output_role
and item.get("owner_feature_id") == source_owner
and item.get("source_output_role") == source_role
for item in record.get("output_role_sources") or []
)
)
]
if len(candidates) != 1:
status = "not_found" if not candidates else "ambiguous"
raise ValueError(
f"{feature['id']}: selector_output_role_{status} after prefix rebuild"
)
resolved.append(candidates[0])
continue
geometry = placeholder.get("geometry") or {}
candidates = _circle_records(geometry, records) if geometry.get("source_circle_radius_mm") else []
if not candidates:
same_kind = [record for record in records if record.get("kind") == placeholder.get("kind")]
owner = placeholder.get("owner_feature_id")
owner_matches = [record for record in same_kind if owner in (record.get("owner_feature_ids") or [record.get("feature_id")])]
pool = owner_matches or same_kind
# Pattern instance provenance is stronger than an ordinary
# feature owner: an unresolved instance cannot fall back to
# an aggregate face with matching geometry. Ordinary source
# selectors retain their established geometry-binding path
# across topology-changing dress-up operations.
pool = owner_matches if placeholder.get("owner_match_required") else (owner_matches or same_kind)
if not geometry:
# Context selectors (notably a generated mirror plane)
# may have no geometric snapshot. Their owner-qualified
# singleton identity is sufficient and must not be scored
# as a zero-information geometric match.
if len(pool) != 1:
raise ValueError(f"{feature['id']}: selector_ambiguous after prefix rebuild")
status = "not_found" if not pool else "ambiguous"
raise ValueError(f"{feature['id']}: selector_{status} after prefix rebuild")
candidates = [pool[0]]
else:
scored = [(score, record) for record in pool if (score := _score(geometry, record.get("geometry") or {})) is not None and score >= 0.8]
# A source owner is preferred for ordinary geometry
# selectors, but can be retained by an unrelated exact
# continuation after a dress-up. If none of that
# owner's active records satisfies the full geometry
# signature, bind against the current active body and
# still require a unique threshold-qualified match.
# Instance-qualified selectors never take this path.
if not scored and not placeholder.get("owner_match_required"):
scored = [
(score, record)
for record in same_kind
if (score := _score(geometry, record.get("geometry") or {})) is not None
and score >= 0.8
]
scored.sort(key=lambda value: (-value[0], str(value[1].get("record_id"))))
if scored:
if placeholder.get("match_mode") != "all" and len(scored) > 1 and abs(scored[0][0] - scored[1][0]) <= 1e-9:
@@ -177,7 +231,22 @@ def bind_candidate_selectors(cdsl: dict[str, Any]) -> tuple[dict[str, Any], list
placeholder.clear(); placeholder.update(selector)
resolved.extend(bound_selectors)
feature_selectors = feature.get("selectors") or []
unique = {selector["stable_id"]: selector for selector in feature_selectors}; feature["selectors"] = list(unique.values())
def selector_key(selector: dict[str, Any]) -> tuple[Any, ...]:
stable_id = selector.get("stable_id")
if stable_id is not None:
return ("stable_id", str(stable_id))
source = selector.get("output_role_source")
return (
"output_role",
selector.get("owner_feature_id"),
selector.get("kind"),
selector.get("output_role"),
source.get("owner_feature_id") if isinstance(source, dict) else None,
source.get("output_role") if isinstance(source, dict) else None,
)
unique = {selector_key(selector): selector for selector in feature_selectors}
feature["selectors"] = list(unique.values())
if feature.get("atomic_id") == "pattern_mirror":
planes = [selector for selector in feature["selectors"] if selector.get("kind") == "plane"]
if len(planes) != 1:
+71
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from copy import deepcopy
import tempfile, unittest
from pathlib import Path
from cadfs_to_cdsl.compare import compare_steps
@@ -130,6 +131,32 @@ class IntegrationTests(unittest.TestCase):
self.assertEqual(outcome["status"], "rebuilt")
self.assertEqual(outcome["result"]["solid_count"], 2)
def test_outward_cap_shells_lower_and_rebuild_prefixes(self):
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
cases = (("00090436", 2.5), ("00107631", 3.8))
for sample_id, thickness in cases:
with self.subTest(sample_id=sample_id):
feature = root / "featurescript_rp" / sample_id[:4] / f"{sample_id}.txt"
if not feature.exists(): self.skipTest("CADFS sample is not installed")
result = lower_model(parse_featurescript(feature.read_text(), sample_id), {})
shell_index, shell = next(
(index, item)
for index, item in enumerate(result.cdsl["features"])
if item["id"] == "f_F2"
)
self.assertEqual(shell["atomic_id"], "shell")
self.assertEqual(shell["params"], {"thickness_mm": thickness, "inward": False})
self.assertNotIn("shell_outward", {item.get("capability") for item in result.diagnostics})
prefix = deepcopy(result.cdsl)
prefix["features"] = prefix["features"][:shell_index + 1]
with tempfile.TemporaryDirectory() as tmp:
outcome = rebuild_candidate(prefix, Path(tmp) / "outward-shell.step")
self.assertEqual(outcome["status"], "rebuilt")
self.assertEqual(outcome["result"]["solid_count"], 1)
def test_sweep_00542223_preserves_its_open_bspline_path(self):
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
feature = root / "featurescript_rp/0054/00542223.txt"
@@ -137,13 +164,57 @@ class IntegrationTests(unittest.TestCase):
result = lower_model(parse_featurescript(feature.read_text(), "00542223"), {})
sweep = next(item for item in result.cdsl["features"] if item["id"] == "f_F5")
segment = sweep["params"]["path"]["segment"]
self.assertEqual(result.status, "converted_complete")
self.assertEqual(sweep["atomic_id"], "sweep_add")
self.assertEqual(sweep["sketch_id"], "sketch_F4")
self.assertEqual(segment["points"], [[-40.0, -50.0], [-20.0, -14.96], [0.0, 5.0]])
self.assertEqual(segment["start_tangent"], [27.94, 92.49])
self.assertEqual(segment["end_tangent"], [52.88, 49.52])
self.assertNotIn("F2", [item.get("feature_id") for item in result.diagnostics])
self.assertNotIn("F5", [item.get("feature_id") for item in result.diagnostics])
def test_circular_pattern_00542223_preserves_all_sweep_arms(self):
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
feature = root / "featurescript_rp/0054/00542223.txt"
if not feature.exists(): self.skipTest("CADFS sample is not installed")
result = lower_model(parse_featurescript(feature.read_text(), "00542223"), {})
candidate = dict(result.cdsl)
candidate["features"] = result.cdsl["features"][:4]
from engine.cdsl_engine.runtime import rebuild_cdsl
with tempfile.TemporaryDirectory() as tmp:
rebuilt = rebuild_cdsl(candidate, Path(tmp) / "pattern.step")
self.assertEqual(rebuilt["solid_count"], 1)
bbox = rebuilt["bbox_mm"]
self.assertAlmostEqual(bbox["min"][0], -52.14101625137758)
self.assertAlmostEqual(bbox["min"][1], -57.500000100000065)
self.assertAlmostEqual(bbox["max"][0], 52.14101625137762)
self.assertAlmostEqual(bbox["max"][1], 37.5000001000001)
def test_fused_body_circular_copy_faces_bind_and_shell_00542223(self):
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
feature = root / "featurescript_rp/0054/00542223.txt"
if not feature.exists(): self.skipTest("CADFS sample is not installed")
candidate = lower_model(parse_featurescript(feature.read_text(), "00542223"), {}).cdsl
with tempfile.TemporaryDirectory() as tmp:
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
self.assertEqual(outcome["status"], "rebuilt")
result = outcome["result"]
self.assertIn("f_F7", [item["feature_id"] for item in result["feature_results"]])
shell_selectors = [
item for item in result["selector_resolution"]
if item["feature_id"] == "f_F7"
]
self.assertEqual(
{item["selector"]["owner_feature_id"] for item in shell_selectors},
{"f_F5", "f_F6.c1.f_F5", "f_F6.c2.f_F5", "f_F1"},
)
self.assertTrue(all(item["status"] == "resolved" for item in shell_selectors))
def test_face_chamfer_ignores_periodic_seams_00111611(self):
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
feature = root / "featurescript_rp/0011/00111611.txt"
File diff suppressed because it is too large Load Diff
+135 -20
View File
@@ -1,10 +1,14 @@
from __future__ import annotations
from copy import deepcopy
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from cadfs_to_cdsl.featurescript_parser import parse_featurescript
from cadfs_to_cdsl.lowering import lower_model
from cadfs_to_cdsl.rebuild import rebuild_candidate
from cadfs_to_cdsl.selector_binding import _score, bind_candidate_selectors
@@ -33,6 +37,66 @@ class SelectorBindingTests(unittest.TestCase):
def test_empty_snapshot_score_is_not_treated_as_a_match(self) -> None:
self.assertEqual(_score({}, {"normal": [0.0, 0.0, 1.0]}), 0.0)
def test_geometry_free_context_selector_uses_unique_active_record_when_owner_is_stale(self) -> None:
cdsl = {
"features": [
{"id": "f_source", "atomic_id": "reference_plane"},
{
"id": "f_mirror",
"atomic_id": "pattern_mirror",
"params": {},
"selectors": [{"kind": "plane", "owner_feature_id": "f_source"}],
},
],
}
report = {
"feature_results": [],
"topology_records": [{
"kind": "plane",
"record_id": "context:plane:1",
"owner_feature_ids": ["f_live_plane"],
"geometry": {},
}],
}
with patch("engine.cdsl_engine.runtime.rebuild_cdsl", return_value=report):
bound, _ = bind_candidate_selectors(cdsl)
selector = bound["features"][1]["selectors"][0]
self.assertEqual(selector["owner_feature_id"], "f_live_plane")
self.assertEqual(selector["stable_id"], "context:plane:1")
self.assertEqual(bound["features"][1]["params"]["mirror_plane"], selector)
def test_geometry_free_instance_selector_does_not_fall_back_to_active_record(self) -> None:
cdsl = {
"features": [
{"id": "f_source", "atomic_id": "reference_plane"},
{
"id": "f_mirror",
"atomic_id": "pattern_mirror",
"params": {},
"selectors": [{
"kind": "plane",
"owner_feature_id": "f_source",
"owner_match_required": True,
}],
},
],
}
report = {
"feature_results": [],
"topology_records": [{
"kind": "plane",
"record_id": "context:plane:1",
"owner_feature_ids": ["f_live_plane"],
"geometry": {},
}],
}
with patch("engine.cdsl_engine.runtime.rebuild_cdsl", return_value=report):
with self.assertRaisesRegex(ValueError, "f_mirror: selector_not_found after prefix rebuild"):
bind_candidate_selectors(cdsl)
def test_swept_face_area_lower_bound_rejects_coplanar_fragment(self) -> None:
expected = {"normal": [0.0, 1.0, 0.0], "plane_offset_mm": 54.69, "minimum_area_mm2": 285.0}
self.assertIsNone(_score(expected, {"normal": [0.0, 1.0, 0.0], "plane_offset_mm": 54.69, "area_mm2": 0.64}))
@@ -50,28 +114,79 @@ class SelectorBindingTests(unittest.TestCase):
self.assertEqual(score, 1.0)
def test_intersection_vertex_binds_pattern_and_current_body_prefixes(self) -> None:
def test_cap_face_output_role_is_validated_without_snapshot_rebinding(self) -> None:
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
feature = root / "featurescript_rp/0001/00016195.txt"
if not feature.exists(): self.skipTest("CADFS sample is not installed")
candidate = lower_model(parse_featurescript(feature.read_text(), "00016195"), {})
bound, evidence = bind_candidate_selectors(candidate.cdsl)
selector = next(item for item in bound["features"] if item["id"] == "f_F3")["selectors"][0]
self.assertEqual(selector, {
"kind": "face", "owner_feature_id": "f_F1", "output_role": "extrude.end",
"source": "runtime_snapshot", "confidence": 1.0,
})
self.assertNotIn("stable_id", selector)
self.assertNotIn("snapshot_id", selector)
self.assertNotIn("geometry", selector)
binding = next(item for item in evidence if item["feature_id"] == "f_F3")
self.assertEqual(len(binding["resolved"]), 1)
self.assertEqual(binding["resolved"][0]["output_roles"], ["extrude.end"])
def test_shell_offset_face_role_binds_only_its_true_dependency_source(self) -> None:
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
feature = root / "featurescript_rp/0010/00107631.txt"
if not feature.exists(): self.skipTest("CADFS sample is not installed")
candidate = lower_model(parse_featurescript(feature.read_text(), "00107631"), {})
self.assertEqual(candidate.status, "converted_complete")
# F2 retains F1's end-cap provenance, but F3 also selects the distinct
# start-cap plane. Its direct owner has no geometry-qualified active
# candidate, so binding must consider the unique active F2 face without
# weakening the exact qualified OFFSET_FACE role.
prefix = deepcopy(candidate.cdsl)
prefix["features"] = prefix["features"][:3]
bound, evidence = bind_candidate_selectors(prefix)
first, offset = bound["features"][-1]["selectors"]
self.assertEqual(first["owner_feature_id"], "f_F2")
self.assertEqual(first["stable_id"], "body:f_F2:face:1")
self.assertEqual(first["snapshot_id"], "body:f_F2:face:1")
self.assertEqual(first["source"], "runtime_snapshot")
self.assertEqual(first["confidence"], 1.0)
self.assertEqual(offset, {
"kind": "face", "owner_feature_id": "f_F2", "output_role": "shell.offset_face",
"output_role_source": {"owner_feature_id": "f_F1", "output_role": "extrude.start"},
"source": "runtime_snapshot", "confidence": 1.0,
})
binding = next(item for item in evidence if item["feature_id"] == "f_F3")
self.assertEqual(binding["resolved"][0]["snapshot_id"], "body:f_F2:face:1")
self.assertEqual(binding["resolved"][1]["record_id"], "body:f_F2:face:5")
# Binding must not hide the OCC feasibility boundary. The requested
# second shell currently produces an invalid shape, so preserve the
# F2 prefix instead of changing thickness or removal faces.
with tempfile.TemporaryDirectory() as directory:
outcome = rebuild_candidate(candidate.cdsl, Path(directory) / "00107631.step")
self.assertEqual(outcome["status"], "rebuild_failed")
self.assertEqual(outcome["error"]["type"], "RuntimeExecutionError")
self.assertEqual(outcome["error"]["message"], "OCC shell operation produced an invalid shape")
self.assertEqual(outcome["last_executable_prefix"]["failed_feature_id"], "f_F3")
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F2")
def test_intersection_vertex_binds_a_pattern_copy_with_exact_instance_owner_evidence(self) -> None:
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
feature = root / "featurescript_rp/0042/00423838.txt"
if not feature.exists(): self.skipTest("CADFS sample is not installed")
candidate = lower_model(parse_featurescript(feature.read_text(), "00423838"), {})
bound, evidence = bind_candidate_selectors(candidate.cdsl)
reference = next(item for item in candidate.cdsl["features"] if item["id"] == "f_F7")["params"]["end_condition"]["reference"]
self.assertTrue(all(item["owner_match_required"] for item in reference["intersection_of"][:2]))
f7 = next(item for item in bound["features"] if item["id"] == "f_F7")
reference = f7["params"]["end_condition"]["reference"]
components = reference["intersection_of"]
self.assertEqual(len(components), 3)
self.assertEqual([item["binding_feature_id"] for item in components], ["f_F4", "f_F4", "f_F6"])
# 绑定发生在 F4 完整 pattern 前缀;owner 保留 instance 4 的语义来源,
# snapshot 则指向该前缀的 active Compound B-rep。
self.assertEqual([item["owner_feature_id"] for item in components[:2]], ["f_F4.c4.f_F1", "f_F4.c4.f_F1"])
self.assertTrue(all(item["snapshot_id"].startswith("body:f_F4:") for item in components[:2]))
self.assertEqual(components[2]["match_mode"], "all")
# 同一圆柱面可能被前缀 boolean 切成不同数量的 B-rep face;关键是
# match_mode=all 保留每个可匹配片段,而不是把它收缩为任意一个面。
matched = components[2]["matched_selectors"]
self.assertTrue(matched)
self.assertTrue(all(item["owner_feature_id"] == "f_F6" for item in matched))
self.assertTrue(all(item["snapshot_id"].startswith("body:f_F6:") for item in matched))
f7_evidence = next(item for item in evidence if item["feature_id"] == "f_F7")
self.assertEqual(len(f7_evidence["resolved"]), len(matched) + 2)
bound, evidence = bind_candidate_selectors(candidate.cdsl)
reference = next(item for item in bound["features"] if item["id"] == "f_F7")["params"]["end_condition"]["reference"]
copy_components = reference["intersection_of"][:2]
self.assertTrue(all(item["owner_feature_id"] == "f_F4.c4.f_F1" for item in copy_components))
self.assertTrue(all(item.get("snapshot_id") for item in copy_components))
binding = next(item for item in evidence if item["feature_id"] == "f_F7")
self.assertGreaterEqual(len(binding["resolved"]), 2)