feat(selector): 增加离线候选遍历与严格回放验证 Demo
- 新增 selector_candidate_demo,移除 provenance intent 后枚举候选 selector - 对候选分支执行有界重建与严格 STEP 比较 - 仅在候选遍历完整且唯一 strict 通过时生成 selector 映射记录 - 增加 selector 候选搜索、预算限制和记录生成的测试 - 保持生产 selector resolver 不受 Demo 逻辑影响 - 更新 CADFS 能力台账,记录 IMPRINT 派生 profile 的 lineage selector 缺口
This commit is contained in:
@@ -202,6 +202,18 @@ instance 生命周期。每项采用或拒绝外部方案的决定、理由和
|
||||
strict 仍因 max/p99 surface `0.01 mm` 与体积/面积阈值失败而保留为诊断。单元测试还覆盖
|
||||
copy、boolean、pattern 与 delete 均清空 lineage;没有能唯一表示的 nested source-member
|
||||
chain 仍属未完成 contract,继续保留明确诊断。
|
||||
`SWEPT_BODY@1511` 另有一条不能与上述 successor lineage 混同的即时终止 contract:只有
|
||||
singleton `qUnion([makeQuery(..., SWEPT_BODY, EntityType.BODY)])` 引用紧邻前序独立、无 draft、
|
||||
`new_body` blind prism,且其 producer body record 仍唯一 active 时,单侧 `up_to_body` 才能
|
||||
以 `active_body_member` / `direct_new_body` 解析该 record。resolver 以 producer identity 与
|
||||
active-member lifecycle 证明 `body_member`,不读 stable ID、binding ID、几何、aggregate 或
|
||||
current body。`00694309` F3 是正向 body-member evidence;该样本的 F4 strict/RP pass 由下述
|
||||
独立 `CAP_EDGE` continuation contract 证明,而非放宽 body-member 规则。later successor、`ADD`、cut、
|
||||
revolve、sweep、`COPY`、boolean/pattern/delete、多 body/multiple-query 和其它 FeatureScript
|
||||
version 都不属于这个 contract。fresh `output/core-17-swept-body-up-to-body-member-20260910` 的
|
||||
`tier=all` pipeline/RP run 完成 17/17,分类为 1 `comparison_timeout`、10 `rebuild_failed`、
|
||||
1 `rebuilt_approximate`、5 `rebuilt_rejected`;它只证明本窄 selector contract 未改变核心集
|
||||
的既有分类,不能标记 general `SWEPT_BODY` 或 `up_to_body` 完成。
|
||||
2. 在 adapter 层记录每次建模操作的 `preserved`、`modified`、`generated`、`deleted`
|
||||
topology delta 及 CAP/SWEPT/boolean 等输出角色;runtime 以该记录完成后继 selector
|
||||
绑定,不以“当前形状中最相近元素”猜测。当前增量已覆盖 OCC 刚体
|
||||
@@ -232,7 +244,13 @@ instance 生命周期。每项采用或拒绝外部方案的决定、理由和
|
||||
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,以及满足全部条件的
|
||||
混用。`extrude_add_blind`/`extrude_cut_blind` 的
|
||||
`params.end_condition.reference` 与 `shell` 的 `feature.selectors` 各有一个有界 direct-cap
|
||||
contract:仅紧邻前序、`result_mode: new_body`、blind extrusion 的 `extrude.start`/
|
||||
`extrude.end` 可作为 `up_to_surface` reference 或 shell removal face,且必须由该 producer 的
|
||||
完整/已证明 cap role 解析;schema、semantic preflight 与 runtime 同时拒绝非紧邻 producer、
|
||||
其它 role、stable ID、binding ID 和 geometry fallback。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 跨
|
||||
@@ -244,8 +262,99 @@ instance 生命周期。每项采用或拒绝外部方案的决定、理由和
|
||||
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 和唯一性证明。找不到或多解必须稳定诊断。
|
||||
`TopologyDeltaRelation` 还必须显式记录 `source_kind` 与 `result_kind`,禁止把 face、edge
|
||||
或其它 subshape 的 relation 混作同一种后继。每个 `TopologyDelta` 必须随操作输出 input/
|
||||
output snapshot、body member 和 history 状态证据;当前 contract 的状态为 `proven`、
|
||||
`partial`、`unknown` 或 `rejected`,并可带原因。只有完整、可追溯的 kernel relation 才能
|
||||
进入 lineage;`IsDeleted` 不等于存在 replacement,缺失的 `IsDeleted` 也不等于 deleted。
|
||||
boolean 的 section edge 可以作为 builder 生成事实记录,但没有 source-face 的精确 relation
|
||||
时不得被提升为 source-qualified lineage。当前 `INTERSECT@1511` 仅有一个受限 consumer:
|
||||
explicit `boolean_bodies` 的单一 target/tool subtract/intersect 中,两个 direct-prism
|
||||
`CAP_FACE`/`SWEPT_FACE` input 必须分别由 OCC `Generated(face)` 返回同一 `SectionEdges()`
|
||||
edge,且三个 handle 都以 `IsSame` 绑定 input/final snapshot;resolver 再要求唯一 relation、
|
||||
`intersection`/`one` policy 和 active result member。另有一条同样受限的 primary `REMOVE` 路径:
|
||||
immediate `extrude_cut_blind` 只可在一个 active target solid/member、一个 direct-prism tool、完整
|
||||
source anchor/history 且 source query 没有未核实 disambiguation 时注册 `transient:<feature>` tool
|
||||
snapshot;它仅作紧随 cut 的 source-qualified section 历史输入,永不成为 active/selectable topology。
|
||||
普通 `SectionEdges()`、多输出、keep-tools、多 member、COPY/transform/pattern/TDD/IMPRINT/non-prism、
|
||||
primary query 的未核实 `disambiguationData` inputs 均不执行。
|
||||
这不构成通用 INTERSECT coverage,真实语料 RP 矩阵仍待补齐。这些记录是通用 topology 事实,不是由 stable ID
|
||||
或最终几何相似性反推的替代品。
|
||||
3. 建立统一 selector resolver,并让 runtime 与诊断性的 prefix binder 共用
|
||||
`TopologyRegistry.resolve` 及已导出的 topology facts。resolver 按“语义 source -> kernel
|
||||
lineage -> derivation policy/cardinality -> active body member”顺序绑定,报告实际分支使用的
|
||||
`resolution_mode` 和 evidence,而不是从 selector 字段事后推断模式。stable ID 只可定位
|
||||
source,不能直接在 active record 上成功;owner、output role 或 geometry 也不能绕过完整
|
||||
lineage、允许的 derivation policy、operation-wide cardinality 与 active-body 证明。
|
||||
`none`、`source_qualified`、`one` 与 `all_fragments` 都必须有明确、可审计的语义:merge 即使
|
||||
最终只有一个 result 仍按 N:1 检查,`all_fragments` 遇到任一分支缺失或 partial 必须拒绝。
|
||||
resolver 失败不得自动改走 legacy 成功路径;只有已文档化的 legacy explicit-geometry selector
|
||||
可保留 geometry fallback,provenance/COPY instance selector 不得回退到几何相近、current body
|
||||
或普通 context selector。找不到、多解、历史不完整、版本不支持和 policy/cardinality 不满足
|
||||
都必须稳定诊断并保留可执行前缀。
|
||||
`selector_intent.version` 是唯一 canonical intent version;顶层
|
||||
`selector_intent_version` 仅为兼容字段,存在时必须一致。schema 可接收只有
|
||||
`selector_intent` 的语义 selector,并将 FeatureScript 标准库路径、精确 import version、全部 direct
|
||||
import 列表与 source language version 元数据和 runtime ID 分离保存。lowering 不得为缺失 source
|
||||
version 虚构 `"0"`;resolver 应返回 `selector_query_version_unknown`。query family/language
|
||||
version/direct standard-library revision 通过显式 allow-list 注册,不得从 `15xx` 数字前缀、相同 import
|
||||
path 或单一样本推断兼容性。当前
|
||||
`FEATURESCRIPT_QUERY_CAPABILITY_MATRIX.md` 将 FeatureScript `1511` 的受限
|
||||
`CAP_FACE`、`OFFSET_FACE` 以及 direct-prism `SWEPT_FACE`/`SWEPT_EDGE` contract 标为
|
||||
端到端可执行。后两者只适用于紧邻独立、无 draft、`new_body` blind prism 的 consumer:
|
||||
adapter 必须保留 direct source-profile edge/vertex construction handle,并分别由
|
||||
`BRepPrimAPI_MakePrism.Generated(edge)` 的 edge -> side-face 或
|
||||
`Generated(vertex)` 的 vertex -> vertical-edge relation 完整证明;resolver 再检查
|
||||
`boundary` policy、source qualification 和 active member。`00111611` F3、`00974931` F2、
|
||||
`00594348` F2 和 `00000715` F3 是这些受限路径的真实语料证据。`SWEPT_EDGE` 的 direct
|
||||
profile contract 现还接受一个 analytic contour region 的 hole wires:adapter 将外环和全部内环一次
|
||||
交给 `BRepBuilderAPI_MakeFace`,只在 source edge/vertex 经 finished-face `IsSame` 验证后登记
|
||||
anchor;`00007264` F2 的四个 selector 已 `kernel_lineage` resolved(最终 comparison rejected),
|
||||
`00039669` F2 strict/RP passing,`00151159` F2 RP passing。含 hole 的多 region profile 必须继续走
|
||||
既有 `Face.make_holes` geometry path 且不登记 anchor,避免改变 prism combine 语义;`00248377` 是
|
||||
该拒绝/保留边界,仍导出 F4 checkpoint。`00594348` 的 immediate
|
||||
shell 仅接受 direct source 的 non-construction line wall,并从同一 edge -> face relation
|
||||
解析两张 removal face;它不输出 geometry 或 stable-ID hint。direct all-circle construction 可保留一个
|
||||
annular hole 的 outer/inner source edge;多个独立 direct circle region 只在每个 prism
|
||||
result 都仍位于 final snapshot 时保留各自 relation(当前为原子证据,尚非多语料完成项)。
|
||||
`CAP_FACE` 还可作为上述受限 `up_to_surface` nested reference 或 immediate shell removal
|
||||
face 的 cap role;`00212904` F2 与 `00789939` F2 分别以 `extrude.start` 和 `extrude.end`
|
||||
的 `operation_role` 执行。fresh `output/cap-face-immediate-shell-00212904-20260910-v1`
|
||||
证明前者执行到 F5,但 strict/RP 均拒绝;fresh
|
||||
`output/cap-face-immediate-shell-00789939-20260910-v1` 证明 F2 及后续同一 direct-cap
|
||||
shell F5 都执行,F6 仍为 `selector_query_unsupported` 并保留 F5 STEP checkpoint。fresh
|
||||
`output/core-17-cap-face-immediate-shell-20260910-v1` 的 `tier=all` / pipeline / RP run 为
|
||||
17/17 completed、1 `rebuilt_strict`、1 `rebuilt_approximate`、6 `rebuilt_rejected`、8
|
||||
`rebuild_failed`、1 `comparison_timeout`;其中 `00212904` 的分类由 selector failure 变为
|
||||
executable-but-rejected,不能将此记为几何相似通过。它们不允许在 producer 后续 mutation、
|
||||
non-immediate owner、stable ID、binding ID 或 geometry fallback 中继续解析;
|
||||
direct-prism `SWEPT_FACE` 也可作为单侧 `up_to_surface` reference,但它不是 output role:lowering
|
||||
只保留一个 direct source-profile edge anchor,resolver 必须逐段验证 `boundary` 以及后续
|
||||
`continuation` 的 complete/proven kernel relation、operation-wide cardinality 和 active body。
|
||||
`00925274` 的 F3 以 `extrude.start` 的 `operation_role` 解析;F6、F9、F12 与 F15 都以 F1 E0
|
||||
side-wall `kernel_lineage` 依次经过 complete/proven 的 F3、F6、F9、F12 target-side subtract
|
||||
continuation 解析。单实体 primary cut 现在始终保留其 cut builder 的 target snapshot history;仅
|
||||
transient tool snapshot 仍要求 direct-prism tool history,故没有扩大 source-qualified `INTERSECT`
|
||||
的工具可选范围。resolver 只在 selector 请求的 source/result topology kind 内计算 complete/proven
|
||||
final-snapshot relation 的基数;没有 final snapshot member 的 partial intermediate handle 及 cross-kind
|
||||
section diagnostic 保留在 delta 中但不伪造成 face fragment。任何绑定到 final result 的 partial、split、
|
||||
merge、inactive 或无 history relation 仍稳定拒绝,绝不以 geometry、stable ID、binding ID 或 current
|
||||
body 猜测。fresh `output/core-17-swept-face-final-20260910` 的 `tier=all` / pipeline / RP run 完成
|
||||
17/17:2 `rebuilt_strict`(`00694309`、`00925274`)、1 `rebuilt_approximate`、6
|
||||
`rebuilt_rejected`、7 `rebuild_failed`、1 `comparison_timeout`;`00925274` 为 strict/RP pass,不能
|
||||
推广为一般到面终止或 general SWEPT_FACE completion。受控完整 `0040` shard 的 fresh
|
||||
`output/shard-0040-swept-face-final-20260910-v2` 也完成 71/71:7 strict、9 approximate、16 rejected、
|
||||
34 rebuild-failed、3 runtime-ineligible、1 timeout、1 deferred,并保留 32 个 comparison artifact;它是
|
||||
shared runtime 回归分类证据,不把非 strict/RP 样本重述为 selector success。IMPRINT/split source profile、draft、多/two-sided extent、fused source result、未证明或
|
||||
split continuation、boolean/SPLIT 后继、revolve、
|
||||
sweep、loft、copy/pattern 及一般 SWEPT query 仍未完成,绝不能标记为 query-family 完成。
|
||||
CADFS production rebuild 以一个 `IncrementalCdslExecution` 按 history 顺序执行:每个 feature
|
||||
在执行前只对本 session 已登记的 topology snapshot 调用 resolver,随后立即执行并登记新的
|
||||
body/history evidence。`binding_feature_id` 读取同一 session 保留的历史 snapshot,不能通过
|
||||
重建 prefix 或复制 OCC body 获得结果。feature 或 selector 失败时,系统直接从该 session 导出
|
||||
最后已执行 checkpoint;不会为尝试更早 prefix 而重放历史。`bind_candidate_selectors` 仍可为
|
||||
不完整的外部/诊断 CDSL 显式运行 legacy prefix adapter,但不得进入 production success/failure
|
||||
path,也不得将 provenance 或 output-role selector 降级为 geometry binding。
|
||||
4. 完成草图 region/wire 模型:多轮廓、内环、开口 reference geometry、B-spline/ellipse、
|
||||
profile query、显式 construction 和退化检测。
|
||||
非周期 `skFitSpline` 的两点受限变体现已贯通:仅当端点不同、同时给出两端导数,且
|
||||
@@ -263,7 +372,62 @@ instance 生命周期。每项采用或拒绝外部方案的决定、理由和
|
||||
`SWEPT_EDGE`、`OFFSET_FACE`、`INTERSECT`、`MID_CAP_EDGE`,将其物化为可追溯的
|
||||
profile/wire,而不是复用原草图或写入样本特例。现有 direct-prism,以及单闭合无内环、
|
||||
法向同向的 `LocOpe_DPrism` draft CAP_FACE,是受限例外:它们以 builder-proven output
|
||||
role 直接消费物理 B-rep 面,未试图物化或重建 profile。
|
||||
role 直接消费物理 B-rep 面,未试图物化或重建 profile。`OFFSET_FACE` 的端到端 status
|
||||
同样只限于 capability matrix 所列、带 TDD source cap 的 direct shell output role;它不代表
|
||||
任意 offset query 已可重放。`SWEPT_FACE` 与 `SWEPT_EDGE` 现在各有一条 1511 direct-prism
|
||||
纵向 contract:source profile edge 经 `Generated(edge)` 到 side face,以及由完整 incident
|
||||
source-edge set 唯一命名的 source vertex 经 `Generated(vertex)` 到 vertical edge。lowering 只在
|
||||
producer 紧邻、独立、无 draft、`new_body` blind prism 且原 profile 未变时产生语义 selector;
|
||||
adapter 只将 direct wire/face construction 中 exact `IsSame` anchor 注册为 transient source fact,
|
||||
resolver 则只沿 complete/proven 跨 kind relation 解析。单一 analytic region 的 direct hole wires
|
||||
也可在 finished face identity 未改变时提供这份 evidence:`00007264` 的四个 vertical-edge selector
|
||||
皆已解析,`00039669` strict/RP passing,`00151159` RP passing;含 hole 的 multi-region profile
|
||||
继续使用旧 geometry path 并明确不产生 anchor。`00111611` F3 的圆 edge -> side face、
|
||||
`00974931` F2 的 direct annulus outer edge -> side face,与 `00000715` F3 的两个 incident-edge
|
||||
pair -> vertical edge 为其它真实语料证据。direct all-circle construction 还保留 annular inner edge
|
||||
及每个 direct circle region 的独立 delta;后者目前只有原子 executor evidence。IMPRINT 或 split
|
||||
profile、draft、multiple/two-sided extent、fused result、boolean/SPLIT、revolve、
|
||||
sweep、loft、copy/pattern 及 profile/wire consumer 也未覆盖。因此这只是两个受限 end-to-end
|
||||
contract,`SWEPT_*` family 和 P1 的通用派生 profile 目标仍未完成,不能由 AST、unit relation
|
||||
或上述两个样本提前完成。
|
||||
`CAP_EDGE` 现在也有一条独立的 1511 direct-prism contract,但它不物化 profile/wire:source
|
||||
sketch entity 必须仍对应一个 exact direct-profile edge,query 的 `isStart` 被保存为
|
||||
`lineage_role: extrude.start|extrude.end`,adapter 以
|
||||
`BRepPrimAPI_MakePrism.FirstShape(source_edge)` / `LastShape(source_edge)` 取得 cap edge,并以
|
||||
final snapshot 的 `IsSame` 证明该 handle 未失效。resolver 只沿这个 source edge 与 role-qualified
|
||||
`boundary` relation 解析;stable ID、几何、owner propagation 和 current-body 均不是回退路径。
|
||||
`00021014` F2 的九个 CAP_EDGE selector 覆盖两个 role,完整 history strict/RP 通过。该例外只接受
|
||||
紧邻、独立、无 draft 的 `new_body` blind prism 与未变 direct profile。一个 direct all-circle
|
||||
annulus(一个外圆和一个包含的内圆)也可保留 outer/inner 的 exact source anchor:`00479470`、
|
||||
`00501170`、`00526649` 在 F2 的 CAP_EDGE consumer 后 RP 通过,`00621329` strict/RP 通过;
|
||||
`00566233` 的两个 F2 selector 已由 kernel lineage 解析,随后才因 OCC chamfer feasibility 失败,
|
||||
`00614954`/`00678961` 也都在 F2 解析后才被后续未支持 query 截断。对于线段外环加圆孔,solver
|
||||
仅给同一 source circle 拆出的全部四条 arc 加 `logical_circle_source_entity_id`;adapter 再要求这个
|
||||
ID 在原 profile 中唯一指向一个未拆分圆、重建 native wire,并以 finished-face `IsSame` 重新证明。
|
||||
任一 marker、source 或 final membership 缺失都没有 anchor。`00735367` 的 F2 因而以
|
||||
`kernel_lineage` 解析、F1--F3 执行,随后 F4 才以 `selector_query_unsupported` 导出 F3 checkpoint。
|
||||
不完整 logical-circle loop 不产生该 circle 的 anchor;含 hole 的 multi-region、trimmed/split profile、
|
||||
draft、multiple/two-sided extent、fused result、boolean/SPLIT successor、revolve、sweep、loft、copy/
|
||||
pattern 继续拒绝。
|
||||
对 direct-prism `CAP_EDGE` 的 fillet/chamfer consumer,boundary-only policy 另可在运行时存在
|
||||
完整 kernel continuation 时扩展为 `boundary + continuation`:每一跳都必须为 exact
|
||||
`preserved`/`modified` relation、`coverage: complete`、`status: proven`,且 operation-wide
|
||||
cardinality 保持 1:1;任何 partial、split/merge、inactive result 或无 history 都稳定拒绝。
|
||||
`00694309` 证明这一条:F4 的 F1 `extrude.start` cap edge 经 F3 primary-cut `preserved`
|
||||
continuation 解析为 `body:f_F3:edge:1`,完整 pipeline `output/cap-edge-continuation-after-primary-cut-20260910-v1`
|
||||
的三 feature history strict/RP passing。任何 boolean/SPLIT、COPY/pattern 或其它中间操作也必须
|
||||
提供同样完整的 1:1 relation;它们目前没有独立的 corpus coverage,不能仅按 owner/operation 名称
|
||||
通过。这个 contract 也不接受任意 CAP_EDGE source,且没有 geometry、stable-ID、
|
||||
binding-ID 或 current-body fallback。fresh
|
||||
`output/core-17-cap-edge-continuation-20260910-v1` 的 `tier=all` / pipeline / RP run 完成 17/17:
|
||||
1 `rebuilt_strict`(`00694309`)、1 `rebuilt_approximate`、5 `rebuilt_rejected`、9
|
||||
`rebuild_failed`、1 `comparison_timeout`。这只确认该完整 relation contract 将此样本从 F3
|
||||
executable prefix 推进为实际 strict/RP pass,不能作为 CAP_EDGE family completion 或 general
|
||||
boolean continuation 的证据。
|
||||
共享 selector/runtime suite 为 261 passed、1 skipped;强制 core-17
|
||||
重跑 `output/core-17-direct-prism-cap-edge-hole-20260910` 为 1 RP、5 rejected、10 rebuild-failed、1
|
||||
comparison timeout,与 selector-migration 基线分类一致。因此 `CAP_EDGE` family 与 P1 的通用派生
|
||||
profile 目标仍未完成。
|
||||
`OFFSET_FACE` 现有一个独立的受限 profile 物化路径:只有 inward shell 直接消费一个
|
||||
`new_body` blind additive prism,shell 恰移除该 prism 的一个 CAP,且 OFFSET query 恰指向
|
||||
其未修改、单闭合凸线性 profile 中的一条非 construction line,lowering 才根据 source
|
||||
@@ -302,11 +466,23 @@ instance 生命周期。每项采用或拒绝外部方案的决定、理由和
|
||||
`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 部分完成项。工件
|
||||
B-rep region。top-level `IMPRINT FACE` 的 multi-face query 只有既有的
|
||||
`_profile_selection_sketch` 不能将它缩减为单一/圆形/open 专用 profile 时,才进入该 typed
|
||||
arrangement profile;不能把 nested `INTERSECT` 误当作 outer profile family,也不能抢占
|
||||
existing circle/open/surface IMPRINT contracts。没有唯一 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-multiface-20260910` 的 seven-source forced RP matrix 验证这个收紧后的
|
||||
分派:`00304488`、`00253824` rebuilt_rejected;`00354246` 在 F2 deferred `SWEPT_FACE` 时保留
|
||||
F1 checkpoint,`00306689` 在 F1 OCC `BRep_API: command not done` 失败,`00784127` 在 F1、
|
||||
`00782218` 在 F2 因 selected region unbounded 拒绝(后者保留 F1 checkpoint),`00429745`
|
||||
在 F1 OCC extrude 无有效 solid 时拒绝。该 matrix 不把任一 IMPRINT-derived profile 的下游
|
||||
`SWEPT_FACE` 视为完成:它们保留 source intent 并由 shared resolver 有界拒绝。当前 shared
|
||||
selector/runtime suite 为 262 passed、1 skipped;强制 core-17
|
||||
`output/core-17-planar-imprint-multiface-20260910` 为 1 RP、5 rejected、10 rebuild-failed、1
|
||||
comparison timeout,与 selector-migration 基线一致。工件
|
||||
`output/planar-imprint-intersect-20260909-runtime` 是 CAP_FACE 扩展前的历史工件:
|
||||
`00082324` 与当时的 `00835610` 都保留可执行 STEP 但 `rp.passed == false`,`00071859` 和
|
||||
`00093912` 分别保留既有 `TopoDS::Solid` 和 revolution-segmentation runtime failure。
|
||||
@@ -350,6 +526,26 @@ instance 生命周期。每项采用或拒绝外部方案的决定、理由和
|
||||
作为受限 CDSL output-role selector 供 downstream feature 消费;同一 contract 还支持
|
||||
independent blind prism CAP_FACE 的 `extrude_from_face`。这些路径仍不能代替完整的
|
||||
derived-topology selector 语义,CADFS lowering 也尚未以此表示一般 CAP/SWEPT query。
|
||||
capability matrix 中 `SWEPT_FACE@1511` 与 `SWEPT_EDGE@1511` 另有 direct-prism 的受限
|
||||
end-to-end contract:独立、无 draft、`new_body` blind prism 的 direct source profile edge/
|
||||
vertex(含一个 analytic region 的 exact hole wires)由 exact construction anchor 和 `Generated`
|
||||
cross-kind history 绑定给紧邻 consumer;含 hole 的 multi-region profile 不产生这类 anchor。
|
||||
这不覆盖本段 sweep/loft/revolve 的 output role,亦不把任一受限 output role 或特征专用
|
||||
selector 升级为通用 CADFS query-family 支持。
|
||||
对 `sweep.path`,另有一个 source-sketch-only lowering contract:FeatureScript 1511 的 exact
|
||||
`qUnion([qConstructionFilter(qBodyType(qCreatedBy(sketch, EDGE), WIRE), NO)])` 仅在 source 还直接 import
|
||||
`onshape/std/geometry.fs@1511.0`、owner 是 source
|
||||
sketch、恰有一个 non-construction `line` 或 `bspline` entity 时解为路径,不能作为 runtime
|
||||
`qBodyType` selector。两点 `skFitSpline` 只有 source 明确给出 start/end derivative 时才会保留为
|
||||
B-spline 并由 OCC 执行;没有两个导数时拒绝,绝不改成 chord。`00896761` F2(F1/E2 two-point
|
||||
B-spline)现在 converted/rebuilt 且无 runtime diagnostic,artifact bundle
|
||||
`output/qbodytype-direct-sketch-wire-20260910-v2` 包含 STEP、GLB 和七视图;但 RP comparison 在 60s
|
||||
预算超时,分类为 `comparison_timeout`,不是相似通过。fresh seven-sample RP matrix
|
||||
`output/qbodytype-direct-sketch-wire-matrix-20260910-v1` confirms all direct-path cases rebuild: `00191739`
|
||||
strict, `00726304` RP-only approximate, `00227428`/`00287471`/`00500952`/`00816123` rejected, and
|
||||
`00896761` timed out during comparison. These classifications are evidence of executable paths, not a claim
|
||||
that rejected/timeout output is similar. `00786708` F2 因多个 non-construction path entity 继续 deferred。这不是 generic `qBodyType`、qConstructionFilter、sweep
|
||||
path、loft guide、arc/circle path 或 sweep complete 的完成声明。
|
||||
2. `shell`:单一 selected solid 的 direct-builder topology delta 与有限 output-role
|
||||
evidence 已覆盖。CADFS lowering 现额外接受一个受限的 removal selector:直接 blind 或
|
||||
two-sided linear extrusion 的立即后继 shell,可从同一未修改 source profile 中唯一的、非
|
||||
@@ -413,7 +609,22 @@ instance 生命周期。每项采用或拒绝外部方案的决定、理由和
|
||||
都不继承该 contract。该受限路径不覆盖 sweep、surface/partial/fused revolve、
|
||||
multi-section loft、generated/trimmed/transformed profile 或 boolean/pattern 后继;任一
|
||||
端点、axis 或 owner 不唯一时必须保留前缀并诊断。内核不能完成时不能伪造较小半径或不同
|
||||
孔型。
|
||||
孔型。这里的 endpoint-bbox/revolve selector 不属于 capability matrix 的 direct-prism
|
||||
`SWEPT_EDGE` provenance contract,不能借其成功。只有该受限 blind-prism path 具备 query
|
||||
source semantics、lowering、adapter evidence、consumer 和真实语料回归;其它 generator/
|
||||
consumer 仍须逐一补齐后,才可将整个 query family 标记为完成。
|
||||
`hole` 现增加一个独立且更窄的 source-location/body-scope contract:lowering 仅接受原始
|
||||
`sQuery`/`sketchEntityQuery(VERTEX, ...)` 的 `skPoint`、`skCircle.center`、`skLineSegment`
|
||||
或 `skArc` 的 `.start`/`.end`,并要求所有 location 都来自同一显式草图平面;派生 suffix、
|
||||
CAP/SWEPT/COPY topology、query combinator 与几何邻近性均不会成为 location fallback。CADFS
|
||||
`scope` 必须是一个直接 `SWEPT_BODY`,并在 lowering-time body graph 中仍是唯一独立成员,才写入
|
||||
`hole_wizard.params.scope_feature_id`。schema、semantic validation、capability preflight 和 executor
|
||||
共同检查该成员仍是唯一 active solid;executor 在每次切除后保留原 scope member key,因而后续
|
||||
CADFS hole 可以继续引用同一 body,而无 scope 的既有 CDSL hole 仍保留 feature-owned lifecycle。
|
||||
`00406667` 的 F1/F3/F5 是端到端证据:两个 `skCircle.center` hole 与同一 F1 revolve scope 完整执行,
|
||||
`output/hole-direct-sketch-vertex-20260910-v2` 分类为 `rebuilt_approximate`(RP passed,strict failed)。
|
||||
这不覆盖 B-spline/interior vertex、多个 host plane、多个 scope body、COPY/boolean/pattern successor、
|
||||
non-direct query、全部 hole start/end styles 或完整孔型/螺纹语义。
|
||||
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`
|
||||
@@ -471,6 +682,69 @@ instance 生命周期。每项采用或拒绝外部方案的决定、理由和
|
||||
`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。
|
||||
`LINE_ANGLE` 现额外覆盖单一 direct `skCircle` edge:FeatureScript 1511 的
|
||||
`cplane.fs::lineAnglePlane` 将该选择传入 `evAxis`,而同版本 `query.fs` 将 circle
|
||||
列为 `ALLOWS_AXIS`。lowering 因此只用 circle centre 与 source sketch plane normal
|
||||
作为 axis,并按同版本 `vector.fs::perpendicularVector` 的确定性阈值分支适配 zero-angle
|
||||
direction,再按
|
||||
`angle` 和 `oppositeDirection` 的 signed angle 旋转;不会以 circle centre vertex、
|
||||
`CAP_EDGE`/`SWEPT_*`/COPY 等派生 topology 或 current body 替代这个 source contract。
|
||||
该语义依据来自 MIT 许可的 Onshape standard-library mirror 的 1511.0 commit
|
||||
`eaef87b22d3b15e9377d8b1c6cb701f8111b6a0c`;适配该 MIT 算法但未引入依赖。`00030209`
|
||||
的 F1/F2/F4/F5 是现有的四个真实 direct-circle evidence:
|
||||
`output/cplane-line-angle-direct-circle-20260910` 不再含 cPlane diagnostic,并保留
|
||||
四个 executable `reference_plane`。该 source 后续因 arc sweep path 未实现而没有
|
||||
solid feature,pipeline 正确分类为 `runtime_ineligible`,没有将工作平面 lowering
|
||||
误报为 STEP/RP 成功。多实体 reference、direct arc/cylinder/cone 等其他 `evAxis`
|
||||
输入和一般 derived topology 仍未覆盖。focused shared selector/runtime suite 为
|
||||
`263 passed, 1 skipped`(142.98 s;仅既有 build123d deprecation 与 workplane
|
||||
re-orthogonalization warnings)。强制 RP Core-17 refresh
|
||||
`output/core-17-line-angle-direct-circle-20260910` 为 1 `comparison_timeout`、10
|
||||
`rebuild_failed`、1 `rebuilt_approximate`、5 `rebuilt_rejected`,与既有核心集分类一致;
|
||||
它只证明共享改动没有改写这些既有结果,不构成 generic `LINE_ANGLE` 或 STEP/RP success。
|
||||
两实体 `LINE_ANGLE` 现额外覆盖 source-only contract:direct `skLineSegment`/`skCircle`
|
||||
提供 axis,默认 datum 或已 lower reference plane、direct `skPoint` 或 direct line endpoint
|
||||
提供第二 reference。实现严格镜像 1511 `cplane.fs::lineAnglePlane`:第一个 selection
|
||||
不是 axis 而第二个是 axis 时交换;两个非平行 axis 用第二 direction,平行 axis 用两 origin
|
||||
之差,plane 用 `cross(axis, plane.normal)`,point 用 `point - axis.origin`,再以 signed angle
|
||||
旋转 `cross(axis, secondInPlaneDirection)`。共线/零长度选择明确 defer。此 direct branch
|
||||
只接受未组合的 `sQuery`/`sketchEntityQuery`,不通过 `_query_line`、current body 或 geometry hint
|
||||
推导 CAP/SWEPT/COPY/trim/derived query;
|
||||
它继续引用 MIT-licensed Onshape standard-library mirror commit
|
||||
`eaef87b22d3b15e9377d8b1c6cb701f8111b6a0c`,未引入依赖或复制外部代码。lowering regression
|
||||
覆盖 datum-first swap、axis-first、nonparallel/parallel-offset axes、direct point、reversed
|
||||
point/axis、`oppositeDirection`、degenerate reject,并断言 direct helper 不接受 `CAP_EDGE`。
|
||||
真实 matrix `output/cplane-line-angle-two-entity-matrix-20260910` 的六条直接 axis/default-plane
|
||||
history(`00067847`、`00185138`、`00217351`、`00298026`、`00586156`、`00831400`)均无 cPlane
|
||||
diagnostic 且保留 rebuild STEP:`00586156` strict/RP pass,`00298026` RP pass/strict diagnostic,
|
||||
前三条为独立 `rebuilt_rejected`,`00831400` 在后续 F5 `BRep_API: command not done` 保留 prefix。
|
||||
这只证明受限 frame lowering,不能将后续 geometry 分类归为 `LINE_ANGLE` 成功;direct arc、
|
||||
non-source vertex、任意 derived topology、一般 face/curve/mate-connector axis 和完整 cPlane
|
||||
coverage 仍未完成。既有圆柱母线 compatibility path 也已收紧为单一 direct `makeQuery`
|
||||
`CAP_EDGE` 或 `SWEPT_FACE` 的 source `skCircle`,要求 producer start/end frame 与另一显式 datum/
|
||||
lowered reference plane;qAdjacent/qUnion composition、CAP/SWEPT line、trim/COPY/boolean source 均稳定
|
||||
defer,不能因 AST 内出现 source token 获得 `_query_line` fallback。1511 `query.fs`/`evaluate.fs` 确实
|
||||
将 arc 列为 `ALLOWS_AXIS`/`evAxis` 输入,但当前 9,347 条 corpus 未发现 direct `skArc` 被
|
||||
`LINE_ANGLE` 实际选择的 history;这只是待验证语义,不能提前宣称 direct-arc coverage。
|
||||
`CURVE_POINT` 现额外接受同一 direct source curve 的精确切线:`skArc.start/end` 由解析
|
||||
圆心、半径和方向生成切线,`skFitSpline.start/end` 只在 FeatureScript 导出了相应 endpoint
|
||||
derivative 时执行。它还受限支持 source token `E<n>.<index>.internal`:必须是同一 direct、
|
||||
non-periodic `skFitSpline`,并有完整 interpolation points、centripetal parameters 与两端
|
||||
derivatives。lowerer 复用 runtime 的 `Edge.make_spline(..., scale=False)` 构造,再在显式
|
||||
parameter[index] 以 OCC `BRepAdaptor_Curve.D1` 读取导数,同时核验返回点仍等于 source
|
||||
interpolation point;绝不以 adjacent interpolation-point chord 代替曲线切线。`00827798`
|
||||
F1 与 `00845891` F3 是两个真实 B-spline endpoint 证据,`00456498` F1 (`E0.2.internal`)
|
||||
与 `00812557` F2 (`E0.3.internal`) 是 source interpolation-token 证据;`00057273` 的受控
|
||||
`skArc` endpoint 变体覆盖圆弧方向。derived suffix、越界 index、无完整 interpolation
|
||||
data、无导数/periodic spline、curve/surface topology source 和退化 tangent 继续明确拒绝。
|
||||
两条新增样本 pipeline `output/cplane-curve-point-bspline-interpolation-20260910` 均已无
|
||||
cPlane diagnostic;`00456498` 在后续 sweep OCC solid failure 停止,`00812557` 在后续
|
||||
sweep OCC valid-solid failure 停止,保留诊断和可用工件,不误报为整体成功。shared
|
||||
selector/runtime suite 为 `277 passed, 1 skipped`(既有 workplane re-orthogonalization
|
||||
warning)。RP Core-17 refresh `output/core-17-cplane-curve-point-bspline-interpolation-20260910`
|
||||
为 1 `comparison_timeout`、10 `rebuild_failed`、1 `rebuilt_approximate`、5
|
||||
`rebuilt_rejected`,与 prior semantic-selector baseline 分类一致;它不构成完整 cPlane
|
||||
或 generic curve-attachment completion。
|
||||
|
||||
### P3:尚未支持的 FeatureScript 操作
|
||||
|
||||
@@ -495,14 +769,22 @@ instance 生命周期。每项采用或拒绝外部方案的决定、理由和
|
||||
|
||||
一项能力仅在满足以下条件时可标记完成:
|
||||
|
||||
1. 有版本化的 CDSL 表达,且 schema/semantic validation 拒绝不完整或歧义输入。
|
||||
1. 有版本化的 CDSL 表达,且 schema/semantic validation 拒绝不完整或歧义输入。对
|
||||
FeatureScript query,`selector_intent.version`、source query family、精确 language version 和直接
|
||||
standard-library import path/version 必须有明确契约;缺版本和未注册 family/language/library revision
|
||||
必须诊断,不能以 legacy 顶层版本、数字前缀、相同路径或默认版本补齐。
|
||||
2. lowering 仅根据 FeatureScript source 产生该表达,并保留 source feature、selector
|
||||
和 body provenance。仅有 runtime CDSL contract 而未由 lowering 产生的能力必须标为
|
||||
部分完成。
|
||||
3. runtime/adapter 按通用算法执行,记录结果 body 和 topology delta,不依赖样本信息。
|
||||
3. runtime/adapter 按通用算法执行,记录结果 body 和 topology delta,不依赖样本信息。派生
|
||||
selector 必须具有 source/result kind、输入/输出 snapshot、body member、history status 及
|
||||
kernel relation evidence;resolver 必须据此证明 derivation policy、operation-wide cardinality
|
||||
和 active member,并输出实际 `resolution_mode` 与 evidence。
|
||||
4. 能力矩阵记录对应 FeatureScript API/query 的源版本文档或标准库依据、采用的语义和
|
||||
未覆盖边界;原子语义矩阵包含正向、边界和拒绝测试,且至少多个真实语料样本覆盖
|
||||
不同几何和生命周期组合。
|
||||
不同几何和生命周期组合。只有 parser/AST 或 resolver unit test 的 query family 只能标为
|
||||
resolver-level/部分完成;必须同时具备 lowering、schema、adapter、consumer 和多个真实样本
|
||||
的端到端证据后才可标为 complete。
|
||||
5. 受影响核心集、扩展集和全量 shard 有可复现结果,工程相似通过率、失败数和剩余
|
||||
exception 均更新到本地台账。
|
||||
6. 代码审查确认没有 sample-specific 分支、gold STEP 参数回填、隐式默认尺寸或为
|
||||
@@ -518,3 +800,54 @@ instance 生命周期。每项采用或拒绝外部方案的决定、理由和
|
||||
全量目标完成时,报告必须按“工程相似”“严格一致”“source exception”“未实现能力”
|
||||
和“基础设施失败”分别给出分母、样本 ID、工件和原因。任何仍可执行的模型都必须
|
||||
保留输出,不能因未满足最终几何目标而丢弃。
|
||||
|
||||
## Selector 迁移台账(2026-09-09)
|
||||
|
||||
已完成第一条迁移边界,而非完整 query-family 实现:CDSL schema 允许保留未知的
|
||||
大写 FeatureScript query family;CADFS lowering 对全部 topology fallback 写入 source
|
||||
AST、版本、语义 family 和 `feature_script_query` evidence。未在版本化 capability matrix
|
||||
登记的 query 使用非空 derivation policy 加 `multiplicity: none`,因此 resolver 在 stable ID、
|
||||
geometry、owner 或 current-body fallback 前稳定返回 `selector_query_unsupported`。嵌套的
|
||||
extent、COPY 和 INTERSECT selector 同样递归写入 source metadata;COPY/INTERSECT 保留外层
|
||||
query family,不错误继承其内部 CAP/SWEPT component。显式 datum plane 单独为
|
||||
`GEOMETRIC` / `explicit_datum`,不构成派生 topology 的逃逸通道。
|
||||
|
||||
此前 core-17 candidate inventory 中有 14 个 runtime-snapshot edge、7 个 runtime-snapshot
|
||||
face、7 个 SolidWorks face 和 2 个 plane legacy selector,只有少数 direct-prism provenance
|
||||
selector。此次不把这些历史 geometry hint 宣称为 lineage:`00005267`(FeatureScript 1793)F4
|
||||
的四个 loft `SWEPT_EDGE` 已从 strict geometry success 改为 F3 executable checkpoint 加
|
||||
`selector_query_unsupported`。同类未实现的 CAP/SWEPT/OFFSET/COPY/INTERSECT paths 必须保留
|
||||
候选、bound CDSL、最后 STEP/GLB(可生成时)和诊断,直到具备 source API evidence、lowering、
|
||||
adapter history、resolver policy 和 corpus regression 的完整 contract。受限
|
||||
`CAP_EDGE@1511`、`SWEPT_FACE@1511` 和 `SWEPT_EDGE@1511` direct-prism contracts 不受影响;
|
||||
它们仍只在 capability matrix 的已证明边界内执行。
|
||||
|
||||
2026-09-10 的 source-version migration 补齐了 import-level evidence:parser 逐条保留直接
|
||||
`onshape/std/*` import 的 `{path, version}`,CDSL 顶层 `source_featurescript` 和嵌套 selector
|
||||
`source_query` 同时保留 primary import 与完整 import list。resolver 对 provenance selector 先要求
|
||||
language version、direct `standard_library` 和 `standard_library_version`,再按 capability matrix 的精确
|
||||
三元组匹配。缺 import version 返回 `selector_query_version_unknown`;未登记 library revision 返回
|
||||
`selector_query_unsupported`,且不会进入 stable-ID、geometry、owner 或 current-body fallback。全量
|
||||
9,347 条 source scan 记录到 `geometry.fs` 与 FeatureScript 版本一一成对变化;只有现有
|
||||
`1511` / `geometry.fs@1511.0` direct contracts 保持 executable。该迁移只完善来源证据门槛,不新增
|
||||
`COPY`、`SPLIT` 或其他 query family 的执行能力。验证包括 parser/lowering/provenance suite
|
||||
`140 passed`、runtime foundation `130 passed, 1 skipped`、以及 fresh `00212904` RP pipeline
|
||||
(完整 F1--F5 replay,`rebuilt_rejected`)和 `core-17-selector-standard-library-revision-20260910-v1`
|
||||
(1 strict、1 approximate、6 rejected、8 rebuild-failed、1 timeout);所有结果保留各自 STEP/checkpoint。
|
||||
同步的 integration/binder regression 也确认现有 immediate CAP shell contract 的真实前缀:`00789939`
|
||||
执行 F2/F5 后在 F6 停止并保留两-solid F5 checkpoint,`00090436` 与 `00107631` 的 outward F1/F2
|
||||
prefix 都可 rebuild;combined selector/lowering/binding/integration suite 为 `168 passed`。
|
||||
|
||||
`intersection_of` vertex 是由 `ExecutionSession` 从已解析的 face component 直接计算的特殊
|
||||
consumer,因而不能绕开 outer selector 的 provenance gate。runtime 现将 nested/legacy version
|
||||
一致性、FeatureScript source version、capability allow-list 和 derivation policy 抽为
|
||||
`TopologyRegistry.resolve` 与该 session path 共用的 contract:`multiplicity:none`、未知 version 或
|
||||
未登记 capability 的 outer `INTERSECT` 在任何 component resolve 或 OCC 求交前稳定拒绝。没有
|
||||
`selector_intent` 的既有显式几何 intersection selector 仍维持其 legacy contract。原子回归覆盖这三种
|
||||
拒绝以及 legacy 正向路径(`backend.tests.test_selector_provenance_contract`: 21 passed);这只关闭
|
||||
provenance bypass,不新增 general `INTERSECT`、vertex provenance 或 source-qualified section-edge 覆盖。
|
||||
当前代码的 shared selector/runtime suite 为 `264 passed, 1 skipped, 43 subtests`(142.57 s;仅既有
|
||||
build123d deprecation 与 workplane re-orthogonalization warnings)。真实 `00423838` 以
|
||||
`output/session-intersection-outer-gate-20260910` 强制 single-sample pipeline 验证:F7 outer deferred
|
||||
`INTERSECT` 在 incremental replay 中报告 `selector_query_unsupported`,并导出 F6 的三个 feature
|
||||
executable `rebuild.step` checkpoint;结果是 `rebuild_failed`,不是该 query family 的 RP 成功。
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# FeatureScript Query Capability Matrix
|
||||
|
||||
This matrix is the versioned allow-list consumed by the CDSL selector resolver.
|
||||
It records what the current runtime may execute, not what FeatureScript text it
|
||||
can parse. A missing entry is intentionally rejected with
|
||||
`selector_query_unsupported`; a missing source version is rejected with
|
||||
`selector_query_version_unknown`. Every registered row currently requires the
|
||||
direct source import `onshape/std/geometry.fs@1511.0` in addition to its
|
||||
FeatureScript language version. The lowerer preserves every direct
|
||||
`onshape/std/*` import as `{path, version}` metadata, but an unregistered
|
||||
library revision is not treated as compatible merely because its path matches.
|
||||
|
||||
| Query family | FeatureScript version | Source evidence | CDSL/runtime contract | Verified boundary |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `CAP_FACE` | `1511` | CADFS FeatureScript 1511 exported query history | Direct `extrude.start` / `extrude.end` builder output role; an immediate `up_to_surface` reference or a shell face-removal selector may consume that role | The consumer must immediately follow a direct `extrude_add_blind` `new_body` blind producer, with a complete/proven cap role and no stable-ID, binding-ID, or geometry fallback. `00925274` F3 resolves `extrude.start` for `up_to_surface`; `00212904` F2 and `00789939` F2 resolve immediate shell removals through `extrude.start` and `extrude.end`, respectively. Fresh pipelines preserve the distinction between execution and similarity: `00212904` executes through F5 but is RP-rejected, while `00789939` executes F2/F5 then preserves the F5 checkpoint when F6 remains unsupported. Later mutations, draft-with-holes, fused/multi-region results and generic CAP queries remain unsupported. |
|
||||
| `CAP_EDGE` | `1511` | CADFS FeatureScript 1511 exported query history; `BRepPrimAPI_MakePrism.FirstShape(source_edge)` / `LastShape(source_edge)` exact cap-edge handles | A direct fillet/chamfer consumer of an independent, undrafted, `new_body` blind prism: one exact direct source-profile edge -> one role-qualified `extrude.start` or `extrude.end` cap edge, followed only by complete/proven one-to-one continuations to the active body | The selector must name one retained direct source entity and explicit CAP side. The adapter accepts the cap handle only after `IsSame` verifies it is present in the final snapshot; resolver follows the source anchor and role through `boundary`, and accepts subsequent `continuation` only when every relation is complete/proven and operation cardinality remains one-to-one. A direct all-circle annulus with one contained circle is included when both source wires retain exact final-face membership; outer/inner roles remain distinct. A direct analytic region with a solver-split circular hole is included only when all four arcs carry the same explicit logical-circle source marker and that source maps uniquely to one unsplit profile circle; the adapter reconstructs one native wire before the same final-face check. Corpus evidence: `00021014` F2 resolves nine non-hole start/end selectors and is strict/RP passing; hole matrix `00479470`, `00501170`, `00526649` is RP passing and `00621329` is strict/RP passing. `00694309` F4 resolves F1's start CAP edge through F3's proven primary-cut continuation and is strict/RP passing. `00566233` resolves both F2 selectors before an OCC chamfer feasibility failure, while `00614954`/`00678961` resolve F2 before later unsupported queries. `00735367` resolves F2 from its line-outer/circular-hole region and executes F3 before an unrelated F4 unsupported query. Incomplete/mixed logical-circle markers do not create a circular-hole anchor; partial/branched continuation, trimmed/split profiles, multi-region-with-hole profiles, draft, multiple/two-sided extents, fused results, and unproven boolean/SPLIT/COPY/pattern continuations are rejected. This is not general CAP_EDGE replay. |
|
||||
| `SWEPT_BODY` | `1511` | CADFS FeatureScript 1511 exported singleton `qUnion([makeQuery(..., SWEPT_BODY, EntityType.BODY)])` history | An immediate one-sided `up_to_body` extent may reference exactly one active body record produced by a preceding independent `new_body` blind prism | The source body must be the immediately preceding `extrude_add_blind`, with `result_mode: new_body`, blind undrafted direct-prism semantics, and an active producer body record. Lowering emits only `active_body_member` evidence with `body_member_contract: direct_new_body`; resolver proves the active record and producer identity, then returns `body_member`. `00694309` F3 resolves this reference; its F4 strict/RP pass is separately established by the bounded `CAP_EDGE` continuation contract, not by widening this body-member contract. Later successors, `ADD`, cut/revolve/sweep, `COPY`, boolean/pattern/delete, multiple active bodies, multiple query items, other versions, and all stable-ID, geometry, binding-ID, aggregate/current-body fallbacks are rejected. |
|
||||
| `OFFSET_FACE` | `1511` | CADFS FeatureScript 1511 exported query history | `shell.offset_face` builder output role plus one TDD source cap | Immediate direct shell owner and one explicit `extrude.start` or `extrude.end` true dependency. |
|
||||
| `SWEPT_FACE` | `1511` | CADFS FeatureScript 1511 exported query history; direct-prism `BRepPrimAPI_MakePrism.Generated(edge)` relation plus final-snapshot `BRepAlgoAPI_Cut.Modified/Preserved` target history | An immediate fillet/chamfer consumer or shell removal, or a one-sided `up_to_surface` extent: one retained direct source-profile edge -> its generated side face, followed only by complete/proven kernel continuations to one active face | Limited end-to-end contract. The source prism must be independent, undrafted, `new_body`, blind, and retain one exact direct wire/face construction anchor. The extent variant carries no output role, stable ID, binding ID, or geometry hint. For a single-solid primary cut, target-side builder history is registered independently of whether its tool has direct-prism history; transient tool topology remains restricted to the separate source-qualified `INTERSECT` contract. Resolver traverses only the requested result topology kind, measures cardinality over complete/proven final-snapshot relations of that source/result kind, and still rejects any bound partial, split, merge, or inactive branch. Non-final intermediate handles and cross-type section diagnostics remain diagnostic only. `00925274` resolves F1 E0 through F3, F6, F9 and F12 target continuations and is strict/RP passing. The shell variant accepts only original non-construction lines. Direct all-circle construction includes one annular hole; independently constructed direct circle regions retain one delta per source face only when every generated result remains in the final snapshot. IMPRINT/split source profiles, draft, multiple/two-sided extents, fused source results, unproven/split continuations, revolve, sweep, loft, copy and pattern are rejected. Fresh corpus evidence: `00594348` and `00925274` are strict in `output/swept-face-target-continuation-20260910`; `00111611` and `00974931` retain rebuild failures. This is not family completion. |
|
||||
| `SWEPT_EDGE` | `1511` | CADFS FeatureScript 1511 exported query history; direct-prism `BRepPrimAPI_MakePrism.Generated(vertex)` relation | One immediate downstream consumer of an independent, undrafted, `new_body` blind prism: a source profile vertex, identified by its complete incident source-edge set -> its generated vertical edge | Limited end-to-end contract. The source vertex must retain two exact direct-profile edge anchors and the generated relation must be complete and active. One analytic contour region may contain hole wires: all wires are passed once to `BRepBuilderAPI_MakeFace`, then each retained anchor is checked against the finished face with `IsSame`. A profile with multiple regions and any hole stays on the established `Face.make_holes` path with no anchors, preserving its executable geometry. IMPRINT/split or otherwise mutated profiles, draft, multiple/two-sided extents, fused results, boolean/SPLIT successors, revolve, sweep, loft, copy and pattern are rejected; this is not family completion. Corpus evidence: `00000715` F3; `00007264` F2 resolves four selectors by kernel lineage but is comparison-rejected; `00039669` F2 is strict/RP passing; `00151159` F2 is RP passing. |
|
||||
| `INTERSECT` | `1511` | CADFS FeatureScript 1511 exported query history; OCC boolean `SectionEdges()` plus exact `Generated(face)` handles from both inputs | One `INTERSECT EDGE` of exactly two source-qualified direct-prism `CAP_FACE`/`SWEPT_FACE` inputs from either one explicit `boolean_bodies` target/tool pair, or one immediate primary `extrude_cut_blind` target plus its transient direct-prism tool snapshot | The adapter creates a relation only when the same section edge is returned by `Generated(face)` for exactly one face in each boolean input and `IsSame` binds all three handles to the input/final snapshots. A primary cut records its tool only as transient historical topology: exactly one active target solid/member, one tool solid, complete direct-prism anchors/history and one immediate consumer are required; the tool itself cannot be selected. Resolver requires one complete relation, the `intersection`/`one` policy and final active member. Unqualified `SectionEdges()`, duplicate output edges, incomplete source snapshots, keep-tools, multiple target/tool members, copied/transformed/patterned/TDD/IMPRINT inputs, non-direct prisms, primary query `disambiguationData` whose FeatureScript semantics are unverified, and unknown versions remain rejected. This is a contract test boundary; `00020311`/`00029250` retain their F3 checkpoints because their `OD(0/1)` selectors are deferred. Real-corpus RP evidence is not yet sufficient to mark general `INTERSECT` replay complete. |
|
||||
|
||||
## Source Sketch Path Queries (Not Runtime Selector Capabilities)
|
||||
|
||||
This section is intentionally outside the resolver allow-list above. The query
|
||||
does not select rebuilt body topology: it selects source sketch reference-wire
|
||||
geometry while lowering a `sweep` path. It therefore creates no
|
||||
`selector_intent`, kernel-lineage relation, or stable-ID fallback permission.
|
||||
|
||||
| Source query / consumer | FeatureScript version | Source evidence | Lowering and runtime boundary | Evidence |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `qUnion([qConstructionFilter(qBodyType(qCreatedBy(sketch, EDGE), WIRE), NO)])` as `sweep.path` | `1511` + direct `onshape/std/geometry.fs@1511.0` | Local Onshape standard-library mirror `query.fs`: `BodyType.WIRE` describes sketch curves (lines or curves), `qBodyType` retains entities owned by that body type, and `qConstructionFilter(..., NO)` retains only non-construction entities. The repository does not vendor the exact 1511 standard-library snapshot, so this is source-semantics evidence rather than a claim that all revisions are compatible. | The owner must resolve to a source sketch with exactly one non-construction entity, whose exact source type is `line` or `bspline`; the outer query must be the exact singleton `qUnion` wrapper. `line` paths retain their endpoints. `bspline` paths retain the exported interpolation points, parameters, and endpoint derivatives; a two-point B-spline is executable only when both derivatives are present. Multiple entities, arcs/circles, construction-only geometry, omitted wrapper/filter, other query composition, other language/library revisions, loft guides, surface/profile consumers, and all runtime-body uses remain rejected or deferred. | The fresh seven-sample RP matrix `output/qbodytype-direct-sketch-wire-matrix-20260910-v1` rebuilds all direct-path cases: `00191739` is strict, `00726304` is RP-only approximate, `00227428`/`00287471`/`00500952`/`00816123` are comparison-rejected, and two-point B-spline `00896761` is `comparison_timeout` at 60 seconds. These are execution classifications, not a claim that rejected/timeout models are similar. `00786708` F2 remains deferred because F0 contains multiple non-construction path entities; its independent F5 single-line path still lowers. |
|
||||
|
||||
## Deferred source queries
|
||||
|
||||
Every CADFS topology selector now retains a `selector_intent`, including
|
||||
selectors for query families and source versions not present in this matrix.
|
||||
Such selectors use `evidence: "feature_script_query"` and a non-executable
|
||||
`multiplicity: "none"` policy. Their geometry and legacy stable ID remain
|
||||
diagnostic context only: `TopologyRegistry.resolve` rejects them before either
|
||||
can bind. Direct datum planes are the sole exception; they use
|
||||
`query_family: "GEOMETRIC"` with `evidence: "explicit_datum"` and retain the
|
||||
explicit-frame resolution path.
|
||||
|
||||
This closes the former legacy path where `1793` loft `SWEPT_EDGE` selectors in
|
||||
`00005267` F4 could bind from endpoint bounding boxes despite no registered
|
||||
FeatureScript query contract. The current result is a `selector_query_unsupported`
|
||||
failure with the executable F3 checkpoint exported. Equivalent bounded
|
||||
rejections now cover unproven CAP/SWEPT/OFFSET/COPY/INTERSECT fallbacks; they do
|
||||
not reduce the matrix's direct-prism coverage or claim general query support.
|
||||
|
||||
The repository does not currently vendor the FeatureScript standard-library
|
||||
sources for other versions or `makeQuery` encodings. They are not inferred
|
||||
from a `15xx` prefix or a single corpus sample.
|
||||
@@ -6,6 +6,27 @@ from .featurescript_lexer import Token, lex
|
||||
from .ir import Call, FeatureIR, ModelIR, SketchIR
|
||||
|
||||
|
||||
_IMPORT_RE = re.compile(r"\bimport\s*\(\s*(?P<arguments>.*?)\s*\)\s*;", re.DOTALL)
|
||||
_IMPORT_PATH_RE = re.compile(r"\bpath\s*:\s*['\"]([^'\"]+)['\"]")
|
||||
_IMPORT_VERSION_RE = re.compile(r"\bversion\s*:\s*['\"]([^'\"]+)['\"]")
|
||||
|
||||
|
||||
def _standard_library_imports(source: str) -> list[dict[str, str]]:
|
||||
"""Preserve each directly imported Onshape standard-library revision."""
|
||||
imports: list[dict[str, str]] = []
|
||||
for match in _IMPORT_RE.finditer(source):
|
||||
arguments = match.group("arguments")
|
||||
path = _IMPORT_PATH_RE.search(arguments)
|
||||
if path is None or "onshape/std/" not in path.group(1):
|
||||
continue
|
||||
item = {"path": path.group(1)}
|
||||
version = _IMPORT_VERSION_RE.search(arguments)
|
||||
if version is not None:
|
||||
item["version"] = version.group(1)
|
||||
imports.append(item)
|
||||
return imports
|
||||
|
||||
|
||||
def _string(value: Any) -> Any:
|
||||
if isinstance(value, str) and len(value) >= 2 and value[0] in {'"', "'"} and value[-1] == value[0]:
|
||||
try:
|
||||
@@ -133,15 +154,15 @@ def _arg_map(call: Call) -> dict[str, Any]:
|
||||
def parse_featurescript(source: str, sample_id: str = "unknown") -> ModelIR:
|
||||
parser = Parser(source); calls = parser.statements()
|
||||
version = re.search(r"\bFeatureScript\s+(\d+(?:\.\d+)*)\s*;", source)
|
||||
standard_library = re.search(
|
||||
r"\bimport\s*\(\s*path\s*:\s*[\"']([^\"']*onshape/std/[^\"']*)[\"']",
|
||||
source,
|
||||
)
|
||||
standard_library_imports = _standard_library_imports(source)
|
||||
primary_standard_library = standard_library_imports[0] if standard_library_imports else {}
|
||||
model = ModelIR(
|
||||
sample_id,
|
||||
raw_source=source,
|
||||
featurescript_version=version.group(1) if version else None,
|
||||
standard_library=standard_library.group(1) if standard_library else None,
|
||||
standard_library=primary_standard_library.get("path"),
|
||||
standard_library_version=primary_standard_library.get("version"),
|
||||
standard_library_imports=standard_library_imports,
|
||||
)
|
||||
for call in calls:
|
||||
if call.name == "newSketch":
|
||||
|
||||
@@ -41,3 +41,5 @@ class ModelIR:
|
||||
# selector can be audited against the FeatureScript API contract it used.
|
||||
featurescript_version: str | None = None
|
||||
standard_library: str | None = None
|
||||
standard_library_version: str | None = None
|
||||
standard_library_imports: list[dict[str, str]] = field(default_factory=list)
|
||||
|
||||
+1468
-67
File diff suppressed because it is too large
Load Diff
+51
-37
@@ -6,36 +6,38 @@ 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
|
||||
def _last_executable_prefix(error: Exception, output: Path) -> dict[str, Any] | None:
|
||||
"""Export the checkpoint already produced by the failed live replay."""
|
||||
execution = getattr(error, "incremental_execution", None)
|
||||
bound = getattr(error, "bound_cdsl", None)
|
||||
if execution is None or not isinstance(bound, dict):
|
||||
return None
|
||||
session = execution.session
|
||||
if not session.results or (session.body is None and not session.surface_members):
|
||||
return None
|
||||
from engine.cdsl_engine.runtime import finalize_cdsl_execution
|
||||
|
||||
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),
|
||||
try:
|
||||
result = finalize_cdsl_execution(execution, output)
|
||||
except Exception:
|
||||
return None
|
||||
last_feature_id = next(reversed(session.results))
|
||||
features = list(bound.get("features") or [])
|
||||
feature_count = next(
|
||||
(index + 1 for index, feature in enumerate(features) if feature.get("id") == last_feature_id),
|
||||
0,
|
||||
)
|
||||
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
|
||||
if feature_count == 0:
|
||||
return None
|
||||
prefix = deepcopy(bound)
|
||||
prefix["features"] = features[:feature_count]
|
||||
return {
|
||||
"failed_feature_id": getattr(error, "failed_feature_id", None) or _failed_feature_id(error),
|
||||
"feature_count": feature_count,
|
||||
"last_feature_id": last_feature_id,
|
||||
"bound_cdsl": prefix,
|
||||
"result": result,
|
||||
}
|
||||
|
||||
|
||||
def _failed_feature_id(error: Exception) -> str | None:
|
||||
@@ -48,22 +50,34 @@ def _failed_feature_id(error: Exception) -> str | 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
|
||||
from engine.cdsl_engine.runtime import analyze_cdsl, finalize_cdsl_execution
|
||||
from .selector_binding import bind_and_execute_candidate_selectors
|
||||
|
||||
analysis = analyze_cdsl(cdsl)
|
||||
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)
|
||||
return {"status": "rebuilt", "analysis": analysis_dict, "selector_binding": binding, "bound_cdsl": bound, "result": result}
|
||||
replay = bind_and_execute_candidate_selectors(cdsl)
|
||||
result = finalize_cdsl_execution(replay.execution, output)
|
||||
return {
|
||||
"status": "rebuilt",
|
||||
"analysis": analysis_dict,
|
||||
"selector_binding": replay.evidence,
|
||||
"bound_cdsl": replay.bound_cdsl,
|
||||
"result": result,
|
||||
}
|
||||
except Exception as exc:
|
||||
detail = {"type": type(exc).__name__, "message": str(exc)}
|
||||
if hasattr(exc, "selector_resolutions"): detail["selector_resolutions"] = exc.selector_resolutions
|
||||
prefix = _last_executable_prefix(cdsl, _failed_feature_id(exc), output)
|
||||
if hasattr(exc, "selector_resolutions"):
|
||||
detail["selector_resolutions"] = exc.selector_resolutions
|
||||
prefix = _last_executable_prefix(exc, output)
|
||||
result = {"status": "rebuild_failed", "analysis": analysis_dict, "error": detail}
|
||||
if bound is not None: result["bound_cdsl"] = bound
|
||||
bound = getattr(exc, "bound_cdsl", None)
|
||||
if isinstance(bound, dict):
|
||||
result["bound_cdsl"] = bound
|
||||
binding = getattr(exc, "selector_binding", None)
|
||||
if isinstance(binding, list):
|
||||
result["selector_binding"] = binding
|
||||
if prefix is not None: result["last_executable_prefix"] = prefix
|
||||
return result
|
||||
|
||||
+230
-140
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Any
|
||||
@@ -95,6 +96,14 @@ def _binding_targets(selector: dict[str, Any]):
|
||||
yield selector
|
||||
|
||||
|
||||
def _runtime_selector(placeholder: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Convert a legacy prefix placeholder to the runtime selector contract."""
|
||||
selector = dict(placeholder)
|
||||
selector.setdefault("source", "runtime_snapshot")
|
||||
selector.setdefault("confidence", 1.0)
|
||||
return selector
|
||||
|
||||
|
||||
def _bound_selector(placeholder: dict[str, Any], records: list[dict[str, Any]]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||
bound = []
|
||||
for record in records:
|
||||
@@ -109,160 +118,241 @@ def _bound_selector(placeholder: dict[str, Any], records: list[dict[str, Any]])
|
||||
return bound[0], bound
|
||||
|
||||
|
||||
def _selector_roots(feature: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
roots = list(feature.get("selectors") or [])
|
||||
for name in ("end_condition", "reverse_end_condition"):
|
||||
condition = (feature.get("params") or {}).get(name)
|
||||
reference = condition.get("reference") if isinstance(condition, dict) else None
|
||||
if isinstance(reference, dict):
|
||||
roots.append(reference)
|
||||
return roots
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def _bind_feature_selectors(
|
||||
feature: dict[str, Any],
|
||||
*,
|
||||
registry: Any,
|
||||
body_id_for_feature: dict[str, str | None],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Bind one feature immediately before it runs in the shared session."""
|
||||
targets = [target for root in _selector_roots(feature) for target in _binding_targets(root)]
|
||||
resolved: list[dict[str, Any]] = []
|
||||
for placeholder in targets:
|
||||
binding_feature_id = placeholder.get("binding_feature_id")
|
||||
if binding_feature_id is None:
|
||||
active_body_id = body_id_for_feature.get("__current__")
|
||||
else:
|
||||
if not isinstance(binding_feature_id, str) or binding_feature_id not in body_id_for_feature:
|
||||
raise ValueError(f"{feature['id']}: selector binding feature is missing or forward")
|
||||
active_body_id = body_id_for_feature[binding_feature_id]
|
||||
intent = placeholder.get("selector_intent")
|
||||
runtime_selector = _runtime_selector(placeholder)
|
||||
resolution = registry.resolve(runtime_selector, active_body_id=active_body_id)
|
||||
# Legacy owner-qualified, geometry-free context selectors predate the
|
||||
# runtime's canonical evidence fields. Their compatibility contract
|
||||
# permits a unique active context object, but never relaxes a
|
||||
# provenance, output-role, or instance-locked selector.
|
||||
if (
|
||||
resolution.status != "resolved"
|
||||
and not isinstance(intent, dict)
|
||||
and not runtime_selector.get("owner_match_required")
|
||||
and not runtime_selector.get("output_role")
|
||||
):
|
||||
fallback = dict(runtime_selector)
|
||||
fallback.pop("owner_feature_id", None)
|
||||
resolution = registry.resolve(fallback, active_body_id=active_body_id)
|
||||
if resolution.status != "resolved":
|
||||
code = resolution.diagnostic.code if resolution.diagnostic is not None else f"selector_{resolution.status}"
|
||||
raise ValueError(f"{feature['id']}: {code} during incremental replay")
|
||||
selected = list(resolution.records or ((resolution.record,) if resolution.record is not None else ()))
|
||||
if not selected:
|
||||
raise ValueError(f"{feature['id']}: selector_not_found during incremental replay")
|
||||
public_records = [record.public_dict() for record in selected]
|
||||
# Operation-role and provenance selectors stay declarative in bound
|
||||
# CDSL. A runtime record ID is execution evidence, never their durable
|
||||
# semantic replacement.
|
||||
if (isinstance(intent, dict) and intent.get("query_family") != "GEOMETRIC") or placeholder.get("output_role"):
|
||||
resolved.extend(public_records)
|
||||
continue
|
||||
selector, bound_selectors = _bound_selector(placeholder, public_records)
|
||||
placeholder.clear()
|
||||
placeholder.update(selector)
|
||||
resolved.extend(bound_selectors)
|
||||
selectors = feature.get("selectors") or []
|
||||
feature["selectors"] = list({_selector_key(selector): selector for selector in selectors}.values())
|
||||
if feature.get("atomic_id") == "pattern_mirror":
|
||||
planes = [selector for selector in feature["selectors"] if selector.get("kind") == "plane"]
|
||||
if len(planes) != 1:
|
||||
raise ValueError(f"{feature['id']}: mirror plane binding is not unique")
|
||||
feature.setdefault("params", {})["mirror_plane"] = planes[0]
|
||||
return resolved
|
||||
|
||||
|
||||
@dataclass
|
||||
class IncrementalBindingReplay:
|
||||
"""Bound candidate plus the one session that produced its evidence."""
|
||||
|
||||
bound_cdsl: dict[str, Any]
|
||||
evidence: list[dict[str, Any]]
|
||||
execution: Any
|
||||
|
||||
|
||||
def bind_and_execute_candidate_selectors(cdsl: dict[str, Any]) -> IncrementalBindingReplay:
|
||||
"""Bind and execute a CADFS candidate in one ordered kernel replay.
|
||||
|
||||
A feature is bound only against topology facts registered by earlier
|
||||
features in this session. Historical ``binding_feature_id`` values select
|
||||
a retained snapshot ID, so the binder never needs to re-run a prefix or
|
||||
recreate an OCC body. Resolver failures retain the live session for the
|
||||
caller to export its last executable checkpoint.
|
||||
"""
|
||||
from engine.cdsl_engine.runtime import prepare_cdsl_execution
|
||||
|
||||
bound = deepcopy(cdsl)
|
||||
execution = prepare_cdsl_execution(bound)
|
||||
if not execution.analysis.runtime_eligible:
|
||||
first = next((result for result in execution.analysis.feature_results if not result.executable), None)
|
||||
code = first.blockers[0].code if first and first.blockers else "runtime_ineligible"
|
||||
raise ValueError(code)
|
||||
bound_features = {str(feature.get("id") or ""): feature for feature in bound.get("features") or []}
|
||||
body_id_for_feature: dict[str, str | None] = {"__current__": None}
|
||||
evidence: list[dict[str, Any]] = []
|
||||
for index, node in enumerate(execution.analysis.plan):
|
||||
feature = node.source_feature
|
||||
try:
|
||||
resolved = _bind_feature_selectors(
|
||||
feature,
|
||||
registry=execution.session.topology,
|
||||
body_id_for_feature=body_id_for_feature,
|
||||
)
|
||||
public_feature = bound_features[node.feature_id]
|
||||
public_feature["selectors"] = deepcopy(feature.get("selectors") or [])
|
||||
public_feature["params"] = deepcopy(feature.get("params") or {})
|
||||
evidence.append({
|
||||
"feature_id": node.feature_id,
|
||||
"prefix_feature_count": index,
|
||||
"selectors": public_feature["selectors"],
|
||||
"resolved": resolved,
|
||||
})
|
||||
execution.execute_next(strict=True)
|
||||
body_id_for_feature[node.feature_id] = execution.session.body_id
|
||||
body_id_for_feature["__current__"] = execution.session.body_id
|
||||
except Exception as error:
|
||||
setattr(error, "bound_cdsl", bound)
|
||||
setattr(error, "selector_binding", evidence)
|
||||
setattr(error, "incremental_execution", execution)
|
||||
setattr(error, "failed_feature_id", node.feature_id)
|
||||
raise
|
||||
return IncrementalBindingReplay(bound, evidence, execution)
|
||||
|
||||
|
||||
def bind_candidate_selectors(cdsl: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||
"""Rebuild every selector-bearing prefix and bind against its active body."""
|
||||
"""Legacy diagnostic binder for incomplete or externally supplied CDSL.
|
||||
|
||||
The production CADFS rebuild path uses ``bind_and_execute_candidate_selectors``.
|
||||
This compatibility entry point deliberately keeps prefix replay explicit
|
||||
for callers that need to inspect a partial document before it is runtime
|
||||
eligible. It rehydrates exact exported topology facts and calls the same
|
||||
resolver; provenance selectors never become geometry guesses here.
|
||||
"""
|
||||
from engine.cdsl_engine.runtime import rebuild_cdsl
|
||||
bound = deepcopy(cdsl); evidence = []
|
||||
from engine.cdsl_engine.topology import TopologyRegistry
|
||||
|
||||
bound = deepcopy(cdsl)
|
||||
evidence: list[dict[str, Any]] = []
|
||||
with tempfile.TemporaryDirectory(prefix="cadfs-bind-") as temporary:
|
||||
for index, feature in enumerate(bound.get("features") or []):
|
||||
roots = list(feature.get("selectors") or [])
|
||||
for name in ("end_condition", "reverse_end_condition"):
|
||||
condition = (feature.get("params") or {}).get(name)
|
||||
reference = condition.get("reference") if isinstance(condition, dict) else None
|
||||
if isinstance(reference, dict): roots.append(reference)
|
||||
targets = [target for root in roots for target in _binding_targets(root)]
|
||||
if not targets: continue
|
||||
prefix_cache: dict[int, tuple[list[dict[str, Any]], str | None]] = {}
|
||||
targets = [target for root in _selector_roots(feature) for target in _binding_targets(root)]
|
||||
if not targets:
|
||||
continue
|
||||
prefix_cache: dict[int, tuple[list[dict[str, Any]], str | None, list[dict[str, Any]]]] = {}
|
||||
|
||||
def prefix_records(binding_feature_id: str | None, owner_feature_id: str | None) -> list[dict[str, Any]]:
|
||||
def prefix_snapshot(binding_feature_id: str | None) -> tuple[list[dict[str, Any]], str | None, list[dict[str, Any]]]:
|
||||
prefix_count = index
|
||||
if binding_feature_id is not None:
|
||||
binding_index = next((item_index for item_index, item in enumerate(bound["features"][:index]) if item["id"] == binding_feature_id), None)
|
||||
if binding_index is None: raise ValueError(f"{feature['id']}: selector binding feature is missing or forward")
|
||||
binding_index = next(
|
||||
(item_index for item_index, item in enumerate(bound["features"][:index]) if item["id"] == binding_feature_id),
|
||||
None,
|
||||
)
|
||||
if binding_index is None:
|
||||
raise ValueError(f"{feature['id']}: selector binding feature is missing or forward")
|
||||
prefix_count = binding_index + 1
|
||||
if prefix_count not in prefix_cache:
|
||||
prefix = deepcopy(bound); prefix["features"] = bound["features"][:prefix_count]
|
||||
if not prefix["features"]: raise ValueError(f"{feature['id']}: selector has no executable prefix")
|
||||
report = rebuild_cdsl(prefix, Path(temporary) / f"prefix-{index}-{prefix_count}.step", strict=True)
|
||||
body_id = next((item.get("body_id") for item in reversed(report.get("feature_results") or []) if item.get("body_id")), None)
|
||||
prefix_cache[prefix_count] = (list(report.get("topology_records") or []), body_id)
|
||||
all_records, body_id = prefix_cache[prefix_count]
|
||||
return [
|
||||
item for item in all_records
|
||||
# Reference planes and axes are session context, not body
|
||||
# topology. They must remain available while binding a mirror
|
||||
# or extent selector against a body-bearing prefix.
|
||||
if item.get("kind") in {"plane", "axis"}
|
||||
or not body_id
|
||||
or item.get("body_id") == body_id
|
||||
or str(item.get("body_id") or "").startswith(f"{body_id}:")
|
||||
]
|
||||
|
||||
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()
|
||||
intent = placeholder.get("selector_intent")
|
||||
if (
|
||||
isinstance(intent, dict)
|
||||
and intent.get("query_family") != "GEOMETRIC"
|
||||
and not output_role
|
||||
):
|
||||
# Prefix rebuilding is retained as a diagnostic adapter.
|
||||
# It must not turn an unproven FeatureScript provenance
|
||||
# query into a geometry-scored stable selector.
|
||||
raise ValueError(
|
||||
f"{feature['id']}: selector_kernel_history_missing after prefix rebuild"
|
||||
prefix = deepcopy(bound)
|
||||
prefix["features"] = bound["features"][:prefix_count]
|
||||
if not prefix["features"]:
|
||||
raise ValueError(f"{feature['id']}: selector has no executable prefix")
|
||||
report = rebuild_cdsl(
|
||||
prefix,
|
||||
Path(temporary) / f"prefix-{index}-{prefix_count}.step",
|
||||
strict=True,
|
||||
)
|
||||
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")])]
|
||||
# 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:
|
||||
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:
|
||||
raise ValueError(f"{feature['id']}: selector_ambiguous after prefix rebuild")
|
||||
candidates = [item[1] for item in scored] if placeholder.get("match_mode") == "all" else [scored[0][1]]
|
||||
if not candidates: raise ValueError(f"{feature['id']}: selector_not_found after prefix rebuild")
|
||||
selector, bound_selectors = _bound_selector(placeholder, candidates)
|
||||
placeholder.clear(); placeholder.update(selector)
|
||||
resolved.extend(bound_selectors)
|
||||
feature_selectors = feature.get("selectors") or []
|
||||
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,
|
||||
)
|
||||
body_id = next(
|
||||
(item.get("body_id") for item in reversed(report.get("feature_results") or []) if item.get("body_id")),
|
||||
None,
|
||||
)
|
||||
prefix_cache[prefix_count] = (
|
||||
list(report.get("topology_records") or []),
|
||||
body_id,
|
||||
list(report.get("topology_deltas") or []),
|
||||
)
|
||||
return prefix_cache[prefix_count]
|
||||
|
||||
unique = {selector_key(selector): selector for selector in feature_selectors}
|
||||
feature["selectors"] = list(unique.values())
|
||||
resolved: list[dict[str, Any]] = []
|
||||
for placeholder in targets:
|
||||
records, body_id, topology_deltas = prefix_snapshot(placeholder.get("binding_feature_id"))
|
||||
registry = TopologyRegistry.from_public_snapshot(records, topology_deltas)
|
||||
intent = placeholder.get("selector_intent")
|
||||
runtime_selector = _runtime_selector(placeholder)
|
||||
resolution = registry.resolve(runtime_selector, active_body_id=body_id)
|
||||
if (
|
||||
resolution.status != "resolved"
|
||||
and not isinstance(intent, dict)
|
||||
and not runtime_selector.get("owner_match_required")
|
||||
and not runtime_selector.get("output_role")
|
||||
):
|
||||
fallback = dict(runtime_selector)
|
||||
fallback.pop("owner_feature_id", None)
|
||||
resolution = registry.resolve(fallback, active_body_id=body_id)
|
||||
if resolution.status != "resolved":
|
||||
code = resolution.diagnostic.code if resolution.diagnostic is not None else f"selector_{resolution.status}"
|
||||
raise ValueError(f"{feature['id']}: {code} after prefix rebuild")
|
||||
selected = list(resolution.records or ((resolution.record,) if resolution.record is not None else ()))
|
||||
if not selected:
|
||||
raise ValueError(f"{feature['id']}: selector_not_found after prefix rebuild")
|
||||
public_records = [record.public_dict() for record in selected]
|
||||
if (isinstance(intent, dict) and intent.get("query_family") != "GEOMETRIC") or placeholder.get("output_role"):
|
||||
resolved.extend(public_records)
|
||||
continue
|
||||
selector, bound_selectors = _bound_selector(placeholder, public_records)
|
||||
placeholder.clear()
|
||||
placeholder.update(selector)
|
||||
resolved.extend(bound_selectors)
|
||||
feature["selectors"] = list({_selector_key(selector): selector for selector in feature.get("selectors") or []}.values())
|
||||
if feature.get("atomic_id") == "pattern_mirror":
|
||||
planes = [selector for selector in feature["selectors"] if selector.get("kind") == "plane"]
|
||||
if len(planes) != 1:
|
||||
raise ValueError(f"{feature['id']}: mirror plane binding is not unique")
|
||||
feature.setdefault("params", {})["mirror_plane"] = planes[0]
|
||||
evidence.append({"feature_id": feature["id"], "prefix_feature_count": index, "selectors": feature["selectors"], "resolved": resolved})
|
||||
evidence.append({
|
||||
"feature_id": feature["id"],
|
||||
"prefix_feature_count": index,
|
||||
"selectors": feature["selectors"],
|
||||
"resolved": resolved,
|
||||
})
|
||||
return bound, evidence
|
||||
|
||||
@@ -0,0 +1,897 @@
|
||||
"""Offline experiment for geometry-only CADFS selector candidates.
|
||||
|
||||
The production resolver must not consume this module. It removes provenance
|
||||
intent only in a copied CDSL document, then compares that diagnostic replay to
|
||||
the source's final STEP. A strict passing probe is replayed a second time from
|
||||
its bound CDSL and can emit a *behavioural* selector record. The record is
|
||||
training/diagnostic evidence, not a claim that a heuristic recovered Onshape's
|
||||
hidden topology identity.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from copy import deepcopy
|
||||
import json
|
||||
import multiprocessing
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .compare import compare_steps
|
||||
from .dataset import Sample, scan_dataset
|
||||
from .featurescript_parser import parse_featurescript
|
||||
from .lowering import lower_model
|
||||
from .rebuild import rebuild_candidate
|
||||
from .reports import read_json, write_json, write_manifest
|
||||
|
||||
|
||||
def strip_provenance_intents(value: Any) -> int:
|
||||
"""Remove provenance-only selector fields from one copied CDSL value."""
|
||||
removed = 0
|
||||
if isinstance(value, dict):
|
||||
for key in ("selector_intent", "selector_intent_version"):
|
||||
if key in value:
|
||||
value.pop(key)
|
||||
removed += 1
|
||||
for child in value.values():
|
||||
removed += strip_provenance_intents(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
removed += strip_provenance_intents(child)
|
||||
return removed
|
||||
|
||||
|
||||
def _write_rebuild_artifacts(directory: Path, name: str, outcome: dict[str, Any]) -> None:
|
||||
"""Persist the same useful replay artifacts as the regular pipeline."""
|
||||
artifact = deepcopy(outcome)
|
||||
bound = artifact.pop("bound_cdsl", None)
|
||||
prefix = artifact.get("last_executable_prefix")
|
||||
if isinstance(prefix, dict):
|
||||
prefix_bound = prefix.pop("bound_cdsl", None)
|
||||
if bound is None:
|
||||
bound = prefix_bound
|
||||
write_json(directory / f"{name}.rebuild.json", artifact)
|
||||
if isinstance(bound, dict):
|
||||
write_json(directory / f"{name}.bound.cdsl.json", bound)
|
||||
|
||||
|
||||
def _rebuild_worker(candidate: str, step: str, result: str) -> None:
|
||||
write_json(Path(result), rebuild_candidate(read_json(Path(candidate)), Path(step)))
|
||||
|
||||
|
||||
def _compare_worker(gold: str, rebuilt: str, result: str) -> None:
|
||||
write_json(Path(result), compare_steps(Path(gold), Path(rebuilt)))
|
||||
|
||||
|
||||
def _isolated(target: Any, args: tuple[str, ...], result_path: Path, timeout_seconds: float) -> str:
|
||||
"""Bound an OCC experiment so one candidate cannot stall the demo."""
|
||||
result_path.unlink(missing_ok=True)
|
||||
process = multiprocessing.get_context("spawn").Process(target=target, args=args)
|
||||
process.start()
|
||||
process.join(timeout_seconds)
|
||||
if process.is_alive():
|
||||
process.terminate()
|
||||
process.join(5)
|
||||
if process.is_alive():
|
||||
process.kill()
|
||||
process.join()
|
||||
return "timeout"
|
||||
return "completed" if process.exitcode == 0 and result_path.exists() else "failed"
|
||||
|
||||
|
||||
def _run_rebuild(candidate_path: Path, step: Path, *, timeout_seconds: float) -> dict[str, Any]:
|
||||
worker = step.with_suffix(".worker.json")
|
||||
outcome = _isolated(_rebuild_worker, (str(candidate_path), str(step), str(worker)), worker, timeout_seconds)
|
||||
if outcome == "completed":
|
||||
result = read_json(worker)
|
||||
worker.unlink(missing_ok=True)
|
||||
return result
|
||||
step.unlink(missing_ok=True)
|
||||
return {
|
||||
"status": "rebuild_timeout" if outcome == "timeout" else "rebuild_failed",
|
||||
"error": {
|
||||
"type": "TimeoutError" if outcome == "timeout" else "WorkerProcessError",
|
||||
"message": f"selector candidate rebuild exceeded {timeout_seconds:g} seconds" if outcome == "timeout" else "selector candidate rebuild worker exited without a result",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _run_comparison(gold_step: Path, rebuilt_step: Path, output: Path, *, timeout_seconds: float) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
||||
worker = output.with_suffix(".worker.json")
|
||||
outcome = _isolated(_compare_worker, (str(gold_step), str(rebuilt_step), str(worker)), worker, timeout_seconds)
|
||||
if outcome == "completed":
|
||||
comparison = read_json(worker)
|
||||
worker.unlink(missing_ok=True)
|
||||
return comparison, None
|
||||
return None, {
|
||||
"type": "TimeoutError" if outcome == "timeout" else "WorkerProcessError",
|
||||
"message": f"selector candidate comparison exceeded {timeout_seconds:g} seconds" if outcome == "timeout" else "selector candidate comparison worker exited without a result",
|
||||
}
|
||||
|
||||
|
||||
def _outcome_summary(outcome: dict[str, Any], comparison: dict[str, Any] | None) -> dict[str, Any]:
|
||||
summary: dict[str, Any] = {
|
||||
"rebuild_status": outcome.get("status"),
|
||||
"last_executable_prefix": {
|
||||
key: value
|
||||
for key, value in (outcome.get("last_executable_prefix") or {}).items()
|
||||
if key in {"failed_feature_id", "feature_count", "last_feature_id"}
|
||||
},
|
||||
}
|
||||
error = outcome.get("error")
|
||||
if isinstance(error, dict):
|
||||
summary["error"] = {key: error.get(key) for key in ("type", "message")}
|
||||
if comparison is not None:
|
||||
summary["comparison"] = {
|
||||
"decision": comparison.get("decision"),
|
||||
"strict_passed": bool((comparison.get("strict") or {}).get("passed")),
|
||||
"rp_passed": bool((comparison.get("rp") or {}).get("passed")),
|
||||
}
|
||||
return summary
|
||||
|
||||
|
||||
def _strict_passed(comparison: dict[str, Any] | None) -> bool:
|
||||
return bool(isinstance(comparison, dict) and (comparison.get("strict") or {}).get("passed"))
|
||||
|
||||
|
||||
def _selector_sites(feature: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
|
||||
"""Return the public selector positions of one CDSL feature.
|
||||
|
||||
The locations are deliberately structural rather than runtime IDs. A
|
||||
selector record must remain useful when a fresh replay creates different
|
||||
topology-record instances.
|
||||
"""
|
||||
sites: list[tuple[str, dict[str, Any]]] = []
|
||||
for index, selector in enumerate(feature.get("selectors") or []):
|
||||
if isinstance(selector, dict):
|
||||
sites.append((f"selectors[{index}]", selector))
|
||||
params = feature.get("params") or {}
|
||||
for name in ("end_condition", "reverse_end_condition"):
|
||||
condition = params.get(name)
|
||||
reference = condition.get("reference") if isinstance(condition, dict) else None
|
||||
if isinstance(reference, dict):
|
||||
sites.append((f"params.{name}.reference", reference))
|
||||
return sites
|
||||
|
||||
|
||||
def _semantic_source_selector(selector: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Keep the FeatureScript meaning, never a source runtime topology ID."""
|
||||
intent = selector.get("selector_intent")
|
||||
if not isinstance(intent, dict):
|
||||
return None
|
||||
result = {
|
||||
key: deepcopy(selector[key])
|
||||
for key in ("kind", "owner_feature_id", "binding_feature_id", "output_role")
|
||||
if selector.get(key) is not None
|
||||
}
|
||||
result["selector_intent"] = deepcopy(intent)
|
||||
return result
|
||||
|
||||
|
||||
def _selector_record_entries(
|
||||
sample_id: str,
|
||||
provenance_candidate: dict[str, Any],
|
||||
compiled_cdsl: dict[str, Any],
|
||||
selector_binding: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Extract FeatureScript meaning plus a freshly replayable binding plan.
|
||||
|
||||
A geometry probe has only the selected record, not rejected alternatives.
|
||||
We preserve that limitation explicitly. When source and compiled selector
|
||||
counts differ (for example, one source query expands to several topology
|
||||
records), emit a feature group instead of inventing a one-to-one mapping.
|
||||
"""
|
||||
source_by_id = {
|
||||
str(feature.get("id")): feature
|
||||
for feature in provenance_candidate.get("features") or []
|
||||
if isinstance(feature, dict) and isinstance(feature.get("id"), str)
|
||||
}
|
||||
compiled_by_id = {
|
||||
str(feature.get("id")): feature
|
||||
for feature in compiled_cdsl.get("features") or []
|
||||
if isinstance(feature, dict) and isinstance(feature.get("id"), str)
|
||||
}
|
||||
entries: list[dict[str, Any]] = []
|
||||
for binding in selector_binding:
|
||||
feature_id = binding.get("feature_id")
|
||||
if not isinstance(feature_id, str):
|
||||
continue
|
||||
source_feature = source_by_id.get(feature_id)
|
||||
compiled_feature = compiled_by_id.get(feature_id)
|
||||
if source_feature is None or compiled_feature is None:
|
||||
continue
|
||||
source_sites = [
|
||||
(location, semantic)
|
||||
for location, selector in _selector_sites(source_feature)
|
||||
if (semantic := _semantic_source_selector(selector)) is not None
|
||||
]
|
||||
if not source_sites:
|
||||
continue
|
||||
compiled_sites = _selector_sites(compiled_feature)
|
||||
resolved = [deepcopy(item) for item in binding.get("resolved") or [] if isinstance(item, dict)]
|
||||
feature_context = {
|
||||
"sample_id": sample_id,
|
||||
"feature_id": feature_id,
|
||||
"atomic_id": compiled_feature.get("atomic_id"),
|
||||
"prefix_feature_count": binding.get("prefix_feature_count"),
|
||||
"verification_scope": "behavioural_strict_replay",
|
||||
}
|
||||
if len(source_sites) == len(compiled_sites) == len(resolved):
|
||||
for index, ((location, source_selector), (_compiled_location, compiled_selector), resolved_record) in enumerate(
|
||||
zip(source_sites, compiled_sites, resolved)
|
||||
):
|
||||
entries.append({
|
||||
"schema": "cadfs_to_cdsl.selector_record.v1",
|
||||
"entry_id": f"{sample_id}:{feature_id}:{location}",
|
||||
**feature_context,
|
||||
"source_location": location,
|
||||
"source_selector": source_selector,
|
||||
"compiled_selector": deepcopy(compiled_selector),
|
||||
"resolved_record": resolved_record,
|
||||
"mapping": "one_to_one",
|
||||
"limitations": [
|
||||
"The source dataset has no per-feature Onshape query witness.",
|
||||
"The compiled selector is verified only by a fresh local strict replay.",
|
||||
"Runtime snapshot IDs are replay evidence, not portable topology identities.",
|
||||
],
|
||||
})
|
||||
continue
|
||||
entries.append({
|
||||
"schema": "cadfs_to_cdsl.selector_record.v1",
|
||||
"entry_id": f"{sample_id}:{feature_id}:feature_group",
|
||||
**feature_context,
|
||||
"source_selectors": [
|
||||
{"location": location, "selector": selector}
|
||||
for location, selector in source_sites
|
||||
],
|
||||
"compiled_selectors": [
|
||||
{"location": location, "selector": deepcopy(selector)}
|
||||
for location, selector in compiled_sites
|
||||
],
|
||||
"resolved_records": resolved,
|
||||
"mapping": "feature_group",
|
||||
"limitations": [
|
||||
"The source query expanded or contracted during local binding, so this record does not claim a one-to-one mapping.",
|
||||
"The source dataset has no per-feature Onshape query witness.",
|
||||
"The compiled selector is verified only by a fresh local strict replay.",
|
||||
"Runtime snapshot IDs are replay evidence, not portable topology identities.",
|
||||
],
|
||||
})
|
||||
return entries
|
||||
|
||||
|
||||
def _emit_strict_selector_record(
|
||||
sample: Sample,
|
||||
directory: Path,
|
||||
provenance_candidate: dict[str, Any],
|
||||
geometry_probe: dict[str, Any],
|
||||
geometry_comparison: dict[str, Any] | None,
|
||||
gold_step: Path,
|
||||
*,
|
||||
rebuild_timeout_seconds: float,
|
||||
comparison_timeout_seconds: float,
|
||||
) -> dict[str, Any]:
|
||||
"""Create and independently verify a compiled CDSL only after strict proof."""
|
||||
if not _strict_passed(geometry_comparison):
|
||||
return {
|
||||
"status": "not_emitted",
|
||||
"reason": "geometry_probe_not_strict",
|
||||
}
|
||||
bound_cdsl = geometry_probe.get("bound_cdsl")
|
||||
binding = geometry_probe.get("selector_binding")
|
||||
if not isinstance(bound_cdsl, dict) or not isinstance(binding, list):
|
||||
return {
|
||||
"status": "not_emitted",
|
||||
"reason": "geometry_probe_binding_missing",
|
||||
}
|
||||
|
||||
compiled_candidate = deepcopy(bound_cdsl)
|
||||
compiled_path = directory / "selector_record.compiled.cdsl.json"
|
||||
write_json(compiled_path, compiled_candidate)
|
||||
compiled = _run_rebuild(
|
||||
compiled_path,
|
||||
directory / "selector_record.compiled.rebuild.step",
|
||||
timeout_seconds=rebuild_timeout_seconds,
|
||||
)
|
||||
_write_rebuild_artifacts(directory, "selector_record.compiled", compiled)
|
||||
comparison = None
|
||||
comparison_error = None
|
||||
if compiled.get("status") == "rebuilt":
|
||||
comparison, comparison_error = _run_comparison(
|
||||
gold_step,
|
||||
directory / "selector_record.compiled.rebuild.step",
|
||||
directory / "selector_record.compiled.comparison.json",
|
||||
timeout_seconds=comparison_timeout_seconds,
|
||||
)
|
||||
if comparison is not None:
|
||||
write_json(directory / "selector_record.compiled.comparison.json", comparison)
|
||||
|
||||
result = {
|
||||
"status": "not_emitted",
|
||||
"compiled_cdsl": str(compiled_path),
|
||||
"compiled_replay": _outcome_summary(compiled, comparison),
|
||||
}
|
||||
if comparison_error is not None:
|
||||
result["compiled_replay"]["comparison_error"] = comparison_error
|
||||
if not _strict_passed(comparison):
|
||||
result["reason"] = "compiled_cdsl_not_strict"
|
||||
return result
|
||||
|
||||
records = _selector_record_entries(sample.sample_id, provenance_candidate, compiled_candidate, binding)
|
||||
if not records:
|
||||
result["reason"] = "no_featurescript_selector_binding"
|
||||
return result
|
||||
record_path = directory / "selector-record.json"
|
||||
write_json(record_path, {
|
||||
"schema": "cadfs_to_cdsl.selector_record_set.v1",
|
||||
"sample_id": sample.sample_id,
|
||||
"source": {
|
||||
"featurescript": sample.files.get("featurescript"),
|
||||
"step": sample.files.get("step"),
|
||||
},
|
||||
"verification": {
|
||||
"geometry_probe_strict": True,
|
||||
"compiled_cdsl_strict": True,
|
||||
"compiled_cdsl": str(compiled_path),
|
||||
},
|
||||
"records": records,
|
||||
})
|
||||
result.update({
|
||||
"status": "strict_replayed",
|
||||
"record_count": len(records),
|
||||
"record_path": str(record_path),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def _searchable_selector_sites(feature: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
|
||||
"""Return direct selector roots that can be replaced as a search branch.
|
||||
|
||||
The experimental search does not flatten an intersection query into an
|
||||
unrelated member. It can search direct roots and extent references; other
|
||||
query trees remain a bounded unsupported search outcome.
|
||||
"""
|
||||
sites = _selector_sites(feature)
|
||||
return [
|
||||
(location, selector)
|
||||
for location, selector in sites
|
||||
if not isinstance(selector.get("intersection_of"), list)
|
||||
]
|
||||
|
||||
|
||||
def _replace_selector_site(cdsl: dict[str, Any], feature_id: str, location: str, selector: dict[str, Any]) -> None:
|
||||
"""Replace one public direct selector site in a copied candidate CDSL."""
|
||||
feature = next(
|
||||
(item for item in cdsl.get("features") or [] if isinstance(item, dict) and item.get("id") == feature_id),
|
||||
None,
|
||||
)
|
||||
if feature is None:
|
||||
raise ValueError(f"search branch cannot find feature {feature_id}")
|
||||
if location.startswith("selectors[") and location.endswith("]"):
|
||||
index = int(location[len("selectors["):-1])
|
||||
selectors = feature.get("selectors") or []
|
||||
if index < 0 or index >= len(selectors) or not isinstance(selectors[index], dict):
|
||||
raise ValueError(f"search branch selector site is unavailable: {feature_id}:{location}")
|
||||
selectors[index] = deepcopy(selector)
|
||||
return
|
||||
prefix = "params."
|
||||
suffix = ".reference"
|
||||
if location.startswith(prefix) and location.endswith(suffix):
|
||||
name = location[len(prefix):-len(suffix)]
|
||||
condition = (feature.get("params") or {}).get(name)
|
||||
if not isinstance(condition, dict) or not isinstance(condition.get("reference"), dict):
|
||||
raise ValueError(f"search branch selector site is unavailable: {feature_id}:{location}")
|
||||
condition["reference"] = deepcopy(selector)
|
||||
return
|
||||
raise ValueError(f"search branch does not support nested selector site: {feature_id}:{location}")
|
||||
|
||||
|
||||
def _forced_candidate_selector(placeholder: dict[str, Any], candidate: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Turn one public runtime record into a branch-local explicit selector."""
|
||||
from .selector_binding import _bound_selector
|
||||
|
||||
selector, _bound = _bound_selector(placeholder, [candidate])
|
||||
if not isinstance(selector, dict):
|
||||
raise ValueError("search branch produced an invalid explicit selector")
|
||||
return selector
|
||||
|
||||
|
||||
def _resolve_search_selector(placeholder: dict[str, Any], *, registry: Any, active_body_id: str | None) -> Any:
|
||||
"""Mirror the legacy diagnostic binding policy without changing it."""
|
||||
from .selector_binding import _runtime_selector
|
||||
|
||||
runtime_selector = _runtime_selector(placeholder)
|
||||
resolution = registry.resolve(runtime_selector, active_body_id=active_body_id)
|
||||
intent = placeholder.get("selector_intent")
|
||||
if (
|
||||
resolution.status != "resolved"
|
||||
and not isinstance(intent, dict)
|
||||
and not runtime_selector.get("owner_match_required")
|
||||
and not runtime_selector.get("output_role")
|
||||
):
|
||||
fallback = dict(runtime_selector)
|
||||
fallback.pop("owner_feature_id", None)
|
||||
resolution = registry.resolve(fallback, active_body_id=active_body_id)
|
||||
return resolution
|
||||
|
||||
|
||||
def _search_candidate_records(
|
||||
resolution: Any,
|
||||
*,
|
||||
maximum: int,
|
||||
minimum_score: float = 0.8,
|
||||
) -> tuple[list[dict[str, Any]], int]:
|
||||
"""Keep only candidates the normal resolver considers geometrically valid."""
|
||||
candidates = [
|
||||
item for item in resolution.candidates
|
||||
if isinstance(item, dict)
|
||||
and item.get("record_id")
|
||||
and float(item.get("score") or 0.0) >= minimum_score
|
||||
]
|
||||
candidates.sort(key=lambda item: (-float(item.get("score") or 0.0), str(item.get("record_id"))))
|
||||
return [deepcopy(item) for item in candidates[:maximum]], len(candidates)
|
||||
|
||||
|
||||
def _search_replay_worker(candidate: str, step: str, result: str, maximum_candidates: int) -> None:
|
||||
"""Replay one branch until it builds or exposes its next selector decision."""
|
||||
from engine.cdsl_engine.runtime import finalize_cdsl_execution, prepare_cdsl_execution
|
||||
from .selector_binding import _bound_selector, _selector_key
|
||||
|
||||
bound = read_json(Path(candidate))
|
||||
execution = prepare_cdsl_execution(bound)
|
||||
if not execution.analysis.runtime_eligible:
|
||||
first = next((item for item in execution.analysis.feature_results if not item.executable), None)
|
||||
code = first.blockers[0].code if first and first.blockers else "runtime_ineligible"
|
||||
write_json(Path(result), {"status": "runtime_ineligible", "reason": code})
|
||||
return
|
||||
bound_features = {
|
||||
str(feature.get("id") or ""): feature
|
||||
for feature in bound.get("features") or []
|
||||
if isinstance(feature, dict)
|
||||
}
|
||||
body_id_for_feature: dict[str, str | None] = {"__current__": None}
|
||||
evidence: list[dict[str, Any]] = []
|
||||
for index, node in enumerate(execution.analysis.plan):
|
||||
feature = node.source_feature
|
||||
sites = _searchable_selector_sites(feature)
|
||||
if len(sites) != len(_selector_sites(feature)):
|
||||
write_json(Path(result), {
|
||||
"status": "search_unsupported",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": "nested_selector_tree",
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
resolved: list[dict[str, Any]] = []
|
||||
for location, placeholder in sites:
|
||||
binding_feature_id = placeholder.get("binding_feature_id")
|
||||
if binding_feature_id is None:
|
||||
active_body_id = body_id_for_feature["__current__"]
|
||||
elif isinstance(binding_feature_id, str) and binding_feature_id in body_id_for_feature:
|
||||
active_body_id = body_id_for_feature[binding_feature_id]
|
||||
else:
|
||||
write_json(Path(result), {
|
||||
"status": "search_failed",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": "selector_binding_feature_missing_or_forward",
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
resolution = _resolve_search_selector(
|
||||
placeholder,
|
||||
registry=execution.session.topology,
|
||||
active_body_id=active_body_id,
|
||||
)
|
||||
if resolution.status != "resolved":
|
||||
candidates, eligible_count = _search_candidate_records(
|
||||
resolution,
|
||||
maximum=maximum_candidates,
|
||||
)
|
||||
if candidates:
|
||||
write_json(Path(result), {
|
||||
"status": "branchable_selector",
|
||||
"feature_id": node.feature_id,
|
||||
"source_location": location,
|
||||
"resolution_status": resolution.status,
|
||||
"diagnostic": resolution.diagnostic.as_dict() if resolution.diagnostic is not None else None,
|
||||
"candidates": candidates,
|
||||
"candidate_count": eligible_count,
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
code = resolution.diagnostic.code if resolution.diagnostic is not None else f"selector_{resolution.status}"
|
||||
write_json(Path(result), {
|
||||
"status": "search_failed",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": code,
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
selected = list(resolution.records or ((resolution.record,) if resolution.record is not None else ()))
|
||||
if not selected:
|
||||
write_json(Path(result), {
|
||||
"status": "search_failed",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": "selector_not_found",
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
public_records = [record.public_dict() for record in selected]
|
||||
selector, bound_selectors = _bound_selector(placeholder, public_records)
|
||||
placeholder.clear()
|
||||
placeholder.update(selector)
|
||||
resolved.extend(bound_selectors)
|
||||
feature["selectors"] = list({
|
||||
_selector_key(selector): selector
|
||||
for selector in feature.get("selectors") or []
|
||||
}.values())
|
||||
if feature.get("atomic_id") == "pattern_mirror":
|
||||
planes = [selector for selector in feature["selectors"] if selector.get("kind") == "plane"]
|
||||
if len(planes) != 1:
|
||||
write_json(Path(result), {
|
||||
"status": "search_failed",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": "mirror_plane_binding_not_unique",
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
feature.setdefault("params", {})["mirror_plane"] = planes[0]
|
||||
public_feature = bound_features[node.feature_id]
|
||||
public_feature["selectors"] = deepcopy(feature.get("selectors") or [])
|
||||
public_feature["params"] = deepcopy(feature.get("params") or {})
|
||||
evidence.append({
|
||||
"feature_id": node.feature_id,
|
||||
"prefix_feature_count": index,
|
||||
"selectors": public_feature["selectors"],
|
||||
"resolved": resolved,
|
||||
})
|
||||
try:
|
||||
execution.execute_next(strict=True)
|
||||
except Exception as error:
|
||||
write_json(Path(result), {
|
||||
"status": "search_failed",
|
||||
"feature_id": node.feature_id,
|
||||
"reason": str(error),
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
})
|
||||
return
|
||||
body_id_for_feature[node.feature_id] = execution.session.body_id
|
||||
body_id_for_feature["__current__"] = execution.session.body_id
|
||||
write_json(Path(result), {
|
||||
"status": "rebuilt",
|
||||
"bound_cdsl": bound,
|
||||
"selector_binding": evidence,
|
||||
"result": finalize_cdsl_execution(execution, Path(step)),
|
||||
})
|
||||
|
||||
|
||||
def _run_search_replay(candidate_path: Path, step: Path, *, timeout_seconds: float, maximum_candidates: int) -> dict[str, Any]:
|
||||
worker = step.with_suffix(".worker.json")
|
||||
outcome = _isolated(
|
||||
_search_replay_worker,
|
||||
(str(candidate_path), str(step), str(worker), maximum_candidates),
|
||||
worker,
|
||||
timeout_seconds,
|
||||
)
|
||||
if outcome == "completed":
|
||||
result = read_json(worker)
|
||||
worker.unlink(missing_ok=True)
|
||||
return result
|
||||
step.unlink(missing_ok=True)
|
||||
return {
|
||||
"status": "search_timeout" if outcome == "timeout" else "search_failed",
|
||||
"reason": "search_worker_timeout" if outcome == "timeout" else "search_worker_failed",
|
||||
}
|
||||
|
||||
|
||||
def _public_branch_summary(result: dict[str, Any], choices: list[dict[str, Any]], comparison: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
summary = {
|
||||
"status": result.get("status"),
|
||||
"feature_id": result.get("feature_id"),
|
||||
"source_location": result.get("source_location"),
|
||||
"reason": result.get("reason"),
|
||||
"resolution_status": result.get("resolution_status"),
|
||||
"choice_path": choices,
|
||||
}
|
||||
if comparison is not None:
|
||||
summary["comparison"] = {
|
||||
"decision": comparison.get("decision"),
|
||||
"strict_passed": _strict_passed(comparison),
|
||||
}
|
||||
return {key: value for key, value in summary.items() if value is not None}
|
||||
|
||||
|
||||
def _run_selector_search(
|
||||
sample: Sample,
|
||||
directory: Path,
|
||||
provenance_candidate: dict[str, Any],
|
||||
geometry_candidate: dict[str, Any],
|
||||
gold_step: Path,
|
||||
*,
|
||||
rebuild_timeout_seconds: float,
|
||||
comparison_timeout_seconds: float,
|
||||
maximum_branches: int,
|
||||
maximum_candidates: int,
|
||||
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
||||
"""Explore selector alternatives, accepting only a unique strict winner.
|
||||
|
||||
Each branch is re-executed from an untouched CDSL document. We therefore
|
||||
never share mutable OCC topology between alternatives and cannot quietly
|
||||
inherit the result of a previous candidate.
|
||||
"""
|
||||
if maximum_branches <= 0 or maximum_candidates <= 0:
|
||||
return {"status": "disabled"}, None
|
||||
search_dir = directory / "selector-search"
|
||||
search_dir.mkdir(parents=True, exist_ok=True)
|
||||
pending: list[tuple[dict[str, Any], list[dict[str, Any]]]] = [(deepcopy(geometry_candidate), [])]
|
||||
seen: set[str] = set()
|
||||
terminals: list[tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]], dict[str, Any] | None]] = []
|
||||
branch_summaries: list[dict[str, Any]] = []
|
||||
budget_exhausted = False
|
||||
candidates_truncated = False
|
||||
branch_index = 0
|
||||
while pending:
|
||||
if branch_index >= maximum_branches:
|
||||
budget_exhausted = True
|
||||
break
|
||||
candidate, choices = pending.pop(0)
|
||||
fingerprint = json.dumps(candidate, sort_keys=True, separators=(",", ":"))
|
||||
if fingerprint in seen:
|
||||
continue
|
||||
seen.add(fingerprint)
|
||||
branch_index += 1
|
||||
branch_name = f"branch-{branch_index:03d}"
|
||||
candidate_path = search_dir / f"{branch_name}.candidate.cdsl.json"
|
||||
step_path = search_dir / f"{branch_name}.rebuild.step"
|
||||
write_json(candidate_path, candidate)
|
||||
outcome = _run_search_replay(
|
||||
candidate_path,
|
||||
step_path,
|
||||
timeout_seconds=rebuild_timeout_seconds,
|
||||
maximum_candidates=maximum_candidates,
|
||||
)
|
||||
_write_rebuild_artifacts(search_dir, branch_name, outcome)
|
||||
if outcome.get("status") == "branchable_selector":
|
||||
candidates = list(outcome.get("candidates") or [])
|
||||
total = int(outcome.get("candidate_count") or len(candidates))
|
||||
if total > len(candidates):
|
||||
candidates_truncated = True
|
||||
branch_summaries.append(_public_branch_summary(outcome, choices))
|
||||
for candidate_record in candidates:
|
||||
child = deepcopy(outcome["bound_cdsl"])
|
||||
feature_id = str(outcome["feature_id"])
|
||||
location = str(outcome["source_location"])
|
||||
source_feature = next(item for item in child["features"] if item.get("id") == feature_id)
|
||||
source_selector = dict(_searchable_selector_sites(source_feature)[
|
||||
next(index for index, (name, _selector) in enumerate(_searchable_selector_sites(source_feature)) if name == location)
|
||||
][1])
|
||||
forced = _forced_candidate_selector(source_selector, candidate_record)
|
||||
_replace_selector_site(child, feature_id, location, forced)
|
||||
pending.append((child, [*choices, {
|
||||
"feature_id": feature_id,
|
||||
"source_location": location,
|
||||
"record_id": candidate_record.get("record_id"),
|
||||
"score": candidate_record.get("score"),
|
||||
}]))
|
||||
continue
|
||||
comparison = None
|
||||
if outcome.get("status") == "rebuilt":
|
||||
comparison, comparison_error = _run_comparison(
|
||||
gold_step,
|
||||
step_path,
|
||||
search_dir / f"{branch_name}.comparison.json",
|
||||
timeout_seconds=comparison_timeout_seconds,
|
||||
)
|
||||
if comparison is not None:
|
||||
write_json(search_dir / f"{branch_name}.comparison.json", comparison)
|
||||
elif comparison_error is not None:
|
||||
outcome["comparison_error"] = comparison_error
|
||||
terminals.append((outcome, candidate, choices, comparison))
|
||||
branch_summaries.append(_public_branch_summary(outcome, choices, comparison))
|
||||
if pending:
|
||||
budget_exhausted = True
|
||||
strict_winners = [item for item in terminals if _strict_passed(item[3])]
|
||||
search_complete = not budget_exhausted and not candidates_truncated
|
||||
report: dict[str, Any] = {
|
||||
"status": "unique_strict" if search_complete and len(strict_winners) == 1 else "no_unique_strict",
|
||||
"branches_executed": branch_index,
|
||||
"maximum_branches": maximum_branches,
|
||||
"maximum_candidates_per_selector": maximum_candidates,
|
||||
"search_complete": search_complete,
|
||||
"budget_exhausted": budget_exhausted,
|
||||
"candidates_truncated": candidates_truncated,
|
||||
"strict_winner_count": len(strict_winners),
|
||||
"branches": branch_summaries,
|
||||
}
|
||||
if not search_complete:
|
||||
report["reason"] = "search_budget_incomplete"
|
||||
return report, None
|
||||
if not strict_winners:
|
||||
report["reason"] = "no_strict_branch"
|
||||
return report, None
|
||||
if len(strict_winners) > 1:
|
||||
report["reason"] = "multiple_strict_branches"
|
||||
report["strict_choice_paths"] = [item[2] for item in strict_winners]
|
||||
return report, None
|
||||
winner, _candidate, choices, comparison = strict_winners[0]
|
||||
report["strict_choice_path"] = choices
|
||||
selector_record = _emit_strict_selector_record(
|
||||
sample,
|
||||
search_dir,
|
||||
provenance_candidate,
|
||||
winner,
|
||||
comparison,
|
||||
gold_step,
|
||||
rebuild_timeout_seconds=rebuild_timeout_seconds,
|
||||
comparison_timeout_seconds=comparison_timeout_seconds,
|
||||
)
|
||||
report["selector_record"] = selector_record
|
||||
return report, selector_record if selector_record.get("status") == "strict_replayed" else None
|
||||
|
||||
|
||||
def run_geometry_probe(
|
||||
sample: Sample,
|
||||
output: Path,
|
||||
*,
|
||||
rebuild_timeout_seconds: float = 30.0,
|
||||
comparison_timeout_seconds: float = 60.0,
|
||||
search_maximum_branches: int = 32,
|
||||
search_maximum_candidates: int = 8,
|
||||
) -> dict[str, Any]:
|
||||
"""Compare normal provenance replay with an explicitly heuristic replay.
|
||||
|
||||
The geometry replay reuses only geometry/stable-ID fields already emitted as
|
||||
diagnostic context. It never invents a selector, writes a new source
|
||||
parameter, or changes the production candidate.
|
||||
"""
|
||||
featurescript = Path(sample.files["featurescript"])
|
||||
gold_step = Path(sample.files["step"])
|
||||
if not featurescript.is_file() or not gold_step.is_file():
|
||||
raise FileNotFoundError(f"{sample.sample_id}: the FeatureScript and source STEP are required")
|
||||
directory = output / "samples" / sample.sample_id
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
result = lower_model(parse_featurescript(featurescript.read_text(encoding="utf-8"), sample.sample_id), {})
|
||||
if not isinstance(result.cdsl, dict):
|
||||
raise ValueError(f"{sample.sample_id}: lowering produced no CDSL candidate")
|
||||
|
||||
provenance_candidate = result.cdsl
|
||||
geometry_candidate = deepcopy(provenance_candidate)
|
||||
removed_intents = strip_provenance_intents(geometry_candidate)
|
||||
write_json(directory / "provenance.candidate.cdsl.json", provenance_candidate)
|
||||
write_json(directory / "geometry_probe.candidate.cdsl.json", geometry_candidate)
|
||||
|
||||
provenance = _run_rebuild(
|
||||
directory / "provenance.candidate.cdsl.json",
|
||||
directory / "provenance.rebuild.step",
|
||||
timeout_seconds=rebuild_timeout_seconds,
|
||||
)
|
||||
geometry_probe = _run_rebuild(
|
||||
directory / "geometry_probe.candidate.cdsl.json",
|
||||
directory / "geometry_probe.rebuild.step",
|
||||
timeout_seconds=rebuild_timeout_seconds,
|
||||
)
|
||||
_write_rebuild_artifacts(directory, "provenance", provenance)
|
||||
_write_rebuild_artifacts(directory, "geometry_probe", geometry_probe)
|
||||
|
||||
comparison = None
|
||||
comparison_error = None
|
||||
if geometry_probe.get("status") == "rebuilt":
|
||||
comparison, comparison_error = _run_comparison(
|
||||
gold_step,
|
||||
directory / "geometry_probe.rebuild.step",
|
||||
directory / "geometry_probe.comparison.json",
|
||||
timeout_seconds=comparison_timeout_seconds,
|
||||
)
|
||||
if comparison is not None:
|
||||
write_json(directory / "geometry_probe.comparison.json", comparison)
|
||||
|
||||
selector_record = _emit_strict_selector_record(
|
||||
sample,
|
||||
directory,
|
||||
provenance_candidate,
|
||||
geometry_probe,
|
||||
comparison,
|
||||
gold_step,
|
||||
rebuild_timeout_seconds=rebuild_timeout_seconds,
|
||||
comparison_timeout_seconds=comparison_timeout_seconds,
|
||||
)
|
||||
selector_search = {"status": "not_needed"}
|
||||
if not _strict_passed(comparison):
|
||||
selector_search, searched_record = _run_selector_search(
|
||||
sample,
|
||||
directory,
|
||||
provenance_candidate,
|
||||
geometry_candidate,
|
||||
gold_step,
|
||||
rebuild_timeout_seconds=rebuild_timeout_seconds,
|
||||
comparison_timeout_seconds=comparison_timeout_seconds,
|
||||
maximum_branches=search_maximum_branches,
|
||||
maximum_candidates=search_maximum_candidates,
|
||||
)
|
||||
if searched_record is not None:
|
||||
selector_record = searched_record
|
||||
|
||||
report = {
|
||||
"schema": "cadfs_to_cdsl.selector_candidate_geometry_probe.v1",
|
||||
"sample_id": sample.sample_id,
|
||||
"source": {"featurescript": str(featurescript), "step": str(gold_step)},
|
||||
"strategy": {
|
||||
"name": "geometry_diagnostic_context",
|
||||
"removed_provenance_intent_fields": removed_intents,
|
||||
"classification": "heuristic_only",
|
||||
"limitations": [
|
||||
"The source dataset contains only a final STEP, not per-feature source checkpoints.",
|
||||
"A final strict or RP pass does not prove that the FeatureScript selector is semantically correct.",
|
||||
"This probe must not be used by the production resolver or counted as provenance-selector coverage.",
|
||||
],
|
||||
},
|
||||
"provenance": _outcome_summary(provenance, None),
|
||||
"geometry_probe": _outcome_summary(geometry_probe, comparison),
|
||||
"selector_record": selector_record,
|
||||
"selector_search": selector_search,
|
||||
}
|
||||
if comparison_error is not None:
|
||||
report["geometry_probe"]["comparison_error"] = comparison_error
|
||||
write_json(directory / "report.json", report)
|
||||
return report
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Run an offline geometry-only selector candidate probe and strict record replay")
|
||||
parser.add_argument("--input", type=Path, default=Path("data/cadfs-sample/CADFS_test"))
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--sample-id", action="append", required=True)
|
||||
parser.add_argument("--rebuild-timeout-seconds", type=float, default=30.0)
|
||||
parser.add_argument("--comparison-timeout-seconds", type=float, default=60.0)
|
||||
parser.add_argument("--search-maximum-branches", type=int, default=32)
|
||||
parser.add_argument("--search-maximum-candidates", type=int, default=8)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _arguments()
|
||||
samples = {sample.sample_id: sample for sample in scan_dataset(args.input, include_hashes=False)}
|
||||
missing = sorted(set(args.sample_id) - samples.keys())
|
||||
if missing:
|
||||
raise ValueError("unknown sample ids: " + ", ".join(missing))
|
||||
if args.rebuild_timeout_seconds <= 0 or args.comparison_timeout_seconds <= 0:
|
||||
raise ValueError("candidate probe timeouts must be positive")
|
||||
if args.search_maximum_branches < 0 or args.search_maximum_candidates < 0:
|
||||
raise ValueError("search budgets cannot be negative")
|
||||
reports = [
|
||||
run_geometry_probe(
|
||||
samples[sample_id],
|
||||
args.output,
|
||||
rebuild_timeout_seconds=args.rebuild_timeout_seconds,
|
||||
comparison_timeout_seconds=args.comparison_timeout_seconds,
|
||||
search_maximum_branches=args.search_maximum_branches,
|
||||
search_maximum_candidates=args.search_maximum_candidates,
|
||||
)
|
||||
for sample_id in args.sample_id
|
||||
]
|
||||
records: list[dict[str, Any]] = []
|
||||
for report in reports:
|
||||
record = report.get("selector_record") or {}
|
||||
record_path = record.get("record_path") if isinstance(record, dict) else None
|
||||
if isinstance(record_path, str) and Path(record_path).is_file():
|
||||
payload = read_json(Path(record_path))
|
||||
records.extend(item for item in payload.get("records") or [] if isinstance(item, dict))
|
||||
write_manifest(args.output / "selector-records.jsonl", records)
|
||||
write_json(args.output / "summary.json", {
|
||||
"schema": "cadfs_to_cdsl.selector_candidate_geometry_probe_summary.v2",
|
||||
"sample_count": len(reports),
|
||||
"strict_selector_record_count": len(records),
|
||||
"reports": reports,
|
||||
})
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -10,7 +10,7 @@ from cadfs_to_cdsl.rebuild import rebuild_candidate
|
||||
|
||||
|
||||
class IntegrationTests(unittest.TestCase):
|
||||
def test_known_rp_roundtrip_00000173(self):
|
||||
def test_known_rp_source_preserves_prefix_when_selector_query_is_deferred(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0000/00000173.txt"; gold = root / "step_abc/0000/00000173.step"
|
||||
if not feature.exists() or not gold.exists(): self.skipTest("CADFS sample is not installed")
|
||||
@@ -18,11 +18,9 @@ class IntegrationTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rebuilt = Path(tmp) / "rebuild.step"
|
||||
result = rebuild_candidate(cdsl, rebuilt)
|
||||
self.assertEqual(result["status"], "rebuilt")
|
||||
comparison = compare_steps(gold, rebuilt)
|
||||
self.assertFalse(comparison["strict"]["passed"])
|
||||
self.assertTrue(comparison["rp"]["passed"])
|
||||
self.assertEqual(comparison["decision"], "approximate_pass")
|
||||
self.assertEqual(result["status"], "rebuild_failed")
|
||||
self.assertEqual(result["error"]["message"], "f_F2: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(result["last_executable_prefix"]["last_feature_id"], "f_F1")
|
||||
|
||||
def test_standard_tapped_through_counterbore_00002243(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -43,9 +41,30 @@ class IntegrationTests(unittest.TestCase):
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rebuilt = Path(tmp) / "rebuild.step"
|
||||
self.assertEqual(rebuild_candidate(result.cdsl, rebuilt)["status"], "rebuilt")
|
||||
comparison = compare_steps(root / "step_abc/0000/00002243.step", rebuilt)
|
||||
self.assertTrue(comparison["strict"]["passed"])
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F4: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F3")
|
||||
|
||||
def test_direct_sketch_vertex_holes_preserve_scoped_body_00406667(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0040/00406667.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
candidate = lower_model(parse_featurescript(feature.read_text(), "00406667"), {}).cdsl
|
||||
holes = [item for item in candidate["features"] if item["atomic_id"] == "hole_wizard"]
|
||||
self.assertEqual([item["id"] for item in holes], ["f_F3", "f_F5"])
|
||||
self.assertEqual([item["params"]["scope_feature_id"] for item in holes], ["f_F1", "f_F1"])
|
||||
self.assertEqual([item["params"]["positions"] for item in holes], [
|
||||
[{"mm": [0.0, 0.0, 0.0]}], [{"mm": [0.0, 26.69, 0.0]}],
|
||||
])
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
self.assertEqual(
|
||||
[item["feature_id"] for item in outcome["result"]["feature_results"]],
|
||||
["f_F1", "f_F3", "f_F5"],
|
||||
)
|
||||
|
||||
def test_rectilinear_fillet_arcs_00129362_preserve_recovered_sketch_geometry(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -75,9 +94,15 @@ class IntegrationTests(unittest.TestCase):
|
||||
fillet = next(item for item in result.cdsl["features"] if item["id"] == "f_F3")
|
||||
self.assertEqual(fillet["atomic_id"], "fillet")
|
||||
self.assertEqual(fillet["selectors"][0]["kind"], "face")
|
||||
self.assertNotIn("geometry", fillet["selectors"][0])
|
||||
self.assertEqual(fillet["selectors"][0]["selector_intent"]["query_family"], "SWEPT_FACE")
|
||||
self.assertEqual(
|
||||
fillet["selectors"][0]["geometry"],
|
||||
{"axis_origin_mm": [0.0, 0.0, 0.0], "axis_direction": [0.0, -1.0, 0.0], "radius_mm": 45.66},
|
||||
fillet["selectors"][0]["selector_intent"]["source_entity"],
|
||||
{"sketch_id": "F0", "entity_id": "E0"},
|
||||
)
|
||||
self.assertEqual(
|
||||
fillet["selectors"][0]["selector_intent"]["derivation_policy"],
|
||||
{"allowed": ["boundary"], "multiplicity": "one"},
|
||||
)
|
||||
|
||||
chamfer = next(item for item in result.cdsl["features"] if item["id"] == "f_F5")
|
||||
@@ -101,18 +126,183 @@ class IntegrationTests(unittest.TestCase):
|
||||
self.assertNotIn("result_mode", next(item for item in result.cdsl["features"] if item["id"] == "f_F7")["params"])
|
||||
self.assertEqual(next(item for item in result.cdsl["features"] if item["id"] == "f_F9")["params"]["result_mode"], "new_body")
|
||||
|
||||
from build123d import import_step
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rebuilt = Path(tmp) / "rebuild.step"
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
runtime_box = outcome["result"]["bbox_mm"]
|
||||
imported_box = import_step(str(rebuilt)).bounding_box()
|
||||
actual = [imported_box.min.X, imported_box.min.Y, imported_box.min.Z, imported_box.max.X, imported_box.max.Y, imported_box.max.Z]
|
||||
expected = runtime_box["min"] + runtime_box["max"]
|
||||
for value, target in zip(actual, expected): self.assertAlmostEqual(value, target, places=5)
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F5: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F4")
|
||||
|
||||
def test_inward_shells_00789939_lower_and_rebuild(self):
|
||||
def test_swept_face_extent_continues_through_primary_cuts_00925274(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0092/00925274.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
candidate = lower_model(parse_featurescript(feature.read_text(), "00925274"), {}).cdsl
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
|
||||
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
result = outcome["result"]
|
||||
self.assertEqual(
|
||||
[item["feature_id"] for item in result["feature_results"]],
|
||||
["f_F1", "f_F3", "f_F4", "f_F6", "f_F7", "f_F9", "f_F10", "f_F12", "f_F13", "f_F15", "f_F17", "f_F18_plane", "f_F18"],
|
||||
)
|
||||
f9_resolution = next(
|
||||
item for item in result["selector_resolution"] if item["feature_id"] == "f_F9"
|
||||
)
|
||||
self.assertEqual(f9_resolution["status"], "resolved")
|
||||
self.assertEqual(f9_resolution["resolution_mode"], "kernel_lineage")
|
||||
self.assertEqual(f9_resolution["evidence"]["result_records"], ["body:f_F6:face:0"])
|
||||
self.assertEqual(
|
||||
[item["feature_id"] for item in f9_resolution["evidence"]["relations"]],
|
||||
["f_F1", "f_F3", "f_F6"],
|
||||
)
|
||||
|
||||
def test_direct_hole_prism_swept_edge_resolves_00007264(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0000/00007264.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
result = lower_model(parse_featurescript(feature.read_text(), "00007264"), {})
|
||||
fillet = next(item for item in result.cdsl["features"] if item["id"] == "f_F2")
|
||||
self.assertEqual(fillet["atomic_id"], "fillet")
|
||||
self.assertTrue(all(
|
||||
selector["selector_intent"]["query_family"] == "SWEPT_EDGE"
|
||||
for selector in fillet["selectors"]
|
||||
))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
outcome = rebuild_candidate(result.cdsl, Path(tmp) / "rebuild.step")
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
binding = next(item for item in outcome["selector_binding"] if item["feature_id"] == "f_F2")
|
||||
self.assertEqual(len(binding["resolved"]), 4)
|
||||
vertical_edges = [
|
||||
relation
|
||||
for delta in outcome["result"]["topology_deltas"]
|
||||
if not any(str(snapshot_id).startswith("transient:") for snapshot_id in delta["output_snapshot_ids"])
|
||||
for relation in delta["relations"]
|
||||
if relation["source_kind"] == "vertex"
|
||||
and relation["result_kind"] == "edge"
|
||||
and relation["source_record_ids"]
|
||||
]
|
||||
self.assertEqual(
|
||||
len({(tuple(item["source_record_ids"]), tuple(item["result_record_ids"])) for item in vertical_edges}),
|
||||
4,
|
||||
)
|
||||
self.assertTrue(all(
|
||||
relation["coverage"] == "complete" and relation["lineage_status"] == "proven"
|
||||
for relation in vertical_edges
|
||||
))
|
||||
|
||||
def test_annular_swept_face_selector_00974931(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0097/00974931.txt"
|
||||
gold = root / "step_abc/0097/00974931.step"
|
||||
if not feature.exists() or not gold.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
candidate = lower_model(parse_featurescript(feature.read_text(), "00974931"), {}).cdsl
|
||||
fillet = next(item for item in candidate["features"] if item["id"] == "f_F2")
|
||||
selector = fillet["selectors"][0]
|
||||
self.assertEqual(fillet["atomic_id"], "fillet")
|
||||
self.assertNotIn("geometry", selector)
|
||||
self.assertNotIn("stable_id", selector)
|
||||
self.assertEqual(selector["selector_intent"]["query_family"], "SWEPT_FACE")
|
||||
self.assertEqual(
|
||||
selector["selector_intent"]["source_entity"],
|
||||
{"sketch_id": "F0", "entity_id": "E0"},
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rebuilt = Path(tmp) / "rebuild.step"
|
||||
outcome = rebuild_candidate(candidate, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
|
||||
f2_resolution = next(
|
||||
item for item in outcome["last_executable_prefix"]["result"]["selector_resolution"]
|
||||
if item["feature_id"] == "f_F2"
|
||||
)
|
||||
self.assertEqual(f2_resolution["status"], "resolved")
|
||||
self.assertEqual(f2_resolution["resolution_mode"], "kernel_lineage")
|
||||
self.assertEqual(f2_resolution["evidence"]["semantic_anchor"], {
|
||||
"type": "source_entity", "sketch_id": "F0", "entity_id": "E0",
|
||||
})
|
||||
self.assertEqual(
|
||||
[item["feature_id"] for item in outcome["last_executable_prefix"]["result"]["feature_results"]],
|
||||
["f_F1", "f_F2"],
|
||||
)
|
||||
self.assertEqual(outcome["error"]["message"], "f_F3: selector_query_unsupported during incremental replay")
|
||||
|
||||
def test_direct_prism_cap_edge_lineage_00021014(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0002/00021014.txt"
|
||||
gold = root / "step_abc/0002/00021014.step"
|
||||
if not feature.exists() or not gold.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
candidate = lower_model(parse_featurescript(feature.read_text(), "00021014"), {}).cdsl
|
||||
fillet = next(item for item in candidate["features"] if item["id"] == "f_F2")
|
||||
cap_edges = [
|
||||
selector for selector in fillet["selectors"]
|
||||
if (selector.get("selector_intent") or {}).get("query_family") == "CAP_EDGE"
|
||||
]
|
||||
self.assertEqual(len(cap_edges), 9)
|
||||
self.assertTrue(all("stable_id" not in selector and "geometry" not in selector for selector in cap_edges))
|
||||
self.assertEqual(
|
||||
{selector["selector_intent"]["lineage_role"] for selector in cap_edges},
|
||||
{"extrude.start", "extrude.end"},
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rebuilt = Path(tmp) / "rebuild.step"
|
||||
outcome = rebuild_candidate(candidate, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
comparison = compare_steps(gold, rebuilt)
|
||||
|
||||
cap_resolutions = [
|
||||
item for item in outcome["result"]["selector_resolution"]
|
||||
if item.get("feature_id") == "f_F2"
|
||||
and item.get("evidence", {}).get("semantic_anchor", {}).get("type") == "source_entity"
|
||||
]
|
||||
self.assertEqual(len(cap_resolutions), 10)
|
||||
direct_caps = [
|
||||
item for item in cap_resolutions
|
||||
if item["evidence"]["relations"][0].get("output_role") in {"extrude.start", "extrude.end"}
|
||||
]
|
||||
self.assertEqual(len(direct_caps), 9)
|
||||
self.assertTrue(all(item["resolution_mode"] == "kernel_lineage" for item in direct_caps))
|
||||
self.assertTrue(comparison["strict"]["passed"])
|
||||
|
||||
def test_direct_mixed_hole_cap_edge_lineage_00735367(self):
|
||||
"""A solver-split circular hole restores one proven source cap edge."""
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0073/00735367.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
candidate = lower_model(parse_featurescript(feature.read_text(), "00735367"), {}).cdsl
|
||||
fillet = next(item for item in candidate["features"] if item["id"] == "f_F2")
|
||||
selector = fillet["selectors"][0]
|
||||
self.assertEqual(selector["selector_intent"]["query_family"], "CAP_EDGE")
|
||||
self.assertEqual(selector["selector_intent"]["source_entity"], {"sketch_id": "F0", "entity_id": "E1"})
|
||||
self.assertEqual(selector["selector_intent"]["lineage_role"], "extrude.end")
|
||||
self.assertNotIn("geometry", selector)
|
||||
self.assertNotIn("stable_id", selector)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
|
||||
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F4: selector_query_unsupported during incremental replay")
|
||||
prefix = outcome["last_executable_prefix"]["result"]
|
||||
self.assertEqual([item["feature_id"] for item in prefix["feature_results"]], ["f_F1", "f_F2", "f_F3"])
|
||||
resolution = next(item for item in prefix["selector_resolution"] if item["feature_id"] == "f_F2")
|
||||
self.assertEqual(resolution["status"], "resolved")
|
||||
self.assertEqual(resolution["resolution_mode"], "kernel_lineage")
|
||||
self.assertEqual(resolution["evidence"]["semantic_anchor"], {
|
||||
"type": "source_entity", "sketch_id": "F0", "entity_id": "E1",
|
||||
})
|
||||
self.assertEqual(resolution["evidence"]["relations"][0]["output_role"], "extrude.end")
|
||||
self.assertTrue(resolution["evidence"]["relations"][0]["source_record_ids"])
|
||||
|
||||
def test_inward_shells_00789939_preserve_prefix_when_face_query_is_deferred(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0078/00789939.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
@@ -128,8 +318,10 @@ class IntegrationTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rebuilt = Path(tmp) / "rebuild.step"
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
self.assertEqual(outcome["result"]["solid_count"], 2)
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F6: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F5")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["result"]["solid_count"], 2)
|
||||
|
||||
def test_outward_cap_shells_lower_and_rebuild_prefixes(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -155,7 +347,10 @@ class IntegrationTests(unittest.TestCase):
|
||||
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)
|
||||
self.assertEqual(
|
||||
[item["feature_id"] for item in outcome["result"]["feature_results"]],
|
||||
["f_F1", "f_F2"],
|
||||
)
|
||||
|
||||
def test_sweep_00542223_preserves_its_open_bspline_path(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -173,6 +368,42 @@ class IntegrationTests(unittest.TestCase):
|
||||
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_direct_sketch_wire_qbodytype_path_lowers_and_rebuilds_00896761(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0089/00896761.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
candidate = lower_model(parse_featurescript(feature.read_text(), "00896761"), {}).cdsl
|
||||
sweep = next(item for item in candidate["features"] if item["id"] == "f_F2")
|
||||
segment = sweep["params"]["path"]["segment"]
|
||||
self.assertEqual(sweep["atomic_id"], "sweep_add")
|
||||
self.assertEqual(segment["type"], "bspline")
|
||||
self.assertEqual(segment["source_entity_id"], "E2")
|
||||
self.assertEqual(segment["points"], [[0.0, 0.0], [87.21, 282.41]])
|
||||
self.assertEqual(segment["start_tangent"], [0.0, 936.12])
|
||||
self.assertEqual(segment["end_tangent"], [168.9, 48.72])
|
||||
|
||||
from engine.cdsl_engine.runtime import analyze_cdsl
|
||||
self.assertTrue(analyze_cdsl(candidate).runtime_eligible)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
self.assertEqual([item["feature_id"] for item in outcome["result"]["feature_results"]], ["f_F2"])
|
||||
|
||||
def test_multi_entity_sketch_wire_qbodytype_path_remains_deferred_00786708(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0078/00786708.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
result = lower_model(parse_featurescript(feature.read_text(), "00786708"), {})
|
||||
self.assertIn({
|
||||
"code": "unsupported_engine_capability", "feature_id": "F2", "operation": "sweep", "capability": "sweep_path_query",
|
||||
"message": "current CDSL sweep requires one direct sketch line or B-spline path",
|
||||
}, result.diagnostics)
|
||||
self.assertNotIn("f_F2", {item["id"] for item in result.cdsl["features"]})
|
||||
later_sweep = next(item for item in result.cdsl["features"] if item["id"] == "f_F5")
|
||||
self.assertEqual(later_sweep["params"]["path"]["segment"]["source_entity_id"], "E5")
|
||||
|
||||
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"
|
||||
@@ -193,7 +424,7 @@ class IntegrationTests(unittest.TestCase):
|
||||
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):
|
||||
def test_fused_body_circular_copy_faces_preserve_shell_prefix_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")
|
||||
@@ -202,53 +433,20 @@ class IntegrationTests(unittest.TestCase):
|
||||
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))
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F7: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F6")
|
||||
|
||||
def test_face_chamfer_ignores_periodic_seams_00111611(self):
|
||||
def test_face_chamfer_source_query_is_not_geometry_bound_00111611(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0011/00111611.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
from copy import deepcopy
|
||||
from engine.cdsl_engine.capabilities import CapabilityAnalyzer, sketch_ids_required_by_contract
|
||||
from engine.cdsl_engine.runtime import EXECUTORS, ExecutionSession, _execute_node, _selector_edges
|
||||
from engine.cdsl_engine.sketch_solver import CORE_SHAPE_GENERATORS, resolve_required_sketches
|
||||
from cadfs_to_cdsl.selector_binding import bind_candidate_selectors
|
||||
|
||||
candidate = lower_model(parse_featurescript(feature.read_text(), "00111611"), {}).cdsl
|
||||
bound, _ = bind_candidate_selectors(candidate)
|
||||
resolved = resolve_required_sketches(
|
||||
deepcopy(bound), sketch_ids_required_by_contract(bound), errors={},
|
||||
)
|
||||
analysis = CapabilityAnalyzer(
|
||||
atomic_ids=EXECUTORS, profile_types=CORE_SHAPE_GENERATORS,
|
||||
).analyze(resolved)
|
||||
session = ExecutionSession(
|
||||
sketches={str(item["id"]): item for item in resolved["geometry"]["sketches"]},
|
||||
nodes={node.feature_id: node for node in analysis.plan},
|
||||
)
|
||||
|
||||
for node in analysis.plan:
|
||||
if node.feature_id == "f_F5":
|
||||
edges = _selector_edges(node, session, tangent_propagation=True)
|
||||
break
|
||||
_execute_node(node, session)
|
||||
else:
|
||||
self.fail("F5 chamfer was not planned")
|
||||
|
||||
self.assertEqual(len(edges), 8)
|
||||
self.assertTrue(all(str(edge.geom_type).endswith("CIRCLE") for edge in edges))
|
||||
with self.assertRaisesRegex(ValueError, "f_F5: selector_query_unsupported after prefix rebuild"):
|
||||
bind_candidate_selectors(candidate)
|
||||
|
||||
def test_circular_remove_pattern_replays_cut_sources_00159804(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
|
||||
@@ -6,7 +6,12 @@ from pathlib import Path
|
||||
from cadfs_to_cdsl.compare import compare_steps
|
||||
from cadfs_to_cdsl.featurescript_parser import parse_featurescript
|
||||
from cadfs_to_cdsl.ir import Call
|
||||
from cadfs_to_cdsl.lowering import _arc, _contours, _global, _number, _record_single_body_successor, lower_model
|
||||
from cadfs_to_cdsl.lowering import (
|
||||
UnsupportedCapability, _arc, _contours, _curve_endpoint_tangent,
|
||||
_direct_hole_location, _direct_line_angle_axis, _direct_sketch_wire_path,
|
||||
_direct_boolean_intersection_selector, _direct_primary_cut_intersection_selector, _global, _number,
|
||||
_record_single_body_successor, lower_model,
|
||||
)
|
||||
from cadfs_to_cdsl.pipeline import compare_one, convert_one, rebuild_one
|
||||
from cadfs_to_cdsl.rebuild import rebuild_candidate
|
||||
from cadfs_to_cdsl.dataset import Sample
|
||||
@@ -14,7 +19,92 @@ from cadfs_to_cdsl.dataset import scan_dataset
|
||||
from cadfs_to_cdsl.tests.test_parser import SOURCE, TRANSFORM_SOURCE
|
||||
|
||||
|
||||
LINE_ANGLE_TWO_ENTITY_SOURCE = '''FeatureScript 1511;
|
||||
import(path : "onshape/std/geometry.fs", version : "1511.0");
|
||||
const mm = millimeter;
|
||||
const VERTEX = EntityType.VERTEX;
|
||||
const EDGE = EntityType.EDGE;
|
||||
const FACE = EntityType.FACE;
|
||||
function v(x, y){return vector(x, y);}
|
||||
annotation { "Feature Type Name" : "Feature" }
|
||||
export const myFeature = defineFeature(function(context is Context, id is Id, definition is map)
|
||||
precondition{}
|
||||
{
|
||||
{ var Q0; Q0=qCreatedBy(makeId("Top.planeOp"),FACE); var sketch=newSketch(context,id+"F0",{"sketchPlane":qUnion([Q0])}); skLineSegment(sketch,"E0",{"start":v(0,0)*mm,"end":v(10,0)*mm}); skLineSegment(sketch,"E1",{"start":v(0,0)*mm,"end":v(0,10)*mm}); skLineSegment(sketch,"E2",{"start":v(0,5)*mm,"end":v(10,5)*mm}); skPoint(sketch,"P0",{"position":v(0,10)*mm}); skPoint(sketch,"P1",{"position":v(10,0)*mm}); skSolve(sketch); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",EDGE,"E0"); var Q1; Q1=sQuery(id+"F0.wireOp",EDGE,"E1"); cPlane(context,id+"F1",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":0*degree}); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",EDGE,"E0"); var Q1; Q1=sQuery(id+"F0.wireOp",EDGE,"E2"); cPlane(context,id+"F2",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":0*degree}); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",EDGE,"E0"); var Q1; Q1=sQuery(id+"F0.wireOp",VERTEX,"P0"); cPlane(context,id+"F3",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":0*degree}); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",VERTEX,"P0"); var Q1; Q1=sQuery(id+"F0.wireOp",EDGE,"E0"); cPlane(context,id+"F4",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":90*degree,"oppositeDirection":true}); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",EDGE,"E0"); var Q1; Q1=sQuery(id+"F0.wireOp",VERTEX,"P1"); cPlane(context,id+"F5",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":0*degree}); }
|
||||
{ var Q0; Q0=sQuery(id+"F0.wireOp",EDGE,"E0"); var Q1; Q1=qCreatedBy(id+"F1.planeOp",FACE); cPlane(context,id+"F6",{"entities":qUnion([Q0,Q1]),"cplaneType":CPlaneType.LINE_ANGLE,"angle":0*degree}); }
|
||||
}'''
|
||||
|
||||
|
||||
class LoweringTests(unittest.TestCase):
|
||||
def test_direct_sketch_wire_path_accepts_only_one_nonconstruction_line_or_bspline(self):
|
||||
plane = {"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "normal": [0.0, 0.0, 1.0]}
|
||||
line = {"type": "line", "start": [0.0, 0.0], "end": [10.0, 0.0], "source_entity_id": "E0"}
|
||||
query = Call("qUnion", [[
|
||||
Call("qConstructionFilter", [
|
||||
Call("qBodyType", [
|
||||
Call("qCreatedBy", [Call("__binary__", ["id", "+", "F0"]), "EDGE"]),
|
||||
"BodyType.WIRE",
|
||||
]),
|
||||
"ConstructionObject.NO",
|
||||
]),
|
||||
]])
|
||||
sketches = {"F0": {"workplane": plane}}
|
||||
entities = {"F0": {"E0": line, "axis": {"type": "line", "start": [0.0, 0.0], "end": [0.0, 5.0], "construction": True}}}
|
||||
|
||||
self.assertEqual(
|
||||
_direct_sketch_wire_path(
|
||||
query, sketches, entities, featurescript_version="1511",
|
||||
standard_library="onshape/std/geometry.fs", standard_library_version="1511.0",
|
||||
),
|
||||
("F0", "E0", line, sketches["F0"]),
|
||||
)
|
||||
self.assertIsNone(_direct_sketch_wire_path(
|
||||
query, sketches, entities, featurescript_version="1512",
|
||||
standard_library="onshape/std/geometry.fs", standard_library_version="1511.0",
|
||||
))
|
||||
self.assertIsNone(_direct_sketch_wire_path(
|
||||
query, sketches, entities, featurescript_version="1511",
|
||||
standard_library="onshape/std/geometry.fs", standard_library_version="1512.0",
|
||||
))
|
||||
self.assertIsNone(_direct_sketch_wire_path(
|
||||
query, sketches, {"F0": {"E0": line, "E1": {**line, "source_entity_id": "E1"}}},
|
||||
featurescript_version="1511", standard_library="onshape/std/geometry.fs", standard_library_version="1511.0",
|
||||
))
|
||||
self.assertIsNone(_direct_sketch_wire_path(
|
||||
Call("qBodyType", [Call("qCreatedBy", ["F0", "EDGE"]), "BodyType.WIRE"]),
|
||||
sketches, entities, featurescript_version="1511",
|
||||
standard_library="onshape/std/geometry.fs", standard_library_version="1511.0",
|
||||
))
|
||||
self.assertIsNone(_direct_sketch_wire_path(
|
||||
query.args[0][0], sketches, entities, featurescript_version="1511",
|
||||
standard_library="onshape/std/geometry.fs", standard_library_version="1511.0",
|
||||
))
|
||||
|
||||
def test_direct_hole_location_accepts_only_original_sketch_vertices(self):
|
||||
plane = {"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "normal": [0.0, 0.0, 1.0]}
|
||||
sketches = {"F2": {"workplane": plane}}
|
||||
entities = {"F2": {
|
||||
"P0": {"type": "point", "point": [1.0, 2.0]},
|
||||
"C0": {"type": "circle", "center": [3.0, 4.0], "radius_mm": 1.0},
|
||||
"L0": {"type": "line", "start": [5.0, 6.0], "end": [7.0, 8.0]},
|
||||
"A0": {"type": "arc", "start": [9.0, 10.0], "end": [11.0, 12.0]},
|
||||
}}
|
||||
|
||||
def vertex(token: str) -> Call:
|
||||
return Call("sQuery", ["F2.wireOp", "VERTEX", token])
|
||||
|
||||
self.assertEqual(_direct_hole_location(vertex("P0"), sketches, entities), ([1.0, 2.0, 0.0], plane))
|
||||
self.assertEqual(_direct_hole_location(vertex("C0.center"), sketches, entities), ([3.0, 4.0, 0.0], plane))
|
||||
self.assertEqual(_direct_hole_location(vertex("L0.end"), sketches, entities), ([7.0, 8.0, 0.0], plane))
|
||||
self.assertEqual(_direct_hole_location(vertex("A0.start"), sketches, entities), ([9.0, 10.0, 0.0], plane))
|
||||
self.assertIsNone(_direct_hole_location(Call("qAdjacent", [vertex("C0.center")]), sketches, entities))
|
||||
self.assertIsNone(_direct_hole_location(vertex("C0.center.copy"), sketches, entities))
|
||||
|
||||
def test_strict_comparison_status_is_not_labeled_approximate(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
output = Path(tmp); directory = output / "samples" / "strict-status"; directory.mkdir(parents=True)
|
||||
@@ -53,7 +143,7 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(arc["center"][1], 0.5)
|
||||
self.assertAlmostEqual(arc["radius_mm"], math.hypot(2.5, 0.5))
|
||||
|
||||
def test_direct_extrude_and_two_section_loft_swept_edges_bind_for_fillet(self):
|
||||
def test_direct_extrude_lineage_succeeds_and_unregistered_loft_query_preserves_prefix(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
cases = {
|
||||
"00000715": ("f_F2", 2, True),
|
||||
@@ -67,14 +157,37 @@ class LoweringTests(unittest.TestCase):
|
||||
fillet = next(item for item in result.cdsl["features"] if item["id"] == fillet_id)
|
||||
self.assertEqual(len(fillet["selectors"]), selector_count)
|
||||
self.assertTrue(all(item["owner_feature_id"] == fillet["depends_on"][0] for item in fillet["selectors"]))
|
||||
self.assertTrue(all("bbox_mm" in item["geometry"] for item in fillet["selectors"]))
|
||||
self.assertEqual(["curve_type" in item["geometry"] for item in fillet["selectors"]], [line_type] * selector_count)
|
||||
if sample_id == "00000715":
|
||||
self.assertTrue(all("geometry" not in item for item in fillet["selectors"]))
|
||||
self.assertTrue(all(
|
||||
item["selector_intent"]["query_family"] == "SWEPT_EDGE"
|
||||
and item["selector_intent"]["derivation_policy"] == {"allowed": ["boundary"], "multiplicity": "one"}
|
||||
and len(item["selector_intent"]["source_entities"]) == 2
|
||||
for item in fillet["selectors"]
|
||||
))
|
||||
else:
|
||||
self.assertTrue(all("bbox_mm" in item["geometry"] for item in fillet["selectors"]))
|
||||
self.assertEqual(["curve_type" in item["geometry"] for item in fillet["selectors"]], [line_type] * selector_count)
|
||||
self.assertTrue(all(
|
||||
item["selector_intent"]["query_family"] == "SWEPT_EDGE"
|
||||
and item["selector_intent"]["evidence"] == "feature_script_query"
|
||||
and item["selector_intent"]["derivation_policy"] == {"allowed": ["continuation"], "multiplicity": "none"}
|
||||
and item["selector_intent"]["source_query"]["featurescript_version"] == "1793"
|
||||
for item in fillet["selectors"]
|
||||
))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
rebuilt = Path(directory) / "rebuild.step"
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
comparison = compare_steps(root / "step_abc" / sample_id[:4] / f"{sample_id}.step", rebuilt)
|
||||
self.assertTrue(comparison["strict"]["passed"])
|
||||
if sample_id == "00005267":
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F4: selector_query_unsupported during incremental replay")
|
||||
self.assertTrue(rebuilt.exists())
|
||||
self.assertEqual(outcome["last_executable_prefix"]["failed_feature_id"], "f_F4")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F3")
|
||||
else:
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
comparison = compare_steps(root / "step_abc" / sample_id[:4] / f"{sample_id}.step", rebuilt)
|
||||
self.assertTrue(comparison["strict"]["passed"])
|
||||
|
||||
def test_swept_edge_requires_one_direct_shared_source_endpoint(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -93,7 +206,7 @@ class LoweringTests(unittest.TestCase):
|
||||
"message": "swept edge source endpoint provenance is unsupported",
|
||||
}])
|
||||
|
||||
def test_direct_full_revolve_swept_circle_edges_bind_for_dressups(self):
|
||||
def test_full_revolve_swept_circle_edges_remain_deferred_with_prefixes(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
for sample_id, dressup_id, selector_count in (
|
||||
("00048326", "f_F3", 2),
|
||||
@@ -109,14 +222,17 @@ class LoweringTests(unittest.TestCase):
|
||||
selector["geometry"].get("curve_type") == "circle"
|
||||
and len(selector["geometry"].get("circle_center_mm") or []) == 3
|
||||
and selector["geometry"].get("radius_mm", 0) > 0
|
||||
and selector["selector_intent"]["query_family"] == "SWEPT_EDGE"
|
||||
and selector["selector_intent"]["evidence"] == "feature_script_query"
|
||||
and selector["selector_intent"]["derivation_policy"]["multiplicity"] == "none"
|
||||
for selector in dressup["selectors"]
|
||||
))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
rebuilt = Path(directory) / "rebuild.step"
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
comparison = compare_steps(root / "step_abc" / sample_id[:4] / f"{sample_id}.step", rebuilt)
|
||||
self.assertTrue(comparison["strict"]["passed"])
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], f"{dressup_id}: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["failed_feature_id"], dressup_id)
|
||||
|
||||
def test_full_revolve_swept_circle_requires_one_direct_shared_source_endpoint(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -135,7 +251,7 @@ class LoweringTests(unittest.TestCase):
|
||||
"message": "swept edge source endpoint provenance is unsupported",
|
||||
}, result.diagnostics)
|
||||
|
||||
def test_equivalent_imprint_profile_retains_direct_full_revolve_swept_edges(self):
|
||||
def test_equivalent_imprint_profile_preserves_but_does_not_bind_full_revolve_queries(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
source = root / "featurescript_rp/0011/00112257.txt"
|
||||
result = lower_model(parse_featurescript(source.read_text(), "00112257"), {})
|
||||
@@ -144,17 +260,16 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertEqual(len(fillet["selectors"]), 5)
|
||||
self.assertTrue(all(
|
||||
selector["geometry"].get("curve_type") == "circle"
|
||||
and selector["selector_intent"]["query_family"] == "SWEPT_EDGE"
|
||||
and selector["selector_intent"]["derivation_policy"]["multiplicity"] == "none"
|
||||
for selector in fillet["selectors"]
|
||||
))
|
||||
|
||||
from cadfs_to_cdsl.selector_binding import bind_candidate_selectors
|
||||
prefix = dict(result.cdsl)
|
||||
prefix["features"] = result.cdsl["features"][:2]
|
||||
bound, evidence = bind_candidate_selectors(prefix)
|
||||
selectors = next(item for item in bound["features"] if item["id"] == "f_F2")["selectors"]
|
||||
self.assertEqual(len(evidence), 1)
|
||||
self.assertEqual(len(selectors), 5)
|
||||
self.assertTrue(all(selector.get("snapshot_id") for selector in selectors))
|
||||
with self.assertRaisesRegex(ValueError, "f_F2: selector_query_unsupported after prefix rebuild"):
|
||||
bind_candidate_selectors(prefix)
|
||||
|
||||
def test_changed_imprint_profile_does_not_inherit_full_revolve_source_edges(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -689,6 +804,30 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertEqual(end["offset_mm"], 3.0)
|
||||
self.assertEqual(end["reference"]["kind"], "body")
|
||||
self.assertEqual(end["reference"]["owner_feature_id"], "f_F1")
|
||||
self.assertEqual(end["reference"]["source"], "runtime_snapshot")
|
||||
self.assertNotIn("stable_id", end["reference"])
|
||||
self.assertEqual(end["reference"]["selector_intent"]["query_family"], "SWEPT_BODY")
|
||||
self.assertEqual(end["reference"]["selector_intent"]["evidence"], "active_body_member")
|
||||
self.assertEqual(
|
||||
end["reference"]["selector_intent"]["derivation_policy"],
|
||||
{"allowed": ["boundary"], "multiplicity": "one"},
|
||||
)
|
||||
fillet = next(item for item in result.cdsl["features"] if item["id"] == "f_F4")
|
||||
self.assertEqual(len(fillet["selectors"]), 1)
|
||||
self.assertEqual(fillet["selectors"][0]["selector_intent"]["query_family"], "CAP_EDGE")
|
||||
self.assertEqual(
|
||||
fillet["selectors"][0]["selector_intent"]["derivation_policy"],
|
||||
{"allowed": ["boundary", "continuation"], "multiplicity": "one"},
|
||||
)
|
||||
from engine.cdsl_engine.semantic_validation import validate_semantic_cdsl
|
||||
|
||||
mixed_evidence = deepcopy(result.cdsl)
|
||||
mixed_reference = next(
|
||||
item for item in mixed_evidence["features"] if item["id"] == "f_F3"
|
||||
)["params"]["end_condition"]["reference"]
|
||||
mixed_reference["stable_id"] = "body:f_F1"
|
||||
with self.assertRaisesRegex(ValueError, "invalid SWEPT_BODY member contract"):
|
||||
validate_semantic_cdsl(mixed_evidence)
|
||||
|
||||
def test_up_to_surface_extrude_captures_the_cap_face(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -700,9 +839,24 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertEqual(end["type"], "up_to_surface")
|
||||
self.assertEqual(end["reference"]["kind"], "face")
|
||||
self.assertEqual(end["reference"]["owner_feature_id"], "f_F1")
|
||||
self.assertEqual(end["reference"]["geometry"]["plane_offset_mm"], 0.0)
|
||||
self.assertEqual(end["reference"]["output_role"], "extrude.start")
|
||||
self.assertEqual(end["reference"]["source"], "runtime_snapshot")
|
||||
self.assertNotIn("geometry", end["reference"])
|
||||
self.assertEqual(end["reference"]["selector_intent"]["query_family"], "CAP_FACE")
|
||||
self.assertEqual(end["reference"]["selector_intent"]["evidence"], "operation_role")
|
||||
swept = next(item for item in result.cdsl["features"] if item["id"] == "f_F6")
|
||||
self.assertEqual(swept["params"]["end_condition"]["reference"]["geometry"]["radius_mm"], 24.3)
|
||||
swept_reference = swept["params"]["end_condition"]["reference"]
|
||||
self.assertEqual(swept_reference["selector_intent"]["query_family"], "SWEPT_FACE")
|
||||
self.assertEqual(
|
||||
swept_reference["selector_intent"]["derivation_policy"],
|
||||
{"allowed": ["boundary", "continuation"], "multiplicity": "one"},
|
||||
)
|
||||
self.assertEqual(swept_reference["selector_intent"]["evidence"], "kernel_history")
|
||||
self.assertEqual(swept_reference["selector_intent"]["source_entity"], {"sketch_id": "F0", "entity_id": "E0"})
|
||||
self.assertEqual(swept_reference["source"], "runtime_snapshot")
|
||||
self.assertNotIn("stable_id", swept_reference)
|
||||
self.assertNotIn("binding_feature_id", swept_reference)
|
||||
self.assertNotIn("geometry", swept_reference)
|
||||
mirror = next(item for item in result.cdsl["features"] if item["id"] == "f_F18")
|
||||
self.assertTrue(mirror["params"]["mirror_current_body"])
|
||||
|
||||
@@ -756,6 +910,160 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(plane["origin_mm"][0], -12.15)
|
||||
self.assertAlmostEqual(plane["origin_mm"][1], -24.3 * math.sqrt(3) / 2)
|
||||
|
||||
def test_line_angle_direct_circle_uses_the_featurescript_circle_axis(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0003/00030209.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
result = lower_model(parse_featurescript(feature.read_text(), "00030209"), {})
|
||||
self.assertEqual(result.status, "converted_partial")
|
||||
planes = {
|
||||
item["id"]: item["params"]["plane"]
|
||||
for item in result.cdsl["features"]
|
||||
if item["atomic_id"] == "reference_plane"
|
||||
}
|
||||
expected_centers = {
|
||||
"f_F1": [45.78, 0.0, 26.2],
|
||||
"f_F2": [13.9, 0.0, 26.5],
|
||||
"f_F4": [-17.08, 0.0, 23.46],
|
||||
"f_F5": [-44.4, 0.0, 19.8],
|
||||
}
|
||||
self.assertEqual(set(planes), set(expected_centers))
|
||||
for feature_id, center in expected_centers.items():
|
||||
with self.subTest(feature_id=feature_id):
|
||||
plane = planes[feature_id]
|
||||
self.assertEqual(plane["origin_mm"], center)
|
||||
self.assertEqual(plane["x_dir"], [0.0, -1.0, 0.0])
|
||||
self.assertAlmostEqual(plane["normal"][0], 1.0)
|
||||
self.assertAlmostEqual(plane["normal"][1], 0.0)
|
||||
self.assertAlmostEqual(plane["normal"][2], 0.0)
|
||||
self.assertFalse(any(
|
||||
diagnostic.get("operation") == "cPlane"
|
||||
for diagnostic in result.diagnostics
|
||||
))
|
||||
|
||||
opposite_source = feature.read_text().replace(
|
||||
'"angle" : 90 * degree, "width"',
|
||||
'"angle" : 90 * degree, "oppositeDirection" : true, "width"',
|
||||
1,
|
||||
)
|
||||
opposite = lower_model(parse_featurescript(opposite_source, "line-angle-circle-opposite"), {})
|
||||
opposite_plane = next(
|
||||
item for item in opposite.cdsl["features"] if item["id"] == "f_F1"
|
||||
)["params"]["plane"]
|
||||
self.assertAlmostEqual(opposite_plane["normal"][0], -1.0)
|
||||
self.assertAlmostEqual(opposite_plane["normal"][1], 0.0)
|
||||
self.assertAlmostEqual(opposite_plane["normal"][2], 0.0)
|
||||
|
||||
def test_line_angle_two_entities_follow_axis_plane_order_and_swap_rules(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
cases = {
|
||||
# F8 selects Right plane first, then the direct F7 line. The
|
||||
# standard library must swap them before calculating the frame.
|
||||
"00298026": ("f_F8", [0.0, -1.0, 0.0], [0.43837114678907757, 0.0, -0.898794046299167]),
|
||||
# F1 supplies the direct line first and the default plane second.
|
||||
"00586156": ("f_F1", [0.0, 0.0, 1.0], [-math.sqrt(0.5), -math.sqrt(0.5), 0.0]),
|
||||
}
|
||||
for sample_id, (feature_id, expected_x, expected_normal) in cases.items():
|
||||
with self.subTest(sample_id=sample_id):
|
||||
source = root / "featurescript_rp" / sample_id[:4] / f"{sample_id}.txt"
|
||||
result = lower_model(parse_featurescript(source.read_text(), sample_id), {})
|
||||
plane = next(item for item in result.cdsl["features"] if item["id"] == feature_id)["params"]["plane"]
|
||||
self.assertEqual(plane["x_dir"], expected_x)
|
||||
for actual, expected in zip(plane["normal"], expected_normal):
|
||||
self.assertAlmostEqual(actual, expected)
|
||||
self.assertFalse(any(
|
||||
diagnostic.get("operation") == "cPlane"
|
||||
and diagnostic.get("feature_id") == feature_id.removeprefix("f_")
|
||||
for diagnostic in result.diagnostics
|
||||
))
|
||||
|
||||
def test_line_angle_two_direct_entities_cover_axis_point_and_degenerate_contracts(self):
|
||||
result = lower_model(parse_featurescript(LINE_ANGLE_TWO_ENTITY_SOURCE, "line-angle-two-entities"), {})
|
||||
self.assertEqual(result.status, "converted_partial")
|
||||
planes = {
|
||||
item["id"]: item["params"]["plane"]
|
||||
for item in result.cdsl["features"]
|
||||
if item["atomic_id"] == "reference_plane"
|
||||
}
|
||||
# Nonparallel axes, parallel offset axes, and an axis plus a direct
|
||||
# point each use the exact source direction, rather than a sketch
|
||||
# workplane frame.
|
||||
for feature_id in ("f_F1", "f_F2", "f_F3"):
|
||||
self.assertEqual(planes[feature_id]["x_dir"], [1.0, 0.0, 0.0])
|
||||
self.assertEqual(planes[feature_id]["normal"], [0.0, 0.0, 1.0])
|
||||
# A point selected before its axis triggers FeatureScript's swap. The
|
||||
# signed angle also follows oppositeDirection.
|
||||
self.assertEqual(planes["f_F4"]["x_dir"], [1.0, 0.0, 0.0])
|
||||
self.assertAlmostEqual(planes["f_F4"]["normal"][0], 0.0)
|
||||
self.assertAlmostEqual(planes["f_F4"]["normal"][1], 1.0)
|
||||
self.assertAlmostEqual(planes["f_F4"]["normal"][2], 0.0)
|
||||
# A cPlane output is represented as qCreatedBy(...planeOp, FACE),
|
||||
# not a sketch source. It is accepted only because F1 is an existing
|
||||
# reference-plane frame, and it supplies the true zero-angle side.
|
||||
self.assertEqual(planes["f_F6"]["x_dir"], [1.0, 0.0, 0.0])
|
||||
self.assertEqual(planes["f_F6"]["normal"], [0.0, 0.0, -1.0])
|
||||
self.assertIn({
|
||||
"code": "feature_deferred",
|
||||
"feature_id": "F5",
|
||||
"operation": "cPlane",
|
||||
"message": "line-angle reference selection is degenerate",
|
||||
}, result.diagnostics)
|
||||
|
||||
def test_line_angle_direct_axis_refuses_a_topology_derived_query(self):
|
||||
direct = Call("sQuery", ["F0.wireOp", "EDGE", "E0"], 1)
|
||||
derived = Call("makeQuery", [
|
||||
"F1.opExtrude", "CAP_EDGE", "EDGE",
|
||||
{"derivedFrom": direct, "isStart": False},
|
||||
], 2)
|
||||
sketch_by_source = {"F0": {"workplane": {
|
||||
"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "normal": [0.0, 0.0, 1.0],
|
||||
}}}
|
||||
entity_by_sketch = {"F0": {"E0": {
|
||||
"type": "line", "start": [0.0, 0.0], "end": [10.0, 0.0], "source_entity_id": "E0",
|
||||
}}}
|
||||
self.assertIsNotNone(_direct_line_angle_axis(direct, sketch_by_source, entity_by_sketch))
|
||||
self.assertIsNone(_direct_line_angle_axis(derived, sketch_by_source, entity_by_sketch))
|
||||
|
||||
def test_line_angle_direct_axis_refuses_a_query_wrapper_and_keeps_the_prefix(self):
|
||||
direct = Call("sQuery", ["F0.wireOp", "EDGE", "E0"], 1)
|
||||
wrapped = Call("qAdjacent", [direct, "VERTEX", "EDGE"], 2)
|
||||
sketch_by_source = {"F0": {"workplane": {
|
||||
"origin_mm": [0.0, 0.0, 0.0], "x_dir": [1.0, 0.0, 0.0], "normal": [0.0, 0.0, 1.0],
|
||||
}}}
|
||||
entity_by_sketch = {"F0": {"E0": {
|
||||
"type": "line", "start": [0.0, 0.0], "end": [10.0, 0.0], "source_entity_id": "E0",
|
||||
}}}
|
||||
self.assertIsNone(_direct_line_angle_axis(wrapped, sketch_by_source, entity_by_sketch))
|
||||
|
||||
model = parse_featurescript(LINE_ANGLE_TWO_ENTITY_SOURCE, "line-angle-wrapped-axis")
|
||||
plane = next(item for item in model.features if item.feature_id == "F1")
|
||||
plane.params["entities"].args[0][0] = wrapped
|
||||
result = lower_model(model, {})
|
||||
self.assertIn({
|
||||
"code": "feature_deferred",
|
||||
"feature_id": "F1",
|
||||
"operation": "cPlane",
|
||||
"message": "line-angle reference selection is unsupported",
|
||||
}, result.diagnostics)
|
||||
self.assertIn("f_F2", {item["id"] for item in result.cdsl["features"]})
|
||||
|
||||
def test_line_angle_rejects_a_circle_center_vertex_as_an_axis(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0003/00030209.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
source = feature.read_text().replace(
|
||||
'Q0=sQuery(id+"F0.wireOp",EDGE,"E0");',
|
||||
'Q0=sQuery(id+"F0.wireOp",VERTEX,"E0.center");',
|
||||
1,
|
||||
)
|
||||
result = lower_model(parse_featurescript(source, "line-angle-circle-center"), {})
|
||||
self.assertIn({
|
||||
"code": "feature_deferred",
|
||||
"feature_id": "F1",
|
||||
"operation": "cPlane",
|
||||
"message": "line-angle reference selection is unsupported",
|
||||
}, result.diagnostics)
|
||||
|
||||
def test_open_nonconstruction_geometry_is_preserved_as_reference(self):
|
||||
source = SOURCE.replace('skSolve(sketch);', 'skLineSegment(sketch, "open", {"start":v(0, 0) * mm, "end":v(20, 0) * mm}); skSolve(sketch);', 1)
|
||||
result = lower_model(parse_featurescript(source, "open-profile"), {})
|
||||
@@ -931,7 +1239,7 @@ class LoweringTests(unittest.TestCase):
|
||||
[("outer", 25.5), ("inner", 19.0)],
|
||||
)
|
||||
|
||||
def test_mixed_extrude_rebuilds_surface_limited_chamfer_history(self):
|
||||
def test_mixed_extrude_preserves_prefix_when_cap_edge_chamfer_query_is_deferred(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0071/00710855.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
@@ -941,10 +1249,10 @@ class LoweringTests(unittest.TestCase):
|
||||
from cadfs_to_cdsl.rebuild import rebuild_candidate
|
||||
rebuilt = rebuild_candidate(result.cdsl, Path(tmp) / "rebuilt.step")
|
||||
|
||||
self.assertEqual(rebuilt["status"], "rebuilt")
|
||||
chamfer = next(item for item in rebuilt["result"]["feature_results"] if item["feature_id"] == "f_F10")
|
||||
self.assertIn("chamfer_surface_limited", [item["code"] for item in chamfer["diagnostics"]])
|
||||
self.assertAlmostEqual(rebuilt["result"]["volume_mm3"], 72577.33528261917, places=6)
|
||||
self.assertEqual(rebuilt["status"], "rebuild_failed")
|
||||
self.assertEqual(rebuilt["error"]["message"], "f_F10: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(rebuilt["last_executable_prefix"]["failed_feature_id"], "f_F10")
|
||||
self.assertEqual(rebuilt["last_executable_prefix"]["last_feature_id"], "f_F9")
|
||||
|
||||
def test_fit_spline_loft_lowers_to_executable_loft_add(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -1048,7 +1356,7 @@ class LoweringTests(unittest.TestCase):
|
||||
"kind": "face", "owner_feature_id": "f_F3", "output_role": "extrude.start",
|
||||
"source": "runtime_snapshot", "confidence": 1.0,
|
||||
})
|
||||
self.assertEqual(selector["selector_intent_version"], "1.0")
|
||||
self.assertNotIn("selector_intent_version", selector)
|
||||
self.assertEqual(selector["selector_intent"]["query_family"], "CAP_FACE")
|
||||
|
||||
from engine.cdsl_engine.runtime import rebuild_cdsl
|
||||
@@ -1130,21 +1438,19 @@ class LoweringTests(unittest.TestCase):
|
||||
})
|
||||
|
||||
# F9 rotates F4 only after F5/F7 have consumed it. It cannot be baked
|
||||
# into the earlier F4 sketch; the unsupported body continuation stays
|
||||
# explicit and F1--F7 remains an executable prefix.
|
||||
# into the earlier F4 sketch. The CAP_EDGE geometry is preserved for
|
||||
# diagnostics, but no source-qualified cap-edge lineage exists for
|
||||
# this two-sided profile contract.
|
||||
prefix = deepcopy(result.cdsl)
|
||||
prefix["features"] = [
|
||||
item for item in prefix["features"]
|
||||
if item["id"] in {"f_F1", "f_F2", "f_F4", "f_F5", "f_F7"}
|
||||
]
|
||||
from engine.cdsl_engine.runtime import rebuild_cdsl
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
rebuilt = rebuild_cdsl(prefix, Path(directory) / "two-sided-cap-prefix.step")
|
||||
self.assertFalse(rebuilt["runtime_diagnostics"])
|
||||
self.assertEqual(
|
||||
[item["feature_id"] for item in rebuilt["feature_results"]],
|
||||
["f_F1", "f_F2", "f_F4", "f_F5", "f_F7"],
|
||||
)
|
||||
rebuilt = rebuild_candidate(prefix, Path(directory) / "two-sided-cap-prefix.step")
|
||||
self.assertEqual(rebuilt["status"], "rebuild_failed")
|
||||
self.assertEqual(rebuilt["error"]["message"], "f_F7: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(rebuilt["last_executable_prefix"]["last_feature_id"], "f_F5")
|
||||
|
||||
from engine.cdsl_engine.runtime import analyze_cdsl
|
||||
f9 = next(item for item in analyze_cdsl(result.cdsl).as_dict()["feature_results"] if item["feature_id"] == "f_F9")
|
||||
@@ -1338,6 +1644,41 @@ class LoweringTests(unittest.TestCase):
|
||||
["E14", "E14"],
|
||||
)
|
||||
|
||||
def test_direct_imprint_profile_preserves_selector_intent_and_f1_checkpoint(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0035/00354246.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
result = lower_model(parse_featurescript(feature.read_text(), "00354246"), {})
|
||||
self.assertEqual(result.status, "converted_complete")
|
||||
features = {item["id"]: item for item in result.cdsl["features"]}
|
||||
sketches = {item["id"]: item for item in result.cdsl["geometry"]["sketches"]}
|
||||
profile = sketches[features["f_F1"]["sketch_id"]]["profile"]
|
||||
self.assertEqual(profile["type"], "planar_imprint")
|
||||
self.assertEqual(profile["selections"], [
|
||||
{"source_entity_id": "E6.MirrorCS", "face_side": -1.0},
|
||||
{"source_entity_id": "E0.MirrorCS", "face_side": 1.0},
|
||||
])
|
||||
|
||||
selectors = features["f_F2"]["selectors"]
|
||||
self.assertEqual(len(selectors), 7)
|
||||
self.assertTrue(all(
|
||||
selector["selector_intent"]["query_family"] == "SWEPT_FACE"
|
||||
and selector["selector_intent"]["derivation_policy"] == {
|
||||
"allowed": ["continuation"], "multiplicity": "none",
|
||||
}
|
||||
for selector in selectors
|
||||
))
|
||||
self.assertTrue(any("geometry" not in selector for selector in selectors))
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
rebuilt = Path(directory) / "rebuild.step"
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F2: selector_query_unsupported during incremental replay")
|
||||
self.assertTrue(rebuilt.exists())
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F1")
|
||||
|
||||
def test_direct_translation_transform_updates_the_source_revolve(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0011/00111611.txt"
|
||||
@@ -1512,6 +1853,121 @@ class LoweringTests(unittest.TestCase):
|
||||
diagnostics = [item for item in result.diagnostics if item.get("operation") == "cPlane"]
|
||||
self.assertFalse(diagnostics)
|
||||
|
||||
def test_curve_point_plane_uses_an_explicit_bspline_endpoint_tangent(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0082/00827798.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
result = lower_model(parse_featurescript(feature.read_text(), "00827798"), {})
|
||||
plane = next(item for item in result.cdsl["features"] if item["id"] == "f_F1")
|
||||
self.assertEqual(plane["atomic_id"], "reference_plane")
|
||||
self.assertFalse(any(
|
||||
item.get("operation") == "cPlane" and item.get("feature_id") == "F1"
|
||||
for item in result.diagnostics
|
||||
))
|
||||
expected = [107.86, 0.0, -176.04]
|
||||
magnitude = math.sqrt(sum(value * value for value in expected))
|
||||
for actual, value in zip(plane["params"]["plane"]["normal"], expected):
|
||||
self.assertAlmostEqual(actual, value / magnitude)
|
||||
|
||||
second_feature = root / "featurescript_rp/0084/00845891.txt"
|
||||
if second_feature.exists():
|
||||
second = lower_model(parse_featurescript(second_feature.read_text(), "00845891"), {})
|
||||
self.assertEqual(
|
||||
next(item for item in second.cdsl["features"] if item["id"] == "f_F3")["atomic_id"],
|
||||
"reference_plane",
|
||||
)
|
||||
self.assertFalse(any(
|
||||
item.get("operation") == "cPlane" and item.get("feature_id") == "F3"
|
||||
for item in second.diagnostics
|
||||
))
|
||||
|
||||
without_end_derivative = feature.read_text().replace(
|
||||
', "endDerivative": vector(107.86, -176.04) * mm', "", 1,
|
||||
)
|
||||
rejected = lower_model(parse_featurescript(without_end_derivative, "00827798-no-end-derivative"), {})
|
||||
self.assertIn({
|
||||
"code": "unsupported_engine_capability",
|
||||
"capability": "reference_plane:curve_point",
|
||||
"feature_id": "F1",
|
||||
"operation": "cPlane",
|
||||
"message": "CURVE_POINT B-spline endpoint has no exported tangent",
|
||||
}, rejected.diagnostics)
|
||||
|
||||
def test_curve_endpoint_tangent_uses_arc_orientation_without_a_chord_fallback(self):
|
||||
arc = {
|
||||
"type": "arc", "start": [1.0, 0.0], "end": [0.0, 1.0],
|
||||
"center": [0.0, 0.0], "clockwise": False,
|
||||
}
|
||||
self.assertEqual(_curve_endpoint_tangent(arc, "start"), [0.0, 1.0])
|
||||
self.assertEqual(_curve_endpoint_tangent(arc, "end"), [-1.0, 0.0])
|
||||
with self.assertRaisesRegex(UnsupportedCapability, "direct source-curve endpoint"):
|
||||
_curve_endpoint_tangent(arc, "internal")
|
||||
|
||||
def test_curve_point_plane_uses_exact_bspline_interpolation_vertex_tangents(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
cases = {
|
||||
"00456498": ("f_F1", "F1", [141.23, 0.0, -13.99]),
|
||||
"00812557": ("f_F2", "F2", [0.0, 191.15, -139.74]),
|
||||
}
|
||||
for sample_id, (cdsl_feature_id, source_feature_id, expected) in cases.items():
|
||||
with self.subTest(sample_id=sample_id):
|
||||
source = root / "featurescript_rp" / sample_id[:4] / f"{sample_id}.txt"
|
||||
if not source.exists():
|
||||
self.skipTest("CADFS sample is not installed")
|
||||
result = lower_model(parse_featurescript(source.read_text(), sample_id), {})
|
||||
plane = next(item for item in result.cdsl["features"] if item["id"] == cdsl_feature_id)
|
||||
self.assertEqual(plane["atomic_id"], "reference_plane")
|
||||
magnitude = math.sqrt(sum(value * value for value in expected))
|
||||
for actual, value in zip(plane["params"]["plane"]["normal"], expected):
|
||||
self.assertAlmostEqual(actual, value / magnitude, places=8)
|
||||
self.assertFalse(any(
|
||||
item.get("operation") == "cPlane" and item.get("feature_id") == source_feature_id
|
||||
for item in result.diagnostics
|
||||
))
|
||||
|
||||
no_derivatives = (root / "featurescript_rp/0045/00456498.txt").read_text().replace(
|
||||
', "startDerivative": vector(0, 64.52) * mm, "endDerivative": vector(141.23, -13.99) * mm', "", 1,
|
||||
)
|
||||
rejected = lower_model(parse_featurescript(no_derivatives, "00456498-no-derivatives"), {})
|
||||
self.assertIn({
|
||||
"code": "unsupported_engine_capability",
|
||||
"capability": "reference_plane:curve_point",
|
||||
"feature_id": "F1",
|
||||
"operation": "cPlane",
|
||||
"message": "CURVE_POINT B-spline interpolation point has no complete source interpolation data",
|
||||
}, rejected.diagnostics)
|
||||
|
||||
derived = (root / "featurescript_rp/0045/00456498.txt").read_text().replace(
|
||||
'"E0.2.internal"', '"E0.2.endSnap0"', 1,
|
||||
)
|
||||
rejected_derived = lower_model(parse_featurescript(derived, "00456498-derived-vertex"), {})
|
||||
self.assertIn({
|
||||
"code": "unsupported_engine_capability",
|
||||
"capability": "reference_plane:curve_point",
|
||||
"feature_id": "F1",
|
||||
"operation": "cPlane",
|
||||
"message": "CURVE_POINT point must name a direct source-curve endpoint or B-spline interpolation vertex",
|
||||
}, rejected_derived.diagnostics)
|
||||
|
||||
def test_curve_point_plane_uses_a_direct_arc_endpoint_tangent(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0005/00057273.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
source = feature.read_text().replace(
|
||||
'EDGE,"E1"', 'EDGE,"E0"', 1,
|
||||
).replace(
|
||||
'VERTEX,"E1.end"', 'VERTEX,"E0.end"', 1,
|
||||
)
|
||||
result = lower_model(parse_featurescript(source, "00057273-arc-curve-point"), {})
|
||||
plane = next(item for item in result.cdsl["features"] if item["id"] == "f_F1")
|
||||
self.assertEqual(plane["atomic_id"], "reference_plane")
|
||||
self.assertEqual(plane["params"]["plane"]["origin_mm"], [-24.6, 0.0, 0.0])
|
||||
self.assertEqual(plane["params"]["plane"]["normal"], [0.0, -1.0, 0.0])
|
||||
self.assertFalse(any(
|
||||
item.get("operation") == "cPlane" and item.get("feature_id") == "F1"
|
||||
for item in result.diagnostics
|
||||
))
|
||||
|
||||
def test_offset_reference_plane_honors_opposite_direction_without_reversing_its_frame(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0077/00777619.txt"
|
||||
@@ -1571,7 +2027,7 @@ class LoweringTests(unittest.TestCase):
|
||||
))
|
||||
self.assertNotIn("F8", {item.get("feature_id") for item in result.diagnostics})
|
||||
|
||||
def test_shell_lowers_and_executes_direct_linear_extrude_swept_faces(self):
|
||||
def test_shell_resolves_direct_prism_swept_faces_through_kernel_lineage(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0059/00594348.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
@@ -1582,18 +2038,105 @@ class LoweringTests(unittest.TestCase):
|
||||
shell = result.cdsl["features"][shell_index]
|
||||
self.assertEqual(shell["atomic_id"], "shell")
|
||||
self.assertEqual([item["owner_feature_id"] for item in shell["selectors"]], ["f_F1", "f_F1"])
|
||||
self.assertTrue(all("normal" in item["geometry"] for item in shell["selectors"]))
|
||||
|
||||
prefix = deepcopy(result.cdsl)
|
||||
prefix["features"] = prefix["features"][:shell_index + 1]
|
||||
self.assertTrue(all("geometry" not in item and "stable_id" not in item for item in shell["selectors"]))
|
||||
self.assertTrue(all(item["selector_intent"]["query_family"] == "SWEPT_FACE" for item in shell["selectors"]))
|
||||
self.assertTrue(all(item["selector_intent"]["evidence"] == "kernel_history" for item in shell["selectors"]))
|
||||
self.assertTrue(all(
|
||||
item["selector_intent"]["derivation_policy"] == {"allowed": ["boundary"], "multiplicity": "one"}
|
||||
for item in shell["selectors"]
|
||||
))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
outcome = rebuild_candidate(prefix, Path(directory) / "swept-face-shell.step")
|
||||
outcome = rebuild_candidate(result.cdsl, Path(directory) / "swept-face-shell.step")
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
self.assertEqual(
|
||||
[item["status"] for item in outcome["result"]["feature_results"]],
|
||||
["executed", "executed"],
|
||||
resolutions = [
|
||||
item for item in outcome["result"]["selector_resolution"]
|
||||
if item["feature_id"] == "f_F2"
|
||||
]
|
||||
self.assertEqual(len(resolutions), 2)
|
||||
self.assertTrue(all(item["resolution_mode"] == "kernel_lineage" for item in resolutions))
|
||||
self.assertTrue(all(
|
||||
item["evidence"]["relations"][0]["source_kind"] == "edge"
|
||||
and item["evidence"]["relations"][0]["result_kind"] == "face"
|
||||
for item in resolutions
|
||||
))
|
||||
|
||||
def test_shell_resolves_immediate_direct_prism_cap_faces_through_output_roles(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
cases = (("00212904", "extrude.start"), ("00789939", "extrude.end"))
|
||||
for sample_id, output_role 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(encoding="utf-8"), sample_id), {})
|
||||
self.assertEqual(result.status, "converted_complete")
|
||||
shell_index = next(index for index, item in enumerate(result.cdsl["features"]) if item["id"] == "f_F2")
|
||||
shell = result.cdsl["features"][shell_index]
|
||||
self.assertEqual(shell["atomic_id"], "shell")
|
||||
self.assertEqual(len(shell["selectors"]), 1)
|
||||
selector = shell["selectors"][0]
|
||||
self.assertEqual(selector["owner_feature_id"], "f_F1")
|
||||
self.assertEqual(selector["output_role"], output_role)
|
||||
self.assertEqual(selector["selector_intent"]["query_family"], "CAP_FACE")
|
||||
self.assertEqual(selector["selector_intent"]["evidence"], "operation_role")
|
||||
self.assertNotIn("stable_id", selector)
|
||||
self.assertNotIn("binding_feature_id", selector)
|
||||
self.assertNotIn("geometry", selector)
|
||||
|
||||
prefix = deepcopy(result.cdsl)
|
||||
prefix["features"] = prefix["features"][:shell_index + 1]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
outcome = rebuild_candidate(prefix, Path(directory) / f"{sample_id}-cap-face-shell.step")
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
resolution = next(
|
||||
item for item in outcome["result"]["selector_resolution"]
|
||||
if item["feature_id"] == "f_F2"
|
||||
)
|
||||
self.assertEqual(resolution["status"], "resolved")
|
||||
self.assertEqual(resolution["resolution_mode"], "operation_role")
|
||||
self.assertEqual(resolution["selected"]["output_roles"], [output_role])
|
||||
|
||||
def test_shell_direct_prism_swept_face_rejects_unregistered_source_version(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0059/00594348.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
source = feature.read_text(encoding="utf-8").replace("FeatureScript 1511;", "FeatureScript 1793;", 1)
|
||||
result = lower_model(parse_featurescript(source, "00594348-version-1793"), {})
|
||||
shell = next(item for item in result.cdsl["features"] if item["id"] == "f_F2")
|
||||
self.assertTrue(all(
|
||||
item["selector_intent"]["evidence"] == "feature_script_query"
|
||||
and item["selector_intent"]["derivation_policy"]["multiplicity"] == "none"
|
||||
for item in shell["selectors"]
|
||||
))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
outcome = rebuild_candidate(result.cdsl, Path(directory) / "swept-face-shell-1793.step")
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F2: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F1")
|
||||
|
||||
def test_shell_direct_prism_swept_face_rejects_unregistered_standard_library_revision(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0059/00594348.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
source = feature.read_text(encoding="utf-8").replace(
|
||||
'import(path : "onshape/std/geometry.fs", version : "1511.0");',
|
||||
'import(path : "onshape/std/geometry.fs", version : "1512.0");',
|
||||
1,
|
||||
)
|
||||
self.assertFalse(outcome["result"].get("diagnostics", []))
|
||||
result = lower_model(parse_featurescript(source, "00594348-library-1512"), {})
|
||||
shell = next(item for item in result.cdsl["features"] if item["id"] == "f_F2")
|
||||
self.assertTrue(all(
|
||||
item["selector_intent"]["source_query"]["featurescript_version"] == "1511"
|
||||
and item["selector_intent"]["source_query"]["standard_library"] == "onshape/std/geometry.fs"
|
||||
and item["selector_intent"]["source_query"]["standard_library_version"] == "1512.0"
|
||||
for item in shell["selectors"]
|
||||
))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
outcome = rebuild_candidate(result.cdsl, Path(directory) / "swept-face-shell-library-1512.step")
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F2: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F1")
|
||||
|
||||
def test_shell_offset_face_requires_true_dependency_cap_evidence(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -1623,13 +2166,14 @@ class LoweringTests(unittest.TestCase):
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertTrue(rebuilt.exists())
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["type"], "RuntimeExecutionError")
|
||||
self.assertEqual(outcome["error"]["type"], "ValueError")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F5: selector_query_unsupported during incremental replay")
|
||||
self.assertIn("bound_cdsl", outcome)
|
||||
self.assertEqual(outcome["last_executable_prefix"]["failed_feature_id"], "f_F5")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F4")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["result"]["solid_count"], 1)
|
||||
|
||||
def test_pattern_copy_selector_binding_rebuilds_the_full_history(self):
|
||||
def test_pattern_copy_intersection_query_preserves_its_prefix_without_instance_geometry_binding(self):
|
||||
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")
|
||||
@@ -1639,14 +2183,15 @@ class LoweringTests(unittest.TestCase):
|
||||
rebuilt = Path(directory) / "selector-prefix.step"
|
||||
outcome = rebuild_candidate(result.cdsl, rebuilt)
|
||||
self.assertTrue(rebuilt.exists())
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
resolved = [
|
||||
item for item in outcome["result"]["selector_resolution"]
|
||||
if item["feature_id"] == "f_F7"
|
||||
and item["selector"].get("owner_feature_id") == "f_F4.c4.f_F1"
|
||||
]
|
||||
self.assertEqual(len(resolved), 2)
|
||||
self.assertTrue(all(item["status"] == "resolved" for item in resolved))
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F7: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F6")
|
||||
reference = next(item for item in result.cdsl["features"] if item["id"] == "f_F7")["params"]["end_condition"]["reference"]
|
||||
self.assertEqual(reference["selector_intent"]["query_family"], "INTERSECT")
|
||||
self.assertEqual(
|
||||
[item["selector_intent"]["query_family"] for item in reference["intersection_of"][:2]],
|
||||
["COPY", "COPY"],
|
||||
)
|
||||
|
||||
def test_shell_rejects_swept_faces_without_direct_linear_extrude_ownership(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
@@ -1698,6 +2243,10 @@ class LoweringTests(unittest.TestCase):
|
||||
|
||||
def test_cap_face_selector_retains_versioned_provenance_intent(self):
|
||||
source = SOURCE.replace(
|
||||
"FeatureScript 1511;",
|
||||
'FeatureScript 1511;\nimport(path : "onshape/std/geometry.fs", version : "1511.0");',
|
||||
1,
|
||||
).replace(
|
||||
'\n});\n',
|
||||
'\n extrude(context, id + "F2", {"entities":makeQuery(id + "F1.opExtrude", "CAP_FACE", FACE, {"isStart":false}), "depth":10 * mm});\n});\n',
|
||||
)
|
||||
@@ -1705,12 +2254,127 @@ class LoweringTests(unittest.TestCase):
|
||||
self.assertEqual(result.status, "converted_complete")
|
||||
selector = next(item for item in result.cdsl["features"] if item["id"] == "f_F2")["selectors"][0]
|
||||
self.assertEqual(selector["selector_intent"]["query_family"], "CAP_FACE")
|
||||
self.assertEqual(selector["selector_intent_version"], "1.0")
|
||||
self.assertNotIn("selector_intent_version", selector)
|
||||
self.assertEqual(selector["selector_intent"]["source_query"]["featurescript_version"], "1511")
|
||||
self.assertEqual(selector["selector_intent"]["source_query"]["standard_library"], "onshape/std/geometry.fs")
|
||||
self.assertEqual(selector["selector_intent"]["source_query"]["standard_library_version"], "1511.0")
|
||||
self.assertEqual(selector["selector_intent"]["evidence"], "operation_role")
|
||||
from engine.cdsl_engine.semantic_validation import validate_semantic_cdsl
|
||||
self.assertTrue(validate_semantic_cdsl(result.cdsl)["future_rebuild_ready"])
|
||||
|
||||
def test_direct_boolean_intersection_lowering_keeps_two_source_contract(self):
|
||||
base = lower_model(parse_featurescript(SOURCE, "intersection-intent"), {}).cdsl
|
||||
sketch = base["geometry"]["sketches"][0]
|
||||
direct = {**base["features"][0], "id": "f_F2", "name": "F2"}
|
||||
feature_by_id = {
|
||||
"f_F1": base["features"][0],
|
||||
"f_F2": direct,
|
||||
"f_F3": {
|
||||
"id": "f_F3", "atomic_id": "boolean_bodies",
|
||||
"params": {
|
||||
"operation": "subtract", "target_feature_ids": ["f_F1"],
|
||||
"tool_feature_ids": ["f_F2"], "keep_tools": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
cap = Call("makeQuery", ["F1.opExtrude", "CAP_FACE", "EntityType.FACE", {"isStart": False}])
|
||||
source_edge = Call("sQuery", ["F0.wireOp", "EntityType.EDGE", "E0"])
|
||||
swept = Call("makeQuery", [
|
||||
"F2.opExtrude", "SWEPT_FACE", "EntityType.FACE",
|
||||
{"disambiguationData": [Call("OSD", [[source_edge]])]},
|
||||
])
|
||||
query = Call("makeQuery", [
|
||||
"F3.boolean.opBoolean", "INTERSECT", "EntityType.EDGE", {"derivedFrom": [cap, swept]},
|
||||
])
|
||||
selector = _direct_boolean_intersection_selector(
|
||||
query,
|
||||
feature_by_id=feature_by_id,
|
||||
feature_frames={"F2": {"profile_source": "F0"}},
|
||||
sketch_by_source={"F0": sketch},
|
||||
sketches_by_id={sketch["id"]: sketch},
|
||||
entity_by_sketch={"F0": {"E0": {"type": "circle", "center": [0, 0], "radius_mm": 9.53}}},
|
||||
featurescript_version="1511",
|
||||
)
|
||||
|
||||
self.assertIsNotNone(selector)
|
||||
self.assertNotIn("stable_id", selector)
|
||||
intent = selector["selector_intent"]
|
||||
self.assertEqual(intent["derivation_policy"], {"allowed": ["intersection"], "multiplicity": "one"})
|
||||
self.assertEqual(intent["intersection_sources"], [
|
||||
{"query_family": "CAP_FACE", "owner_feature_id": "f_F1", "output_role": "extrude.end"},
|
||||
{"query_family": "SWEPT_FACE", "owner_feature_id": "f_F2", "source_entity": {"sketch_id": "F0", "entity_id": "E0"}},
|
||||
])
|
||||
|
||||
def test_direct_primary_cut_intersection_requires_no_unverified_disambiguation(self):
|
||||
base = lower_model(parse_featurescript(SOURCE, "primary-intersection-intent"), {}).cdsl
|
||||
target = base["features"][0]
|
||||
target_sketch = base["geometry"]["sketches"][0]
|
||||
tool_sketch = deepcopy(target_sketch)
|
||||
tool_sketch.update({"id": "sketch_F2", "source_sketch_id": "F2"})
|
||||
if tool_sketch["profile"].get("type") == "circle":
|
||||
tool_sketch["profile"]["source_entity_id"] = "E1"
|
||||
else:
|
||||
for contour in tool_sketch["profile"]["contours"]:
|
||||
for segment in contour["segments"]:
|
||||
segment["source_entity_id"] = "E1"
|
||||
tool = {
|
||||
"id": "f_F3", "atomic_id": "extrude_cut_blind", "sketch_id": "sketch_F2",
|
||||
"params": {"distance_mm": 10, "end_condition": {"type": "blind"}},
|
||||
}
|
||||
cap = Call("makeQuery", ["F1.opExtrude", "CAP_FACE", "EntityType.FACE", {"isStart": False}])
|
||||
tool_edge = Call("sQuery", ["F2.wireOp", "EntityType.EDGE", "E1"])
|
||||
swept = Call("makeQuery", [
|
||||
"F3.opExtrude", "SWEPT_FACE", "EntityType.FACE",
|
||||
{"disambiguationData": [Call("OSD", [[tool_edge]])]},
|
||||
])
|
||||
query = Call("makeQuery", [
|
||||
"F3.boolean.opBoolean", "INTERSECT", "EntityType.EDGE", {"derivedFrom": [cap, swept]},
|
||||
])
|
||||
kwargs = {
|
||||
"feature_by_id": {"f_F1": target, "f_F3": tool},
|
||||
"feature_frames": {"F3": {"profile_source": "F2"}},
|
||||
"sketch_by_source": {"F2": tool_sketch},
|
||||
"sketches_by_id": {tool_sketch["id"]: tool_sketch, target_sketch["id"]: target_sketch},
|
||||
"entity_by_sketch": {"F2": {"E1": {"type": "circle", "center": [0, 0], "radius_mm": 9.53}}},
|
||||
"previous": ["f_F1", "f_F3"],
|
||||
"featurescript_version": "1511",
|
||||
}
|
||||
selector = _direct_primary_cut_intersection_selector(query, **kwargs)
|
||||
|
||||
self.assertIsNotNone(selector)
|
||||
self.assertEqual(selector["owner_feature_id"], "f_F3")
|
||||
self.assertEqual(selector["selector_intent"]["intersection_sources"][1], {
|
||||
"query_family": "SWEPT_FACE", "owner_feature_id": "f_F3",
|
||||
"source_entity": {"sketch_id": "F2", "entity_id": "E1"},
|
||||
})
|
||||
deferred = Call("makeQuery", [
|
||||
"F3.boolean.opBoolean", "INTERSECT", "EntityType.EDGE",
|
||||
{"derivedFrom": [cap, swept], "disambiguationData": [Call("OD", [0.0])]},
|
||||
])
|
||||
self.assertIsNone(_direct_primary_cut_intersection_selector(deferred, **kwargs))
|
||||
|
||||
def test_deferred_primary_intersect_keeps_the_convertible_f3_checkpoint(self):
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0002/00020311.txt"
|
||||
if not feature.exists(): self.skipTest("CADFS sample is not installed")
|
||||
|
||||
result = lower_model(parse_featurescript(feature.read_text(), "00020311"), {})
|
||||
f4 = next(item for item in result.cdsl["features"] if item["id"] == "f_F4")
|
||||
self.assertTrue(all(
|
||||
selector["kind"] == "edge"
|
||||
and selector["selector_intent"]["query_family"] == "INTERSECT"
|
||||
and selector["selector_intent"]["derivation_policy"]["multiplicity"] == "none"
|
||||
for selector in f4["selectors"]
|
||||
))
|
||||
from engine.cdsl_engine.semantic_validation import validate_semantic_cdsl
|
||||
self.assertTrue(validate_semantic_cdsl(result.cdsl)["future_rebuild_ready"])
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
outcome = rebuild_candidate(result.cdsl, Path(directory) / "00020311.step")
|
||||
self.assertTrue((Path(directory) / "00020311.step").exists())
|
||||
self.assertEqual(outcome["status"], "rebuild_failed")
|
||||
self.assertEqual(outcome["error"]["message"], "f_F4: selector_query_unsupported during incremental replay")
|
||||
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F3")
|
||||
|
||||
def test_conversion_writes_status_and_sidecars(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp); source = root / "00000173.txt"; source.write_text(SOURCE)
|
||||
|
||||
@@ -58,10 +58,16 @@ class ParserTests(unittest.TestCase):
|
||||
def test_source_version_and_standard_library_are_retained(self):
|
||||
source = '''FeatureScript 1511;
|
||||
import(path : "onshape/std/geometry.fs", version : "1511.0");
|
||||
import(version : "1511.0", path : "onshape/std/common.fs");
|
||||
export const f = defineFeature(function(context, id, definition) {});'''
|
||||
model = parse_featurescript(source, "versioned")
|
||||
self.assertEqual(model.featurescript_version, "1511")
|
||||
self.assertEqual(model.standard_library, "onshape/std/geometry.fs")
|
||||
self.assertEqual(model.standard_library_version, "1511.0")
|
||||
self.assertEqual(model.standard_library_imports, [
|
||||
{"path": "onshape/std/geometry.fs", "version": "1511.0"},
|
||||
{"path": "onshape/std/common.fs", "version": "1511.0"},
|
||||
])
|
||||
|
||||
def test_block_scoped_query_aliases_do_not_use_the_last_assignment(self):
|
||||
source = r'''
|
||||
|
||||
@@ -138,27 +138,45 @@ class SelectorBindingTests(unittest.TestCase):
|
||||
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:
|
||||
def test_rebuild_candidate_uses_one_incremental_replay(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"), {})
|
||||
|
||||
# The production path must bind and export from one live session. If
|
||||
# it called the old compatibility binder, this patched entry point
|
||||
# would fail while rebuilding selector-bearing prefixes.
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
with patch(
|
||||
"engine.cdsl_engine.runtime.rebuild_cdsl",
|
||||
side_effect=AssertionError("production rebuild invoked legacy prefix replay"),
|
||||
):
|
||||
outcome = rebuild_candidate(candidate.cdsl, Path(directory) / "00016195.step")
|
||||
|
||||
self.assertEqual(outcome["status"], "rebuilt")
|
||||
self.assertEqual(
|
||||
[item["feature_id"] for item in outcome["result"]["feature_results"]],
|
||||
[item["id"] for item in candidate.cdsl["features"]],
|
||||
)
|
||||
self.assertTrue(outcome["selector_binding"])
|
||||
|
||||
def test_shell_offset_face_preserves_true_dependency_but_rejects_unproven_cap_selector(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.
|
||||
# F2 consumes its immediate direct CAP_FACE output role. F3 retains
|
||||
# its exact OFFSET_FACE true-dependency intent, but its separate
|
||||
# start-cap removal selector is deferred and must not rebind
|
||||
# geometrically.
|
||||
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)
|
||||
first, offset = prefix["features"][-1]["selectors"]
|
||||
self.assertEqual(first["selector_intent"]["query_family"], "CAP_FACE")
|
||||
self.assertEqual(first["selector_intent"]["derivation_policy"]["multiplicity"], "none")
|
||||
self.assertEqual({key: offset[key] for key in (
|
||||
"kind", "owner_feature_id", "output_role", "output_role_source", "source", "confidence",
|
||||
)}, {
|
||||
@@ -168,33 +186,25 @@ class SelectorBindingTests(unittest.TestCase):
|
||||
})
|
||||
self.assertEqual(offset["selector_intent"]["query_family"], "OFFSET_FACE")
|
||||
self.assertEqual(offset["selector_intent"]["disambiguation"]["type"], "true_dependency")
|
||||
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")
|
||||
with self.assertRaisesRegex(ValueError, "f_F3: selector_query_unsupported after prefix rebuild"):
|
||||
bind_candidate_selectors(prefix)
|
||||
|
||||
# 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["error"]["message"], "f_F3: selector_query_unsupported during incremental replay")
|
||||
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:
|
||||
def test_intersection_vertex_preserves_outer_copy_provenance_without_geometry_binding(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"), {})
|
||||
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]))
|
||||
|
||||
bound, evidence = bind_candidate_selectors(candidate.cdsl)
|
||||
reference = next(item for item in bound["features"] if item["id"] == "f_F7")["params"]["end_condition"]["reference"]
|
||||
self.assertEqual(reference["selector_intent"]["query_family"], "INTERSECT")
|
||||
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)
|
||||
self.assertEqual([item["selector_intent"]["query_family"] for item in copy_components], ["COPY", "COPY"])
|
||||
with self.assertRaisesRegex(ValueError, "f_F7: selector_query_unsupported after prefix rebuild"):
|
||||
bind_candidate_selectors(candidate.cdsl)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from cadfs_to_cdsl.dataset import Sample
|
||||
from cadfs_to_cdsl.reports import read_json
|
||||
from cadfs_to_cdsl.selector_candidate_demo import _search_candidate_records, run_geometry_probe, strip_provenance_intents
|
||||
|
||||
|
||||
def _contains_selector_intent(value: object) -> bool:
|
||||
if isinstance(value, dict):
|
||||
return "selector_intent" in value or any(_contains_selector_intent(child) for child in value.values())
|
||||
if isinstance(value, list):
|
||||
return any(_contains_selector_intent(child) for child in value)
|
||||
return False
|
||||
|
||||
|
||||
class SelectorCandidateDemoTests(unittest.TestCase):
|
||||
def test_search_candidates_excludes_records_below_normal_resolution_threshold(self) -> None:
|
||||
resolution = type("Resolution", (), {
|
||||
"candidates": (
|
||||
{"record_id": "low", "score": 0.5},
|
||||
{"record_id": "high-b", "score": 1.0},
|
||||
{"record_id": "high-a", "score": 1.0},
|
||||
{"record_id": "zero", "score": 0.0},
|
||||
),
|
||||
})()
|
||||
candidates, count = _search_candidate_records(resolution, maximum=8)
|
||||
self.assertEqual(count, 2)
|
||||
self.assertEqual([item["record_id"] for item in candidates], ["high-a", "high-b"])
|
||||
|
||||
def test_strip_provenance_intents_removes_nested_selector_metadata(self) -> None:
|
||||
value = {
|
||||
"selector_intent": {"version": "1.0"},
|
||||
"params": {"reference": {"selector_intent_version": "1.0"}},
|
||||
}
|
||||
self.assertEqual(strip_provenance_intents(value), 2)
|
||||
self.assertEqual(value, {"params": {"reference": {}}})
|
||||
|
||||
def test_geometry_probe_keeps_heuristic_success_separate_from_provenance(self) -> None:
|
||||
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
|
||||
feature = root / "featurescript_rp/0000/00002243.txt"
|
||||
step = root / "step_abc/0000/00002243.step"
|
||||
if not feature.exists() or not step.exists():
|
||||
self.skipTest("CADFS sample is not installed")
|
||||
sample = Sample("00002243", {"featurescript": str(feature), "step": str(step)})
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
output = Path(temporary)
|
||||
report = run_geometry_probe(sample, output)
|
||||
record = read_json(output / "samples" / sample.sample_id / "selector-record.json")
|
||||
compiled = read_json(output / "samples" / sample.sample_id / "selector_record.compiled.cdsl.json")
|
||||
self.assertEqual(report["provenance"]["rebuild_status"], "rebuild_failed")
|
||||
self.assertEqual(report["geometry_probe"]["rebuild_status"], "rebuilt")
|
||||
self.assertEqual(report["geometry_probe"]["comparison"], {
|
||||
"decision": "strict_pass",
|
||||
"strict_passed": True,
|
||||
"rp_passed": True,
|
||||
})
|
||||
self.assertEqual(report["strategy"]["classification"], "heuristic_only")
|
||||
self.assertEqual(report["selector_record"]["status"], "strict_replayed")
|
||||
self.assertEqual(report["selector_record"]["record_count"], 2)
|
||||
self.assertEqual(record["verification"], {
|
||||
"geometry_probe_strict": True,
|
||||
"compiled_cdsl_strict": True,
|
||||
"compiled_cdsl": report["selector_record"]["compiled_cdsl"],
|
||||
})
|
||||
self.assertEqual([entry["mapping"] for entry in record["records"]], ["one_to_one", "one_to_one"])
|
||||
self.assertTrue(all(entry["source_selector"]["selector_intent"]["query_family"] == "CAP_EDGE" for entry in record["records"]))
|
||||
self.assertFalse(_contains_selector_intent(compiled))
|
||||
Reference in New Issue
Block a user