feat(cadfs): expand selector provenance coverage

This commit is contained in:
2026-09-14 11:00:14 +08:00
parent 994d06aaea
commit de21ee8a5b
45 changed files with 19837 additions and 842 deletions
File diff suppressed because it is too large Load Diff
@@ -10,15 +10,211 @@ 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.
The planned expansion of query algebra, topology lineage and body lifecycle is
defined in [CADFS_FULL_CAPABILITY_TARGET.md](CADFS_FULL_CAPABILITY_TARGET.md),
under “Selector 精度与覆盖目标(2026-09-10 起)”. A row is admitted only after
its explicit source/CDSL contract, exact kernel evidence and consumer semantics
are tested; source STEP comparison remains separately reported and does not
promote a restricted row into general family support.
`SWEPT_EDGE@1511` direct-prism dress-up continuation update (2026-09-12): a
direct source vertex may retain its existing `boundary + continuation` policy
past the producer's feature position only when the topology registry proves a
complete, one-to-one, active lineage. `00094474` F3 is strict after an
intervening datum feature; `00027017` F3 is rejected as non-unique after a
body mutation and retains its F2 checkpoint. `00370320` F4 proves a direct
`extrude boundary -> subtract continuation` and is RP passing. This changes
neither the direct profile, version, body-lifecycle nor no-fallback
restrictions in the rows below. Source anchors are additionally scoped to the
selector's explicit producer owner: a later operation that materializes the
same source sketch entity or endpoint pair cannot become a second anchor.
`00407468` F3 verifies four `qUnion(SWEPT_EDGE)` operands from F1 through F2;
F1--F3 replay and RP comparison pass in
`output/swept-edge-producer-anchor-00407468-20260912` (strict area/volume
precision checks do not pass). The rule does not admit generic repeated-source,
boolean, IMPRINT, COPY, pattern, or N:M lineage forms.
`CAP_FACE@1511` symmetric direct-prism shell update (2026-09-12): an immediate
shell may remove either or both physical caps of an independent, undrafted,
`new_body` symmetric blind prism whose profile is exactly one original circle.
Each CAP OSD must name that sole circle source edge. The two `MakePrism`
builders share the source plane, so the executor maps only their far exact
handles to `extrude.end` and `extrude.start`, checks final-snapshot `IsSame`,
and never exposes the source-plane seam as a cap. `00019252` removes both
caps and RP-passes; `00000316` resolves both caps and executes its shell before
an unrelated `OFFSET_EDGE` query stops later replay. This bridge is limited to
the immediate shell consumer: multi-edge/hole/IMPRINT/split profiles, draft,
ADD/CUT, non-blind extents, continuation, other consumers/versions, and all
geometry, stable-ID, current-body, or source-STEP fallback remain deferred.
`CAP_FACE@1511` initial direct-sweep shell update (2026-09-12): an immediate
shell may consume one `sweep.start` or `sweep.end` only where the producer is
an independent `new_body` direct PipeShell sweep and its CDSL contract retains
one profile source edge plus one source path edge. The CAP OSD must name that
exact profile edge and the matching path `start`/`end` vertex; after any
lowering path reversal, the source endpoint is explicitly mapped to the
physical PipeShell output role before the resolver requires its unique
complete active builder relation. `00330012` F3 resolves `sweep.end` and
rebuilds. `00658358` F4 keeps its F3 checkpoint because its CAP source pair
does not satisfy this contract. No geometry, stable-ID, current-body or source
STEP fallback is introduced; segmented/spatial/hollow/fused/additive sweeps,
copy/pattern successors, later lifecycle, non-shell consumers and generic
sweep CAP queries remain deferred.
`CAP_EDGE@1511` initial direct-sweep dress-up update (2026-09-12): an
immediate `fillet`/`chamfer` may consume one cap edge from an independent
`new_body` direct PipeShell sweep only when its profile is one retained direct
circle edge and its CAP OSD contains exactly that edge plus the corresponding
source path endpoint. PipeShell has no per-profile-edge `FirstShape`/`LastShape`
overload, so the adapter records the relation only when the builder-proven cap
face has exactly one final-snapshot boundary edge (`IsSame` verified). The
runtime additionally receives an exact source-edge anchor and resolves only
the complete/proven role-qualified boundary relation. A `00330012` CAP_EDGE
source variant resolves by `kernel_lineage` and rebuilds; a contradictory
endpoint variant keeps its F2 checkpoint. Multi-edge/inner-wire/split profile,
segmented/spatial/hollow/fused/additive sweep, continuation, copy/pattern,
later lifecycle, other consumers and generic CAP_EDGE remain deferred. No
geometry, stable-ID, current-body or source STEP fallback is allowed.
`CAP_EDGE@1511` primary ADD union-continuation update (2026-09-13): an
immediate `fillet`/`chamfer` may follow a direct, undrafted blind
`extrude_add_blind` only when its default ADD/fuse has exactly one old active
solid, one direct-prism tool solid, complete source anchors, and exact
`BRepAlgoAPI_Fuse` history. The tool is registered only as a transient prism
snapshot; its cap edge becomes selectable only through one complete/proven,
one-to-one `extrude -> union` continuation into the active final member.
`00953397` F7 and `00957101` F4 both resolve by `kernel_lineage` and are RP
approximate. `00406939` has a multi-solid ADD tool and remains a deferred
FeatureScript query. Split/merge, partial/missing/fuzzy union history,
IMPRINT/partial profile, non-CAP_EDGE family, later lifecycle, COPY/pattern,
and all geometry, stable-ID, current-body, or source-STEP fallbacks remain
outside this bridge.
`CAP_FACE@1511` primary ADD shell union-continuation update (2026-09-13): an
immediate shell may consume one cap role from a direct, undrafted blind
`extrude_add_blind` only when its default ADD/fuse has one old active solid,
one direct-prism tool solid, and exact `BRepAlgoAPI_Fuse` history. The tool is
transient; the resolver must establish a complete/proven one-to-one
`extrude -> union` continuation into the active final member before handing
the face to shell. In a 27-history fresh replay scan, seven histories had that
exact relation; `00074047` F4 and `00350698` F7 resolve it by `operation_role`,
then their native shell operation independently fails. `00293014` remains
`selector_output_role_ambiguous`; `00590599` multi-solid ADD remains
`selector_query_unsupported`. This does not authorize extent, dress-up,
multiple removals, split/merge, multi-solid/IMPRINT/partial profile,
missing/partial/fuzzy union history, later lifecycle, COPY/pattern, or any
geometry, stable-ID, current-body, or source-STEP fallback.
`CAP_FACE@1511` primary ADD up-to-surface union-continuation update (2026-09-13):
a one-sided immediate `up_to_surface` extent may consume one primary-ADD cap only
under the separately tagged `primary_add_up_to_surface_union_continuation`
contract. The producer is an undrafted blind `extrude_add_blind` defaulting to
ADD/fuse; its transient direct-prism cap becomes selectable only after one old
active solid plus one tool solid yield exact complete/proven one-to-one
`extrude -> union` history into the active final member. `00007973` F5 -> F7
resolves by `operation_role` and freshly rebuilds. `00638700` F3 -> F5 records
an ambiguous union successor and is rejected as `selector_output_role_ambiguous`.
Two-sided extents, non-immediate/later consumers, split/merge/multi-solid,
draft, incomplete/fuzzy history and all geometry, stable-ID, current-body, or
source-STEP fallback remain outside this bridge; it does not complete general
CAP_FACE or `up_to_surface` coverage.
`CAP_FACE@1511` symmetric two-sided up-to-surface pair update (2026-09-13):
an independent direct `new_body` `extrude_add_two_sided` may consume the two
opposite far caps of its immediately preceding symmetric direct prism only as
one `symmetric_direct_prism_two_sided_up_to_surface_cap_pair`. Both producer
ends must be blind and undrafted; each source CAP query must be a singleton
direct `qUnion(makeQuery(... CAP_FACE ...))` whose OSD names the complete,
unchanged original profile from one source sketch. The pair must have the same
owner and exactly `{extrude.start, extrude.end}`; neither side is legal alone.
The executor's two `MakePrism.LastShape` handles are already final-snapshot
roles, and runtime resolves each through complete/proven `boundary` evidence.
Fresh `00215642` F1 -> F3 resolves both roles and rebuilds its executable
prefix. `00935255` F7 names two different owners and remains deferred. CUT,
ADD/fuse, mixed/non-blind ends, draft, partial/IMPRINT/split profile selection,
same-role or different-owner pairs, non-immediate/later lifecycle, other
versions, and every geometry/stable-ID/current-body/source-STEP fallback remain
rejected; this does not complete generic two-sided `up_to_surface` or
`CAP_FACE` coverage.
`SWEPT_FACE@1511` initial direct-sweep dress-up update (2026-09-12): an
immediate `fillet`/`chamfer` may consume one side face from the same independent
`new_body` direct PipeShell sweep only when its OSD contains exactly one edge
from a complete direct analytic closed profile and one direct source path edge.
The adapter records only `Generated(profile_edge)` face outputs that remain in
the final solid by `IsSame`; the runtime resolves from the exact profile-edge
anchor, while the path edge remains disambiguation evidence. `00330012` circle
and four-edge source-form variants resolve by `kernel_lineage` and rebuild. A
mismatched source pair is kept as a deferred query without geometry or stable-ID
fallback; `00954785` confirms that a multi-segment qUnion path cannot inherit a
representative source edge. Inner/split, segmented/spatial/hollow/fused/additive, continuation,
copy/pattern/later lifecycle, shell/extent/sketch-host consumers and generic
sweep side-face semantics remain deferred.
`CAP_FACE@1511` direct-prism workplane update (2026-09-14): a following
`newSketch` may attach to one physical cap of the immediately preceding,
independent, undrafted, blind `extrude_add_blind` `new_body` prism. Lowering
keeps the CAP output-role selector and a `direct_prism_cap_face_workplane`
consumer contract; runtime resolves the live native face before rebuilding the
sketch coordinates. Semantic validation rejects a delayed or multiple consumer.
`00126630` F1 -> F2 supplies real lowering evidence, while an equivalent
direct consumer resolves by `operation_role`. The separately constrained
attached CAP-edge IMPRINT tuple below is its only profile-region consumer. This
admits neither other CAP-edge profile regions, ADD/fuse, CUT, symmetric
or drafted/non-blind producers, later lifecycle, COPY/pattern/boolean
successors, nor static frame, stable-ID, geometry, current-body or STEP
fallback.
Attached planar-IMPRINT external-boundary foundation (2026-09-14): the CDSL
profile contract can name a session-resolved edge as an external fragment
anchor only when a proven runtime face attachment supplies the splitter support.
Runtime requires that the selector resolve to one edge which is `IsSame` to a
native boundary of that support face. It is generally only a CDSL/runtime
foundation; the one source-qualified CAP-edge form below is the sole current
exception.
Attached direct-prism CAP-edge IMPRINT profile tuple (2026-09-14): FeatureScript
1511 may extrude one selected local circle from a preceding attached
sketch only when its sole `INTERSECT(VERTEX)` witness pairs that curve with one
direct `CAP_EDGE` from the exact same immediate independent blind `new_body`
prism and exact CAP-face attachment role. The lowerer emits one
`planar_imprint.external_anchors` selector, not boundary geometry; semantic
validation requires its runtime face attachment, and runtime proves both the
CAP edge's kernel lineage and native `IsSame` support-boundary membership before
the splitter consumes it. When native attachment orientation reverses the
source sketch normal, the session converts IMPRINT `face_side` only in its
live materialized frame. `00126630` F1 -> F3 lowers completely and its F1/F3
prefix rebuilds (`165528 mm3`), resolving the CAP face by `operation_role` and
CAP edge by `kernel_lineage`; its later F4 CAP-edge dress-up remains deferred.
The owner-mutation rejection returns F3 to
`extrude_profile_topology:cap_edge`. This is one real-history source tuple,
not general CAP-edge profiles: multiple anchors/fragments/local curves,
non-analytic or periodic curves, other attachment types, CUT/ADD, delayed or
continued producers, copies/patterns/booleans, and all unproven region
cardinality remain deferred.
| 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. |
| `qUnion` / `qIntersection` / `qSubtraction` / `QUERY_SET` | `1511` | CADFS exported set-query syntax with exact recursive `query_expr@1.0` nodes; `qIntersection`/`qSubtraction` ordering semantics additionally verified against the local Onshape 1511 standard-library mirror | A `proven_operand_*` parent may be consumed by `fillet` or `chamfer` when every recursive branch ends in a direct, kernel-proven `makeQuery` leaf of one face/edge kind. Each parent declares `active_member` scope, reject-empty/all-multiple policy, and ordered child selectors; nested set nodes retain their own source expression and contract | This remains a set compositor, not a leaf-query allow-list entry. Every branch must have the identical FeatureScript/library tuple, exact matching AST node, runtime-snapshot evidence, an existing executable provenance contract, and complete active results. The resolver recursively evaluates children, preserves operand order, deduplicates exact record IDs for union, and applies exact-record intersection/subtraction. A `QUERY_SET` in the declared `feature.selectors` slot of `fillet`/`chamfer` may pass that slot only to recursive `query_operands`, so an already-contractual immediate direct-prism/planar-IMPRINT `CAP_FACE` output-role leaf receives the normal owner, lifecycle and cardinality checks; `00965724` F2's `{extrude.start, extrude.end}` union converts completely and rebuilds. Output roles in metadata or non-`query_operands` structures, output-role leaves without that existing direct contract, mixed kind/version, geometry/stable-ID/binding evidence, filters, `qAdjacent`/owner/body queries, and shell/extent/hole/transform/reference consumers remain rejected, as do empty or partial/inactive branches. `00000715` F2 and `00354246` F2 remain valid evidence; a nested-union variant of `00000715` now also rebuilds successfully. This does not constitute general query-family completion. |
| `CAP_FACE` | `1511` | CADFS FeatureScript 1511 exported query history; direct prism `BRepPrimAPI_MakePrism` cap handles, direct `BRepOffsetAPI_ThruSections.FirstShape/LastShape` face handles for the loft subset | Direct `extrude.start` / `extrude.end` builder output role; an immediate `up_to_surface` reference or shell face-removal selector may consume the one-sided prism role. An immediate shell may additionally consume either exact far cap of a direct one-circle symmetric blind prism, or `loft.start` / `loft.end` from the initial direct-loft subset. | One-sided prism consumers must immediately follow a direct `extrude_add_blind` `new_body` blind producer. The symmetric shell-only subset requires FeatureScript 1511, one original circle source edge, independent undrafted `extrude_add_two_sided`, both blind ends, exact far-cap final `IsSame` roles and no continuation; `00019252` RP-passes after removing both caps and `00000316` resolves both caps before a later unrelated selector stops. The loft subset is FeatureScript `1511` only: exactly two direct closed IMPRINT sheet profiles, default/`NEW` operation, no prior solid, wire profiles, connections, matching or endpoint derivative options; the CAP OSD must name exactly one of those profile sources. `isStart` is not used to choose the role: source-profile position maps to `loft.start`/`loft.end`, then the resolver requires its unique complete active builder relation. `00023963`, `00051031`, and `00059941` resolve the shell role and execute; their final RP comparisons are rejected. `00157619` and `00139197` resolve before native OCC thick-solid failure. `00925274` F3 resolves prism `extrude.start` for `up_to_surface`; `00212904` F2 and `00789939` F2 resolve immediate prism shell removals through `extrude.start` and `extrude.end`. Multi-edge/hole/IMPRINT/split symmetric profiles, draft, ADD/CUT, non-blind ends, later mutations, other loft versions or forms, non-immediate consumers, and generic CAP queries remain unsupported. No stable-ID, binding-ID, geometry, current-body, or source-STEP fallback is used. |
| `CAP_FACE` primary ADD dress-up | `1511` | `00051494` F3 -> F4 and `00660816` F3 -> F4 direct exported histories; transient direct-prism cap plus exact `BRepAlgoAPI_Fuse` relation | A default ADD/fuse `extrude_add_blind` CAP role may drive its immediately following `fillet` or `chamfer` through `primary_add_dressup_union_continuation` | The producer must be undrafted blind ADD/fuse with one old solid and one transient direct-prism tool. Runtime resolves only a complete/proven, one-to-one `extrude -> union` active-member successor, then expands the resolved physical face to actual body-boundary edges while excluding periodic seams. `00051494` resolves by `operation_role` and rebuilds; `00660816` has a one-to-many successor and remains `selector_output_role_ambiguous`. Shell/extent use separate contracts. Multi-cap sets, split/merge/multi-solid/IMPRINT/partial profiles, draft, non-immediate/later lifecycle, other consumers, stable-ID/binding-ID/geometry/current-body/source-STEP and transient-tool fallback remain rejected. This does not complete CAP_FACE, ADD, dress-up or S1 lifecycle coverage. |
| `CAP_EDGE` | `1511` | CADFS FeatureScript 1511 exported query history; direct-prism `BRepPrimAPI_MakePrism.FirstShape(source_edge)` / `LastShape(source_edge)` exact cap-edge handles, exact single-solid `BRepAlgoAPI_Fuse` history for the primary-ADD subset, plus the one-edge direct-PipeShell cap-face boundary witness | A direct fillet/chamfer consumer of either (a) 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; (b) a default primary ADD/fuse of one old active solid plus one direct blind-prism tool solid, through one exact `extrude -> union` continuation; or (c) the immediately preceding independent `new_body` direct PipeShell sweep with one direct circle profile edge and one direct path edge -> one role-qualified `sweep.start` or `sweep.end` cap edge | Prism selectors must name one retained direct source entity and explicit CAP side. The adapter accepts a prism cap handle only after final-snapshot `IsSame`; resolver follows its source anchor and role through `boundary`, and accepts a continuation only when every relation is complete/proven and operation cardinality remains one-to-one. The primary-ADD tool is transient and never directly selectable: lowering permits it only for FeatureScript 1511 `CAP_EDGE`, immediate fillet/chamfer, direct undrafted blind `extrude_add_blind`, default ADD/fuse, singular old/tool solids and exact union history. `00953397` F7 and `00957101` F4 resolve `extrude -> union` and are RP approximate; `00406939` multi-solid ADD remains deferred. The sweep subset has no PipeShell per-edge history overload: it requires exact profile/path-endpoint OSD, a singleton profile/cap boundary edge, immediate fillet/chamfer consumer, and only the initial `boundary` relation. `00330012` CAP_EDGE source variant resolves by `kernel_lineage` and rebuilds; contradictory endpoint source is rejected with an F2 checkpoint. A direct all-circle annulus with one contained circle is included for the prism form 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, multi-solid/IMPRINT primary ADD, missing/partial/fuzzy union history, later lifecycle, and unproven boolean/SPLIT/COPY/pattern continuations are rejected. The sweep form additionally rejects multi-edge/inner-wire/split profile, segmented/spatial/hollow/fused/additive path, continuation, COPY/pattern/later lifecycle and non-dress-up consumers. This is not general CAP_EDGE replay. |
| `CAP_EDGE` | `2491` | CADFS `00049094` / `00404726` exported `onshape/std/geometry.fs@2491.0` symmetric-extrude histories; two exact `BRepPrimAPI_MakePrism.LastShape(source_edge)` far-cap handles, each checked against the final fused result | An immediate fillet/chamfer may consume an independent, undrafted `new_body` `extrude_add_two_sided` whose primary and reverse ends are both blind and whose lowered profile exactly equals the complete original source profile | The executor records one prism delta per direction. The primary far `LastShape` is `extrude.end`; the reverse far `LastShape` is explicitly remapped to `extrude.start`. Source-plane `FirstShape` results are internal fusion seams and cannot be selected. Each role must retain complete/proven final-snapshot `IsSame` evidence; lowerer emits `boundary` only, with no continuation or fallback evidence. `00049094` F2 resolves four CAP edges as one `QUERY_SET` and the full history rebuilds, but comparison is rejected. `00404726` remains `selector_query_unsupported` at F2 because its selected multi-region/IMPRINT profile is not this complete direct profile. CUT, draft, non-blind/mixed extents, other versions, partial/IMPRINT/split/multi-region profiles, fused/boolean/COPY/pattern/later lifecycle and all geometry/stable-ID/current-body/source-STEP fallback remain rejected. This is not general 2491 CAP_EDGE replay. |
| `CAP_VERTEX` | `1511` | CADFS FeatureScript 1511 direct `makeQuery(..., CAP_VERTEX, VERTEX, {disambiguationData: [OSD([edge, edge])], isStart})`; `BRepPrimAPI_MakePrism.FirstShape(source_vertex)` / `LastShape(source_vertex)` exact cap-vertex handles | A one-sided `up_to_vertex` extent may consume an independent `new_body` undrafted blind prism cap vertex. The OSD must name exactly two distinct retained direct profile edges from one source sketch with exactly one shared original endpoint. Between producer and consumer, only reference planes or independent undrafted `new_body` blind prisms are permitted. | The resolver uses the grouped transient source-vertex anchor and only the role-qualified `generated vertex -> vertex`, `boundary`, complete/proven final-snapshot relation. A separate body may intervene only when the session maps the declared semantic member to exactly one old and one new solid snapshot, and every face/edge/vertex has exactly one reciprocal `IsSame` counterpart; the registry emits `body_member_preserve` only then. Aggregate `body` records are excluded because they are snapshot metadata rather than a member subshape. Vertex records are exported from a final B-rep explorer because build123d's repeated `body.vertices()` wrappers do not retain `IsSame` identity with prism history handles. `00330726` F1 -> F3 -> F5 resolves by `extrude` plus `body_member_preserve` and executes; its fresh full pipeline remains `converted_partial` / `rebuilt_rejected` due to later F7/F8/F9/F10 gaps and a 3 vs 4 solid mismatch. Other versions, changed/replaced/deleted or incomplete/non-unique members, draft, cut/fuse, split/IMPRINT/multi-profile geometry, non-shared or repeated OSD edges, two-sided extent, inactive/non-unique records, backward/non-uniform target projection, and every geometry/stable-ID/current-body/source-STEP fallback remain rejected. Thus this is a narrow runtime contract, not CAP_VERTEX family completion. |
| direct source `sQuery(..., VERTEX, ...)` extent datum | `1511` | A current source audit found 16 `UP_TO_VERTEX` histories: 15 direct source datum targets (`00383982`, `00444951`, `00503730`, `00510558`, `00753006`, `00894150`, `00975649`) plus one each `CAP_VERTEX` and `INTERSECT(VERTEX)` form; each direct datum has an explicit source workplane and named entity endpoint | A one-vertex direct source query may terminate a one-sided or either side of a two-sided `up_to_vertex` extrusion as `{kind: source_vertex, source_sketch_id, source_entity_id, point_mm}` | This is not a topology selector. Schema and semantic validation require one known source sketch, source entity and finite 3D point; selector binding skips it and runtime checks one positive uniform projection across the profile without resolving a topology record. `00444951`'s three extents convert, rebuild and strict/RP-pass in fresh `/private/tmp/cadfs-source-vertex-extent-20260914-r2`. The seven-sample matrix at `/private/tmp/cadfs-source-vertex-extent-matrix-20260914` executes every datum feature in `00510558`, `00753006`, `00894150` and `00975649`, while `00503730` preserves its F5 prefix; their complete comparisons remain rejected or partial for independent downstream modelling gaps. `00383982` stops before its datum feature at a separate unbounded IMPRINT profile failure. `qAdjacent`, set/derived/runtime vertex queries, CAP/COPY/pattern/IMPRINT/INTERSECT vertices, non-1511 source, invalid/non-finite datum and geometry/stable-ID/current-body/source-STEP fallback are rejected. This does not complete `CAP_VERTEX`, `INTERSECT(VERTEX)`, generic vertex selection or general `up_to_vertex`. |
| `BLEND_EDGE` | `1511` | CADFS 1511 direct query history; fillet/chamfer `Generated(source_edge)` patch, `Modified(source_face)` target and final `IsSame` shared-boundary witness | One immediate native fillet/chamfer consumer of a direct independent blind `new_body` prism: exactly one direct `CAP_EDGE` plus either a complete direct `CAP_FACE`, or the same-anchor direct `SWEPT_FACE`, in `blendedFrom`, with an identical single face query in `blendedInto`, resolves to one exact active final patch-boundary edge | `blend_sources` retains the pair independently of outer selector fields. The resolver requires one source-profile edge anchor, its role-qualified cap edge, and either its direct cap face role or its exact `Generated(source_edge -> face)` swept-face relation, followed by one complete/proven `exact_blend_boundary` relation. `00414347` F3 is RP passing but not strict; `00690433` F3 executes but comparison is rejected; `00596552` F5 and `00456146` F4 now lower their same-anchor SWEPT_FACE leaves under this contract, but fresh replays stop respectively at the pre-existing native chamfer feasibility failure and another selector relation ambiguity, so they are contract/lowering evidence only. `00801833` F3 executes before its F4 checkpoint; `00614954` F3 rejects its cap-role mismatch. `MERGE`, `SPLIT`, `COPY`, `BLEND_FACE`, differing source anchors, multi-member sets, non-immediate lifecycle, ambiguous/incomplete/inactive history, angle/two-offset dress-up, other producer/version, and geometry/stable-ID/current-body/source-STEP fallbacks remain rejected. This is not general BLEND_EDGE replay. |
| `BLEND_FACE` | `1511` | CADFS direct dress-up sketch workplanes; final-snapshot native fillet/chamfer `Generated(input_edge)` patch handles | A runtime-attached `newSketch` can host on one immediate native fillet/chamfer patch when one OSD source edge is also its sole role-qualified direct-prism `CAP_EDGE`; producer is an undrafted blind `extrude_add_blind` `new_body` direct profile prism | `blend_face_source` stores no static frame, geometry, stable ID, or record ID. Semantic validation repeats the producer/dress-up lifecycle gate; resolver proves source anchor -> cap edge -> one complete/proven generated active patch, then uses that actual planar B-rep face for the workplane. `00313870` F2 -> F3/F4 and `00436592` F3 -> F4 lower as attached sketches. Fresh replays preserve checkpoints: `00313870` stops at a pre-existing F2 chamfer feasibility failure, while `00436592` executes F3 then stops at later F6 unsupported query; neither is strict/RP evidence. Multi-edge OSD, two-sided/IMPRINT/split/drafted/non-direct profiles, non-immediate/nested dress-up, MERGE/SPLIT/COPY/pattern/boolean successors, non-planar/ambiguous/partial/inactive patches, other versions, and geometry/stable-ID/current-body/source-STEP fallbacks remain rejected. This is not general BLEND_FACE or generic sketch attachment coverage. |
| `COPY` | `1511` | CADFS `00252195` direct `COPY(CAP_EDGE)` plus `00573124`/`00951631` immediate `COPY(CAP_FACE)` workplane histories; exact direct-prism cap and primary-cut `BRepAlgoAPI_Cut` history | An immediate fillet/chamfer may consume one CAP edge, or one following sketch profile/hole location may attach to same-owner CAP face, from an undrafted default blind `extrude_cut_blind` | The edge form requires `primary_cut_cap_edge` and one direct source edge. The face-host form requires `primary_cut_cap_face_workplane`: semantic preflight cross-checks its complete unchanged OSD set against the producer's actual direct profile source sketch/entities, then runtime requires one role-qualified cap -> transient-face boundary and one same-owner complete/proven `subtract` continuation to one active planar face. Only then does runtime derive a native-UV/support-plane workplane and re-resolve local sketch coordinates. Both return `copy_lineage`; transient tools are never selectable. `00252195` resolves then hits native chamfer failure; `00573124`/`00951631` rebuild but fresh strict/RP comparison rejects both, while non-immediate `00252794` F8 remains deferred. COPY `SWEPT_FACE|BODY`, patterns/transforms, draft/two-sided/non-direct or non-immediate lifecycle, opposite-direction attached holes, multiple/nonplanar/partial/split/merge successors, other versions and every geometry/stable/snapshot/current-body/source-STEP fallback remain rejected. This is not general COPY 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. |
| multi-source transform `COPY(BODY)` | `1540` evidence | A direct `COPY(instanceName=1)` chain from one direct source of a preceding `makeCopy:true` transform with at least two selected body sources | Explicit `booleanBodies` target/tool selection may consume the source-qualified copy through `{transform_feature_id, source_feature_id}` | The owner must be preceding, be `transform_bodies`, preserve at least two direct `source_feature_ids`, and retain the selected source's independently active COPY member. Schema, semantic validation, capability preflight and executor reject an owner/source mismatch, single-source COPY, a producer aggregate, inactive member, targetless COPY boolean and any geometry/stable-ID/current-body fallback. The corpus has no native COPY-to-boolean history: a source-only contract variant extends real 1540 `00699847` F3's two-source COPY with F5 `booleanBodies`; all five features lower and execute, but it is not strict/RP evidence and does not mark generic COPY lifecycle complete. |
| single-source chained transform `COPY(BODY)` | `1511` | CADFS `00184423` and `00322866` direct `COPY(instanceName=1)` histories retain an original `SWEPT_BODY` owner while each transform consumes the preceding explicit member | A `make_copy:true` `transform_bodies` feature may retain `{source_feature_id, active_member_feature_id}` provenance metadata when exactly one direct runtime source is the immediately preceding active COPY member | Lowering derives the semantic source only from the recursive `derivedFrom` chain and emits one alias only when it differs from the selected `source_feature_ids` member. Schema/semantic validation require one single-source `make_copy` transform, one preceding semantic source, and an alias member exactly equal to the direct selected source; aliases never participate in runtime body selection. `00184423` F3--F5 and `00322866` rebuild in fresh RP pipeline as `rebuilt_strict` and `rebuilt_approximate`. Multi-source/aggregate COPY, patterns, boolean/delete/non-copy successors, missing or mismatched members, arbitrary transforms, selector topology propagation, and all geometry/stable-ID/current-body/source-STEP fallback remain rejected. This is body-lifecycle provenance only, not general COPY or transform coverage. |
| `qOwnerBody` / `OWNER_BODY` | `1511` | FeatureScript 1511 query AST plus a synthetic `00694309` history variant that replaces its equivalent direct `SWEPT_BODY` reference with `qOwnerBody(makeQuery(..., SWEPT_FACE, FACE, ...))` | A one-sided `up_to_body` extent may project one direct, kernel-proven `SWEPT_FACE` or direct builder-proven `CAP_FACE` input to its exact active body record | The parent must preserve `filter: owner_body` with one typed nested selector, identical FeatureScript/library tuple, `exact_input_owner`, `active_member`, reject-empty and one-result policy. Runtime first resolves the child with its existing provenance contract, requires exactly one result whose `body_id` equals the active aggregate ID, then returns the unique non-transient body record with that same ID. It does not infer ownership from the current aggregate, feature creator, geometry, stable/snapshot/binding ID, or partial/multi-record lineage. Multi-solid member IDs, nested/query-set inputs, later producer lifecycle, non-direct topology producers and generic `qOwnerBody` consumers remain rejected. The corpus currently has no native `qOwnerBody` call, so this is a synthetic contract boundary, not query-family completion. |
| `OFFSET_FACE` | `1511` | CADFS FeatureScript 1511 exported query history; a complete direct-prism profile OSD set may also name the shell's retained cap | (a) `shell.offset_face` builder output role plus one TDD source cap, or (b) an immediate `extrude_from_face` profile from the one retained cap of a direct-prism shell | (a) requires an immediate direct shell owner and one explicit `extrude.start` or `extrude.end` true dependency. (b) is a separate `shell_retained_direct_prism_cap_offset_face_profile` contract: source and library must be 1511; the immediate shell depends only on an undrafted direct `new_body` blind prism, removes exactly one of its CAP roles, and the OFFSET query has exactly one OSD whose source-edge set equals the complete unchanged direct profile. The lowerer names the opposite, retained CAP role as `output_role_source`; semantic validation and preflight repeat the immediate-owner, single-removal, direct-producer, complete-set and blind additive-new-body checks. Runtime selects only the unique active `shell.offset_face` carrying the exact complete/proven kernel relation from that retained role, never a wall or closing descendant. `00719927` F1--F4 executes and F3 resolves this way in `output/offset-face-retained-cap-20260913`, but its final strict/RP comparison is rejected, so it is execution evidence only. Partial/multi-source OSD, multiple removals, the removed cap, draft/cut/fused/revolve/sweep/COPY/pattern producers, non-immediate shell/lifecycle, non-1511 source and geometry/stable-ID/current-body/source-STEP fallback remain rejected. |
| `OFFSET_EDGE` | `1511` | CADFS 1511 shell history in either (a) singleton OSD plus singleton TDD direct-prism `CAP_EDGE`, or (b) one OSD containing two distinct direct profile edges with one shared original endpoint; exact `MakePrism` and final-snapshot shell history | An immediate fillet/chamfer may consume (a) one retained one-sided direct-prism cap edge through `direct_prism_shell_offset_edge_tdd`, or (b) one direct-prism source vertex's swept edge through `direct_prism_shell_offset_edge_vertex` | TDD requires matching outer/nested source edge and routes only through its requested `extrude.start|end` cap boundary; vertex requires exactly two distinct unchanged source edges, one grouped source-vertex anchor and no cap role. Both require an independent undrafted one-sided blind `new_body` producer, one immediate inward shell depending only on it, a single CAP-face removal, and only complete/proven one-to-one lineage into the active member. `00650671` F3 executes through the retained-cap contract; `00768679` F8 rebuilds but comparison rejects volume/area. `00059593` resolves all four OSD-only vertex leaves and rebuilds in `output/offset-edge-vertex-matrix-20260913`, but comparison is rejected, so it is execution evidence only. A set mixing forms (`00791920`) is rejected as a whole: TDD leaves remain explicit while OSD leaves stay deferred. `TDD(SWEPT_EDGE)` is separate and remains deferred: the three native co-occurrences (`00239888`, `00882527`, `00902029`) respectively involve an IMPRINT/pre-shell-filleted prism, BLEND, and revolve/BLEND, with no valid direct-prism immediate-shell continuation. Semantic validation and capability preflight recursively apply these lifecycle predicates to every `QUERY_SET` leaf, so a parent cannot bypass the producer/shell gate. Neither contract treats generated `shell.wall`, cap/face boundary traversal, stable ID, geometry, current body or source STEP as equivalent evidence. Two-sided, draft/ADD/CUT, partial/IMPRINT/split profile, multiple removal faces, non-immediate/later mutation, COPY/pattern/transform, non-unique component/vertex, other versions and all fallback remain rejected. This does not complete general OFFSET_EDGE or inner shell-wall semantics. |
| `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, and direct-PipeShell `Generated(profile_edge)` final-face outputs | An immediate fillet/chamfer consumer or shell removal, a one-sided `up_to_surface` extent, or the separately paired shell successor two-sided extent may use a retained direct-prism source-profile edge -> generated side face, with only complete/proven continuations to one active face. Separately, an immediate fillet/chamfer may use an independent `new_body` direct PipeShell sweep with one selected edge from a complete direct analytic closed profile and one direct path edge -> initial generated side face | The prism contract requires an independent, undrafted, `new_body` blind source and one exact direct wire/face construction anchor. The one-sided extent variant carries no output role, stable ID, binding ID, or geometry hint. `immediate_retained_source_prism_swept_face_up_to_surface` separately admits one immediate 1511 `new_body` blind prism made from an IMPRINT-selected profile only when exactly one OSD edge is unchanged and singleton-identical in both selected and original source profiles; it permits only that next one-sided extent and `boundary`, never continuation. Semantic validation and capability preflight repeat the producer/source-import/edge witness, and runtime requires exact generated edge-to-final-face lineage. `00408613` F1 -> F3 resolves `F0/E0.bottom` by `kernel_lineage`; its later F4 CAP_EDGE failure retains the F3 checkpoint. Tampering the anchor to the excluded `E1` is rejected by both gates. The paired `symmetric_direct_prism_shell_swept_face_up_to_surface_pair` requires exactly two distinct original non-construction line anchors from one prism, immediately followed by a shell depending only on that prism; a target side selected for shell removal is rejected so a `shell.closing_descendant` cannot stand in for `shell.offset_face`. Runtime separately proves each `edge -> prism wall -> shell.offset_face` chain. `00180262` F1 -> F2 -> F4 resolves both targets and executes, but its RP comparison is rejected. 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. The sweep subset requires an exact two-item profile/path-edge OSD, a complete unique source-edge list for the one profile contour, and a source path query with exactly one `sQuery`; every `Generated(profile_edge)` face is final-snapshot `IsSame` verified, runtime anchors only on that profile edge, and source-pair failure stays a deferred query without geometry/stable-ID fallback. `00330012` circle and four-edge source-form variants resolve and rebuild; real `00954785` multi-segment path remains deferred. Resolver traverses only the requested result topology kind and rejects partial, split, merge or inactive branches. `00925274` resolves F1 E0 through F3, F6, F9 and F12 target continuations and is strict/RP passing. 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 other than the singleton exact-edge extent tuple, draft, fused source results, unproven/split continuations, revolve, inner profile sweep, multi-segment/spatial/hollow/fused/additive sweep, loft, copy and pattern are rejected. The sweep form also excludes continuation, later lifecycle and shell/extent/sketch-host consumers except the stated pair. This is not family completion. |
| `SWEPT_EDGE` | `1511` | CADFS FeatureScript 1511 exported query history; direct-prism `BRepPrimAPI_MakePrism.Generated(vertex)`, direct full-revolve `BRepPrimAPI_MakeRevol.Generated(vertex)`, and direct-PipeShell `Generated(profile_vertex)` relations, all verified by final-snapshot membership | Either (a) the existing independent, undrafted, `new_body` blind-prism source vertex -> generated vertical edge contract, (b) an independent `FULL` `revolve_add` `new_body` from the original non-IMPRINT source profile and same-sketch line axis -> one active swept circular edge, or (c) an immediate fillet/chamfer on an independent `new_body` direct PipeShell sweep: two adjacent source profile edges -> one source vertex -> one initial swept edge | Every form requires exact direct source-edge anchors with one shared endpoint, complete active final-membership relations, and no geometry/stable-ID/current-body fallback. The prism form retains its analytic-hole boundary; the revolve form registers only exact `Generated(vertex)` edge handles and permits initial `boundary` plus only complete/proven one-to-one `continuation`. The sweep form requires a complete unique direct analytic contour, an exact three-item OSD of two adjacent profile edges plus one single `sQuery` direct path edge, and only initial boundary consumption; its adapter records only final-solid `Generated(vertex)` edges. `00330012` four-edge/one-line source-form resolves and rebuilds; a non-adjacent pair is deferred. A fresh all-corpus scan found 12 native sweep SWEPT_EDGE histories but none meets this strict producer/path/consumer contract, so it is not real-corpus coverage. `00025622` F2 and `00048326` F3 rebuild strict through the full-revolve path; `00407186` F2 resolves initially, while its later F3 continuation is correctly rejected as non-unique. IMPRINT/materialized 1511 revolve profiles, split/merge/deletion, partial/surface/additive/fused revolve, axis/profile from different sources, draft/multiple/two-sided prism, boolean/SPLIT successors, sweep profiles with circle/inner/split forms, segmented/spatial/hollow/fused/additive paths, sweep continuation, loft, copy and pattern are rejected. Existing prism evidence remains `00000715` F3, `00007264` F2 (comparison-rejected), `00039669` F2 (strict/RP), and `00151159` F2 (RP). This is not family completion. |
| `SWEPT_EDGE` | `2491` | CADFS `00404735` F1/F3 exported `onshape/std/geometry.fs@2491.0` history; `BRepPrimAPI_MakeRevol.Generated(vertex)` verified against the final solid snapshot | A full, independent `revolve_add` `new_body` with a direct profile or an explicitly verified complete unchanged materialization, and a direct in-sketch axis: a uniquely identified non-construction source vertex, expressed by its complete incident source-edge set -> one active swept circular edge | The lowerer requires `FULL`, exact source/library tuple, the profile contract above, a producer still present in the history, at least two distinct direct source edges with exactly one shared endpoint, and no geometry/stable-ID/current-body fallback. The adapter registers only `Generated(vertex)` edge handles which pass final-result membership; resolver permits the initial `boundary` and subsequently only complete/proven one-to-one `continuation` relations. In `00404735`, F3 (`E0`/`E5`) lowers with `kernel_history` evidence and rebuilds; F2 remains deferred because its OSD pair does not identify one direct shared endpoint. Partial/surface/additive/fused revolve, changed/incomplete/split materialization, axis/profile from different sources, axis endpoints, single/non-unique source, unsupported library revisions, split/merge/deletion, and all generic revolve/SWEPT_EDGE forms remain rejected. This is a single 2491 producer contract, not general revolve or family completion. |
| `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. CAP input recovery reads the unique complete/proven builder-role fact from its producer delta, never a cached output role on a later continuation; `00789417` therefore resolves F12's F10-snapshot section edge, while F11's missing active edge continuation correctly rejects its later consumer as `selector_body_member_inactive`. 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)
@@ -29,10 +225,29 @@ geometry while lowering a `sweep` path. It therefore creates no
| 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. |
| `qUnion([qBodyType(qCreatedBy(sketch, EDGE), WIRE)])` with optional exact `qConstructionFilter(..., 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. `qCreatedBy(..., EDGE)` excludes source sketch points. 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. | A singleton non-construction `line` / `bspline` retains the existing contract. One direct source sketch may provide a multi-curve result, or an outer union may contain two or more exact versioned source-wire operands. All resulting `line` / `arc` / non-periodic `bspline` curves must form one source-ordered, connected, non-branching open wire with no repeated source entity; two-point B-splines require both endpoint derivatives. For cross-sketch operands, each curve is captured through its explicit source workplane to a global self-contained spatial `path.segments` variant, with no common-workplane projection, datum/result/final-STEP/current-body lookup, selector intent, kernel lineage, CAP output-role, or stable-ID fallback. A `NO` filter removes construction curves; without it any construction curve rejects the contract; `skPoint` is not an `EDGE`. Closed/disconnected/branching/repeated/degenerate paths, circles/ellipses/unknown curves, non-exact wrappers/filters, other versions, loft guides, surface/profile consumers, and runtime-body uses remain deferred. | The fresh seven-sample RP matrix `output/qbodytype-direct-sketch-wire-matrix-20260910-v1` rebuilds all singleton 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. `00885126` F2 lowers `E0 -> E2.filletArc -> E1`, executes one solid sweep, and preserves its prefix STEP in `/private/tmp/cadfs-segmented-wire-00885126-20260910`; `00786708` executes F2/F5/F7 then is `rebuilt_rejected`. Cross-sketch `00610979` F4 captures filtered F1/F3 source wires as four global segments and rebuilds one solid with no runtime diagnostic in `output/cadfs-spatial-wire-00610979-20260910`; complete-history comparison is rejected because later CAP_FACE-dependent F5/F9 remain deferred and source has four solids. These are execution classifications, not similarity claims. |
| `qUnion([qConstructionFilter(qBodyType(qCreatedBy(sketch, EDGE), WIRE), NO)])` as `loft` `ToolBodyType.SURFACE` wire profiles | `1511` + direct `onshape/std/geometry.fs@1511.0` | Same source-only query semantics; `00174697` supplies two independent circular source wires on parallel explicit planes. | Exactly two distinct source sketches, each with one direct closed non-construction wire (one circle or a connected non-branching line/arc/B-spline cycle), may lower to `loft_surface`. The adapter uses `BRepOffsetAPI_ThruSections(False, False)` and registers an independent shell; it never fuses it into the active solid or grants topology/provenance selector continuation. Spine, guides, connections, matching, endpoint derivatives, sheet profiles, non-`NEW` operation, mixed/derived/runtime wires, construction ambiguity, open/disconnected/branched/inner wires and other library versions are deferred with `loft_surface_wire_profiles`. | `00174697` lowers and freshly rebuilds to one shell (`surface_count: 1`, `solid_count: 0`). This is one direct surface-loft form, not general `qBodyType`, construction filter, loft, or surface-selector coverage. |
| `qBodyType(qCreatedBy(sourceSketch, EDGE), WIRE)` as a `LINE_ANGLE` datum axis | `1511` + direct `onshape/std/geometry.fs@1511.0` | Same source query semantics, with `cplane.fs::lineAnglePlane` consuming the selected source axis rather than runtime model topology | The exact query result must contain one `line` from one source sketch. The line's explicit source workplane supplies the global axis; construction is valid for datum geometry. The bridge rejects multi-line, arc/B-spline, filters/composition, derived topology, unknown versions, and runtime body selection. It emits a CDSL frame, not a selector intent or lineage claim. | `00506444` F6 selects one construction line from each of F4/F5, lowers one reference plane, and allows F8 to execute in a fresh rebuild. `00474220` F4 selects an arc wire and remains a deterministic `line-angle reference selection is unsupported` defer. This is one real 1511 positive form, so it does not complete `qBodyType`, construction filtering, or general datum-axis coverage. |
| Direct-prism `CAP_VERTEX` as a `THREE_POINT` / `PLANE_POINT` datum point | `1511` + direct `onshape/std/geometry.fs@1511.0` | A direct `makeQuery(..., CAP_VERTEX, VERTEX, {disambiguationData: [OSD([edge, edge])], isStart})` names one source-defined physical point, rather than a live selector result | The owner must be an undrafted independent `new_body` blind prism whose lowered profile exactly equals its original direct source profile. OSD must contain exactly two distinct non-construction profile edges from that source sketch with one shared endpoint. The lowerer maps this endpoint into the selected prism cap frame; it allows datum, independent undrafted `new_body` blind prisms, and one exact producer `CAP_FACE` single-cap shell after the producer because CAP_VERTEX remains a producer-history datum, never a shell-result vertex. It emits an explicit cPlane frame, no selector intent or topology lookup. The common analytic-contour region solver also requires a hole loop to lie strictly inside its outer loop at every sampled boundary point; shared/touching loops remain independent regions instead of creating an invalid self-hole. Hole/boolean/other dress-up/transform/COPY/pattern/delete mutations, draft/ADD/CUT/two-sided, IMPRINT/split/multi-profile, ambiguous/repeated endpoint source, other versions, geometry/current-body/stable-ID and STEP fallback are rejected. | `00243142` and `00245768` F2/F4 complete replay in fresh `output/cplane-cap-vertex-datum-20260912-r2`; both final comparisons are rejected (`00245768` also has a solid-count mismatch). `00212904` F2 direct CAP shell followed by F3 THREE_POINT also lowers; both requested and removed cap positions remain producer-history datum points. `00053942` F4 follows a hole and is rejected as `CAP_VERTEX datum source is unsupported`. This is a source-datum bridge only, not CAP_VERTEX selector, generic cPlane, or query-family completion. |
| `PLANE_POINT` with direct-prism `CAP_FACE` and direct source vertex | `1511` | The FeatureScript query AST retains `FACE` and `VERTEX` kinds independently of whether either query uses `qCreatedBy`; a direct blind-prism CAP frame supplies the physical face frame | The cPlane must have exactly two entities: one face and one vertex. The lowerer takes the face frame and locates the direct source vertex in its explicit source sketch frame; it does not reinterpret either query as a runtime topology selector. Multiple faces, multiple vertices, untyped/extra inputs, and unresolved source frames defer. | `00228556` F3 uses F1 `CAP_FACE` plus F2 `sQuery(..., VERTEX, ...)`; F1/F3/F6 execute in `output/cplane-plane-point-cap-face-20260912`, with RP pass and strict volume/area precision diagnostics. No runtime body, stable-ID, geometric-nearness, or STEP fallback is used. General topology faces/vertices, query composition and generic PLANE_POINT semantics remain deferred. |
| Direct-prism `SWEPT_EDGE` as a `LINE_ANGLE` datum axis | `1511` + direct `onshape/std/geometry.fs@1511.0` | A direct source-profile vertex, identified by exactly two distinct original profile edges with one shared endpoint, plus the one-sided prism span | The producer must be the immediately preceding undrafted `new_body` blind `extrude_add_blind`; its lowered profile must exactly equal the complete original source profile. The datum axis is calculated from the source vertex copied to the prism start cap and the explicit start-to-end span. It is not a runtime selector, topology lookup, stable-ID, geometry, active-body, or STEP fallback. Derived/combined edge queries, non-immediate producers, draft, ADD/CUT/two-sided/IMPRINT or changed profiles, ambiguous/repeated/non-shared OSD edges, other versions, and a derived face/curve as the second LINE_ANGLE reference remain deferred. | `00040198` F2 (single axis) and `00722278` F2 (axis plus Front datum) lower to executable reference planes and complete fresh rebuilds. `00644299` F2 deliberately remains deferred because its second reference is a derived `SWEPT_FACE`; this narrow datum bridge does not implement general derived cPlane semantics. |
## Deferred source queries
### Remaining Deferred Tuples
The following source families are registered capability work items, not runtime
allow-list entries. They intentionally remain `feature_script_query` with a
non-executable `multiplicity: "none"` policy until their source contract,
complete final-snapshot builder history, consumer semantics, and multi-corpus
evidence are all present.
| Query family | Observed corpus | Required contract before enabling | Current status |
| --- | --- | --- | --- |
| `BLEND_EDGE` remaining tuples | 382 queries in 183 CADFS histories, including `00407186` F5 and `00614954` F3 | Explicit N:M source-set, producer/lifecycle and final-snapshot incidence contracts for `SWEPT_*`, `MERGE`, `SPLIT`, `COPY`, `BLEND_FACE`, non-immediate consumers and dress-up variants | The direct-prism CAP_EDGE/CAP_FACE tuple is registered above. All other tuples remain non-executable `feature_script_query` selectors; no geometry, stable-ID, current-body or source-STEP fallback is authorized. |
| `MERGE(FACE)` sketch host | 294 direct outer sketch workplanes, including 261 FeatureScript 1511 hosts; 90 have two SWEPT_FACE inputs and 77 have one explicit OSD edge per input | A source-to-body-member merge operation model, complete/proven operation-wide N:M final-face relations, explicit active-member scope and an attached-sketch consumer contract | No MERGE form is executable. An outer MERGE must not inherit a nested CAP_FACE or SWEPT_FACE static frame: `00013930` F7, `00020631` F4 and `00036155` F5 now stop at a named `sketch_deferred` diagnostic while preserving preceding executable features. `NewBodyOperationType.ADD` correctly takes CDSL's fuse path, but its `*.boolean.opBoolean` owner token, aggregate, or generic union history cannot by itself authorize a particular N:M source-face successor. Geometry, face order, stable ID, aggregate/current body and source STEP are forbidden fallbacks. |
| `MID_CAP_EDGE` | 49 queries in 23 CADFS histories: 1511 loft/sweep positions 0--6 and 1549 loft positions 0--1, including `00330207`, `00309311`, `00315819`, and `00703441` | Exact source/library semantics for `capPos`, an operation-wide source-section/edge relation, and a final-snapshot edge handle for every selected section; the consumer must then prove cardinality and active body membership | OCP `BRepOffsetAPI_ThruSections` 7.9.3.1 exposes only no-argument `FirstShape`/`LastShape`; for the default smooth loft used by every observed MID history, `Generated(source_edge)` returns a side face and source/intermediate section edges are absent from the final snapshot. `ruled:true` preserves section edges, but no observed MID history requests that distinct loft semantic. Therefore an index, face-boundary traversal, geometric coincidence, stable ID, current body, or source STEP cannot stand in for `capPos`; all MID queries remain non-executable `feature_script_query` selectors. |
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
@@ -42,6 +257,36 @@ 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.
`COPY(FACE)` does not inherit a nested `CAP_FACE` workplane frame. A separate
`primary_cut_cap_face_workplane` contract admits only a 1511 immediate default
primary `extrude_cut_blind`, same-owner direct `COPY(CAP_FACE)`, complete unchanged
source-profile OSD, and one following sketch consumer. The runtime proves one
transient direct-prism cap role and its same-owner complete/proven `subtract`
continuation to one active planar face, then derives the workplane from that exact
face's native UV orientation and support plane before resolving the local profile
or hole locations. `00573124` F3--F5 and `00951631` F3--F5 rebuild by
`copy_lineage`; both fresh strict/RP comparisons are rejected, so this is execution
evidence only. `00252794` F8 remains deferred because F5/F6/F7 mutate the body
after F3. Source frames, stable/snapshot IDs, geometry, current body, source STEP,
nonplanar/ambiguous/partial successors, `COPY(SWEPT_FACE|BODY)`, patterns,
transforms, draft/two-sided/non-direct tools, opposite-direction attached holes,
other versions and non-immediate lifecycle are not fallbacks or covered forms.
`COPY(SWEPT_FACE)` has a separate `primary_cut_swept_face_workplane` form. It
requires 1511, one same-owner immediate default undrafted blind primary cut, and
one direct original source-profile edge. The runtime follows only the exact
source-edge anchor to one complete/proven transient prism side face, then one
same-owner complete/proven subtract continuation to a single active planar face;
the native face supplies the workplane. `00321940` F3--F5 rebuilds with
`copy_lineage`, but fresh strict/RP comparison is rejected; `00171671` lowers its F4 attachment before an unrelated F5
selector remains deferred. `00326645` is a real rejection because its side face
has no unique subtract continuation. Two-sided/IMPRINT/partial/non-direct or
multi-edge profiles, non-immediate, nonplanar, split/merge/inactive successors,
body/pattern/transform COPY, other versions, static source frames, stable IDs,
geometry, current body, and source STEP remain outside this contract. A fresh
9,347-history lowering pass materializes 12 such attachments; that count is
contract-shape coverage only, not runtime or comparison acceptance.
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`
+156
View File
@@ -0,0 +1,156 @@
# CADFS → CDSL Engine 周报
**周期:2026-09-07 2026-09-11**
**数据集:** `data/cadfs-sample/CADFS_test`,共 9,347 个样本
**最新全量工件:** `cadfs_to_cdsl/output`,报告生成于 `2026-09-11 18:28:01`
**历史工件:** `cadfs_to_cdsl/output-history/*`
**能力依据:** `cadfs_to_cdsl/ENGINE_CAPABILITY_GAPS_PROGRESS.local.md``CADFS_FULL_CAPABILITY_TARGET.md`
## 1. 本周结论
1. **CADFS → CDSL 的转换覆盖继续提升。** 最新全量结果中,`converted_complete` 为 6,626 个(70.89%),`converted_partial` 为 2,325 个(24.87%);无可执行特征的样本降至 366 个(3.92%),解析/降级失败 30 个(0.32%)。
2. **转换完整不等于可执行,也不等于几何相似。** 最新结果有 8,126 个 STEP 工件,但其中包含失败样本保留的可执行前缀;真正完成一次 rebuild 的样本为 4,597 个,其中 RP 接受 1,781 个、strict 接受 744 个。
3. **最新快照的 RP/strict 接受数低于 2026-09-09 快照。** 这与本周 selector provenance、single-session replay 和 fallback 收紧同步发生;`selector_query_unsupported` 成为主要 rebuild 失败层。该变化不能只按百分比判定为几何能力退化,应结合失败 feature、前缀 STEP 和诊断逐层分析。
4. **本周 engine 的主要增量集中在 provenance、拓扑历史和有界 source contract。** 已落地的多数是窄范围、可证明的 contract;没有把单个样本的通过结果升级为通用 CADFS feature 完成。
## 2. 当前 CADFS → CDSL 数据转换情况
### 2.1 最新全量结果
| 层级 | 状态 | 数量 | 占 9,347 样本 | 说明 |
| --- | --- | ---: | ---: | --- |
| 转换 | `converted_complete` | 6,626 | 70.89% | FeatureScript history 全部有 CDSL 表达,仍需单独看 runtime/comparison |
| 转换 | `converted_partial` | 2,325 | 24.87% | 保留可转换前缀,后续 feature 有 deferred/unsupported 等诊断 |
| 转换 | `deferred_no_executable_feature` | 366 | 3.92% | 没有可执行 feature checkpoint |
| 转换 | `parse_failed` | 30 | 0.32% | parser/lowering 层失败 |
| rebuild/比较 | `rebuilt_strict` | 744 | 7.96% | CDSL、STEP rebuild 和 strict comparison 均通过 |
| rebuild/比较 | `rebuilt_approximate` | 1,037 | 11.09% | RP 工程相似通过,strict 未通过 |
| rebuild/比较 | `rebuilt_rejected` | 2,778 | 29.72% | 有 rebuild/comparison,但不满足 RP 验收 |
| 执行 | `rebuild_failed` | 3,932 | 42.07% | runtime/selector/OCC 在完整或增量 replay 中失败 |
| 执行 | `runtime_ineligible` | 408 | 4.37% | CDSL 已产生,但能力预检拒绝完整 runtime execution |
| 基础设施 | `comparison_timeout` | 38 | 0.41% | 比较 worker 超过 60 秒 |
| 基础设施 | `rebuild_timeout` | 14 | 0.15% | rebuild 超时 |
接受口径:RP 接受 = `rebuilt_strict + rebuilt_approximate` = **1,78119.05%**strict 接受 = **7447.96%**。当前 4,559 个样本有 comparison report,其中 strict 占 16.32%RP 占 39.07%;这个分母与全量样本不同,不能混用。
### 2.2 CDSL、STEP、比较工件覆盖
| 工件 | 数量 | 占全量 | 位置/说明 |
| --- | ---: | ---: | --- |
| candidate CDSL | 8,951 | 95.76% | `output/samples/<sample_id>/candidate.cdsl.json` |
| bound CDSL | 8,485 | 90.78% | 已完成 selector/body binding 的候选 |
| STEP | 8,126 | 86.94% | 包含完整 rebuild 和失败时保留的最佳 executable prefix |
| comparison | 4,559 | 48.77% | `comparison.json``comparison_summary.csv` |
| GLB | 0(全量目录扫描) | 0% | 最新全量 `output` 未生成 GLB;专项回归工件中的 GLB 不计入全量统计 |
所有样本的本地 annotation、FeatureScript、image、STEP、STL modality 均存在;JSONL 对齐 fallback 为 0。每个失败样本仍按当前流程保留 `diagnostics.json``history.json``rebuild.json` 以及可用的 `rebuild.step` 前缀。
### 2.3 历史快照对比
以下数据按各快照的 `full_run_report.md` 汇总。旧快照的 manifest 状态命名曾使用 `rebuilt`/`rebuilt_approximate` 等不同字段,因此不把旧 manifest 与最新 manifest 直接拼接;比较时以各自报告的 conversion、RP、strict 和工件计数为准。
| 报告时间(快照目录) | complete | partial | deferred | RP accepted | strict accepted | STEP | candidate | bound | comparison |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 2026-09-02 (`20260907-185128`) | 2,427 | 4,315 | 2,602 | 1,15912.40% | 5786.18% | 1,812 | 8,108 | 1,773 | 1,719 |
| 2026-09-07 (`20260907-235209`) | 2,829 | 4,283 | 2,235 | 1,35814.53% | 6547.00% | 5,554 | 7,112 | 5,554 | 5,250 |
| 2026-09-08 (`20260908-215959`) | 5,367 | 3,266 | 693 | 2,03121.73% | 9059.68% | 6,134 | 8,633 | 6,134 | 6,090 |
| 2026-09-09 (`20260909-191531`) | 6,232 | 2,710 | 396 | 2,25324.10% | 98910.58% | 8,176 | 8,942 | 8,364 | 6,007 |
| 2026-09-11(最新 `output` | **6,626** | **2,325** | **366** | **1,78119.05%** | **7447.96%** | **8,126** | **8,951** | **8,485** | **4,559** |
从 2026-09-02 到最新快照,完整转换增加 4,199 个(+44.92 个百分点),deferred 减少 2,236 个(-23.92 个百分点),candidate/bound CDSL 覆盖明显扩大。最新快照相较 2026-09-09 的 strict/RP 和 comparison 数下降,应归因到当前代码的更严格 provenance/selector gate、runtime 分类和本次全量执行边界,不能仅凭接受率得出“本周 engine 几何能力整体下降”的结论。
与 2026-09-09 快照逐项比较:`converted_complete` **+394**、`converted_partial` **-385**、deferred **-30**、candidate CDSL **+9**、bound CDSL **+121**STEP **-50**、comparison **-1,448**、RP accepted **-472**、strict accepted **-245**。后四项反映本次全量执行的 replay/比较覆盖和 provenance gate 变化,需与保留的 prefix STEP、diagnostics 和 comparison 工件一起解读。
### 2.4 FeatureScript 操作覆盖与主要缺口
最新 source history 中的操作出现次数如下,说明当前回归集已覆盖较宽的 operation 分布:
| 操作 | 次数 | 操作 | 次数 |
| --- | ---: | --- | ---: |
| `newSketch` | 20,857 | `extrude` | 18,168 |
| `fillet` | 5,210 | `revolve` | 2,087 |
| `chamfer` | 1,818 | `cPlane` | 1,620 |
| `hole` | 1,203 | `shell` | 729 |
| `mirror` | 483 | `transform` | 468 |
| `sweep` | 378 | `loft` | 377 |
| `circularPattern` | 252 | `booleanBodies` | 228 |
| `deleteBodies` | 142 | | |
按受影响样本统计的主要 capability gaps(一个样本可能同时命中多个 gap)为:
| Gap | 受影响样本 | Gap | 受影响样本 |
| --- | ---: | --- | ---: |
| `fillet` | 928 | `extrude` | 888 |
| `chamfer` | 264 | `revolve` | 201 |
| `shell_face_selector` | 162 | `mirror` | 139 |
| `extrude_profile_topology:cap_face` | 112 | `extrude_profile_topology:swept_face` | 94 |
| `cPlane` | 92 | `extrude_profile_topology:cap_edge` | 88 |
| `sweep_path` | 71 | `hole_location_vertex` | 70 |
| `loft` | 65 | `circularPattern` | 50 |
| `hole_scope_body_source` | 44 | `shell` | 42 |
| `sweep` | 40 | `booleanBodies` | 36 |
当前共记录 5,095 条 diagnostic record,分布为 `feature_deferred` 3,449、`unsupported_engine_capability` 1,004、`sketch_deferred` 612、`parse_or_lowering_failed` 30;这些是诊断条数,不是互斥样本数。主要 runtime 失败包括:`selector_query_unsupported`F2 904、F4 593、F3 380、F6 278、F5 204)、OCC union invalid shape157)、`BRep_API: command not done`151)和 planar IMPRINT region 无界(116)。
## 3. 本周 engine 新增和完善的能力
### 3.1 Selector source preservation 与 query 语义
| 能力 | 本周新增/完善 | 已验证边界与证据 | 仍未覆盖 |
| --- | --- | --- | --- |
| 版本化 query AST | `selector_intent` 同时保留带行号的原始 `source_query.ast``query_expr@1.0`;递归保存 `qUnion``qIntersection``qSubtraction``qAdjacent``qOwnerBody``qBodyType``qConstructionFilter``qCreatedBy` 以及未知调用,未知语义保留为 `opaque_call`。schema/semantic validation 校验 expression 与 intent version 一致。 | parser/lowering/selector/runtime 回归已覆盖;未知 query 不再被改写成近似 selector。 | 这是 source-preservation foundation,不等于这些 query family 已可 runtime 执行;通用 filter、created/modified/generated/deleted 关系仍 deferred。 |
| Provenance query set | `fillet/chamfer` 增加窄 `QUERY_SET` contractdirect `qUnion``qIntersection``qSubtraction` 通过 exact active record ID 做 union/intersection/subtraction,递归 set 在 9 月 11 日补齐;空集、partial、inactive、mixed kind/version、geometry/stable-ID/output-role 混用均拒绝。 | `backend.tests.test_selector_provenance_contract` 35 passed`cadfs_to_cdsl.tests.test_lowering` 117 passed`backend.tests.test_engine_runtime_foundation` 140 passed、1 skipped。`00000715` 的变体证明 intersection 空集诊断和 subtraction left-only 语义。 | 全量 corpus 未发现 native `qIntersection`/`qSubtraction` call;仍仅限 FeatureScript 1511、direct provenance child、fillet/chamfer,不能称通用 query-set 能力。shell 对 nested/intersection/subtraction 仍拒绝。 |
| `qOwnerBody` 精确 owner bridge | 为 `UP_TO_BODY` 增加递归 `query_input``owner_body_contract: exact_input_owner`;先解析唯一 proven topology child,再以完全相同 body ID 投影到唯一 active body record。 | 合成 `00694309` 变体完成 rebuilt38 项 selector provenance 测试及 lowering 联跑 156 passed。 | 全量 corpus 暂无 native `qOwnerBody`;不支持多 member、multi-solid、later successor、其它 consumer 或 current aggregate/geometry fallback。 |
| fallback 与 replay 收紧 | `selector_intent.version` 成为唯一 canonical versionproduction `rebuild_candidate` 改为 single-session incremental replay;普通 context selector 只在 active context 中存在唯一同 kind record 时回退,COPY/instance selector 不回退。 | 失败统一保留 selector diagnostic 和 prefix checkpoint`selector_query_unsupported``selector_kernel_history_missing``selector_body_member_inactive` 分层可归因。 | 仍缺通用 CAP/SWEPT/OFFSET/COPY/INTERSECT lineage,不能以 stable ID、最终 STEP 或 current body 补齐 source 语义。 |
### 3.2 OCC topology history、output role 与 body provenance
| 能力 | 本周新增/完善 | 已验证边界与证据 | 仍未覆盖 |
| --- | --- | --- | --- |
| 多 builder final-history bridge | planar IMPRINT 多区域 blind prism 先分别保留 `BRepPrimAPI_MakePrism` history,再用 `BRepAlgoAPI_Fuse(SetToFillHistory)` 映射到 final snapshot;只登记 `IsSame` 可证明的 source→final relationdeleted/missing branch 保留 `partial/unknown`。 | `00354246` 的 7 个 `SWEPT_FACE` 由 native lineage 解析后在 OCC fillet feasibility 处失败;`00403485` 的 deleted side branch 正确报告 `selector_kernel_history_missing` 并保留 F1 STEP。最终聚合回归为 293 passed、1 skipped。 | 仅限 planar IMPRINT、至少两个 bounded region、无 draft、单向 blind;一般 fuse N:M、draft/trim/multi-body、boolean/COPY/pattern 后继仍未完成。 |
| output-role / all-fragments | direct prism/sweep/loft/shell 的 start/end cap、side、swept edge、offset/closing/wall 等 role 继续以 operation history 保存;`all_fragments` 要求每个 source fragment 都有 complete/proven final relation,不再返回部分集合冒充成功。 | 既有 direct CAP_FACE/CAP_EDGE/SWEPT_FACE/SWEPT_EDGE、shell、loft 回归均按 final snapshot 和 active member 绑定。 | 一般 CAP/SWEPT/OFFSET/MID_CAP、split/merge、复杂 dress-up 后继仍无完整 N:M resolver。 |
| body member / COPY provenance | boolean target/tool 现在接受 `{pattern_feature_id, source_feature_id, instance_index}`direct surviving `new_body` 的 mirror/circular/transform COPY、single-body successor、fused sole-body circular COPY 和 multi-source transform copy 均增加显式 member/provenance 校验。 | `00000385` F6、`00293508` UNION 及 body graph/runtime 回归证明 target/tool 不再回退到 pattern aggregate;专项 GLB 由同一 rebuild STEP 生成,仅作预览。 | 第二个 `new_body`、aggregate/linear/nested pattern、excluded instance、multi-body boolean/delete/copy 的完整生命周期仍未完成;不能宣称完整 `booleanBodies`/`circularPattern`/`mirror`。 |
### 3.3 CADFS feature/source contract 扩展
| 能力 | 本周新增/完善 | 已验证边界与证据 | 仍未覆盖 |
| --- | --- | --- | --- |
| source-only segmented sweep path | 同一 source sketch 的 ordered line/arc/non-periodic B-spline 多段路径,及跨 source sketch 的 global 3D spatial path;要求 explicit source identity、connected/open/non-branching、无重复/退化段,runtime 用 `BRepBuilderAPI_MakeWire` + pipe-shell。circular pattern 会同步旋转 spatial points/normal/tangent。 | `00376556` 为 executable approximate`00786708` 的 filtered line/arc/line path 可执行但完整 history rejected`00610979` 跨 sketch F4 prefix 可执行;`00034285` 的空消息 `AssertionError` 被稳定翻译为 `OCC sweep operation raised while building the native sweep`。 | 仍是 source-only lowering,不是 runtime `qBodyType` selectorclosed/branched wire、guide/surface/profile consumer、sweep output lineage、一般 multi-sketch path 未完成。 |
| planar IMPRINT dispatch | 修正多 face `IMPRINT FACE` lowering 的 dispatch,避免把合法多区域 profile 错降成普通 analytic contour;保留 source entities、face side 和 fragment intent。 | `00354246` F1 现在保持 typed `planar_imprint`F2 的 selector request 不再丢失,失败停在可归因 selector/OCC 层并保留 F1 STEP。 | 不增加 IMPRINT/SWEPT/INTERSECT family 的通用 success coverageunbounded region、different sketch、trim/copy/pattern successor 仍拒绝。 |
| direct prism topology consumers | 扩展 direct `CAP_FACE` immediate `UP_TO_SURFACE``SWEPT_FACE` continuation、`SWEPT_BODY` immediate `UP_TO_BODY``CAP_EDGE` fillet/chamfer、CAP_FACE shell removal,以及 direct hole profile 的 `SWEPT_EDGE`/`CAP_EDGE` exact source anchors。 | `00835610` drafted CAP_FACE pipeline RP 通过;`00694309` body target contract 有完整 history`00039669``00151159` 等 direct prism hole lineage matrix 有 strict/RP 证据;未证明的 selector 继续拒绝。 | 复杂 profile、draft 内环、fused/multi-region、boolean/COPY/pattern 后继和通用 CAP/SWEPT selector 仍不在 allow-list。 |
| reference/semantic details | 完善 `LINE_ANGLE` direct `skCircle` axis 与 source gate、`cPlane` `oppositeDirection` signed offset、direct sketch-vertex hole location/sole scoped body、shell `OFFSET_FACE`/`parts` source-qualified contract 和 `INTERSECT EDGE` source-qualified section relation/transient tool history。 | 相关原子、selector/runtime suite 和受控 core/shard 回归已记录;`INTERSECT` 未证明的 SectionEdges 不会直接升级为 lineage。 | 任意曲线/面/connector axis、multi-body hole host、generic OFFSET、复杂 shell、primary/copy/pattern intersection 仍未完成。 |
### 3.4 诊断和离线工具完善
- native sweep 的空 `AssertionError` 现在转成稳定、可归因的 OCC 诊断,不以 geometry fallback 伪造实体。
- single-session selector replay 和 prefix retention 使失败点、最后可执行 STEP、`history.json`/`rebuild.json`/`diagnostics.json` 保持一致;当前全量报告中的 F2/F4/F3 selector failures 可直接定位到 feature。
- engine 执行边界继续收敛到显式 schema/capability contract、按 family 的 executor/runtime 路径和 topology evidence export;这使 selector、body lifecycle 和 OCC failure 可以在同一条 replay history 上归因,而不是依赖旧的平行 runtime 路径。
- 新增 `selector_candidate_demo.py` 的 geometry probe、strict replay、bounded branch search 和 query-group recovery。它只在隔离的 copied CDSL 上实验,并要求唯一 strict branch 后再 fresh replay**不改变 production resolver、RP 阈值或 capability matrix,因此不计为 engine selector 能力完成。**
## 4. 本周能力边界与下周重点
### 已能对外说明的结果
- 转换器可以为大多数样本生成 candidate/bound CDSL,并在失败时保留 prefix STEP;完整转换率已达到 70.89%。
- selector source AST、版本和 body/provenance policy 不再被静默抹平;一部分 direct prism、IMPRINT、sweep、shell、boolean/pattern/transform 场景能够以 exact kernel history 执行。
- RP 与 strict comparison 已独立报告;source STEP 精度、拓扑不一致、比较超时和 OCC/runtime failure 不再混为 converter 失败。
### 不能宣称已经完成的范围
- 不能把 `fillet``extrude``chamfer``revolve``shell``mirror``sweep``booleanBodies``circularPattern` 的单个窄 contract 说成整个 feature family 完成。
- 不能把 2026-09-10 的 query-group strict winner、专项 GLB 或单个 RP/strict 样本当作通用 selector mapping。
- 当前最大未覆盖面仍是 selector query unsupported、复杂 profile/拓扑后继、body lifecycle、OCC invalid shape/unbounded IMPRINT 以及 comparison timeout;这些都需要继续保持有界拒绝和可执行前缀。
### 建议下周优先级
1. 先处理全量 runtime 中占比最高的 `selector_query_unsupported`,按 query family、producer history 和 consumer 分层推进,不用 geometry/current-body fallback 换取通过。
2. 扩展 planar IMPRINT、CAP/SWEPT/OFFSET 和 boolean/pattern 的多实体 relation component,至少补齐多个真实 source/lifecycle 的 RP 回归后再升级 capability matrix。
3. 针对 OCC union invalid shape、`BRep_API: command not done`、unbounded IMPRINT 和 comparison timeout 分别建立 kernel/input/infrastructure 证据,继续保留每个失败样本的 STEP/diagnostic/compare 工件。
## 5. 证据入口
- 最新汇总:`cadfs_to_cdsl/output/full_run_report.md``summary.json``manifest.jsonl``capability_gaps.json``comparison_summary.csv`
- 最新样本工件:`cadfs_to_cdsl/output/samples/<sample_id>/`
- 历史汇总:`cadfs_to_cdsl/output-history/<snapshot>/full_run_report.md`
- 能力进度台账(本地,不提交):`cadfs_to_cdsl/ENGINE_CAPABILITY_GAPS_PROGRESS.local.md`
- 全量能力目标:`cadfs_to_cdsl/CADFS_FULL_CAPABILITY_TARGET.md`
@@ -0,0 +1,50 @@
# CADFS → CDSL 周报(2026-09-11
本周完成 9,347 个 CADFS 样本处理,CDSL 完整转换 6,626 个(70.89%),RP/strict 通过 1,781/744 个,失败样本均保留 STEP 前缀。相比 9 月 7 日首次全量归档,候选 CDSL、可执行 STEP 与 RP/strict 接受数均提升;本周新增/完善集中在 selector 血缘、拓扑历史、body/COPY 与受限特征 contract,复杂 selector、多 body 生命周期仍待补齐。
完善绘图引擎,CADFS 样本处理。
## 转换指标
| 指标 | 本次 | 首次归档(2026-09-07 | 变化 |
|---|---:|---:|---:|
| 生成候选 CDSL | 8,951 | 7,112 | +1,839 |
| 完整转换 CDSL | 6,62670.89% | 2,82930.27% | +3,797+40.62 pp |
| 生成可执行 STEP | 8,126 | 5,554 | +2,572 |
| RP 通过 | 1,78119.05% | 1,35814.53% | +423+4.52 pp |
| 严格通过 | 7447.96% | 6547.00% | +90+0.96 pp |
注:几何比对数下降受本次 replay/比较覆盖影响;RP 和 strict 接受数相较首次归档均提升。
## 重要改动与新增
| 类别 | 能力/指标 | 首次(2026-09-07 | 本次 | 变化 | 结论 |
|---|---|---|---|---|---|
| Selector 源语义 | `selector_intent` / `query_expr@1.0` | 不保留递归 query AST | 保留 set、filter、未知调用 | 新增 contract | 不再将未知 selector 静默近似化 |
| 集合/owner 查询 | `QUERY_SET``qOwnerBody` | 无对应 bridge | exact active record-ID 集合、`UP_TO_BODY` owner bridge | 新增 | 仅 FS1511 direct provenance`qOwnerBody` 仅合成回归 |
| Replay 与回退 | 单次 session、context fallback、前缀保留 | selector 回放/归因较弱 | incremental replayCOPY/instance 不回退 | 收紧 | 失败层与最后 STEP checkpoint 可追溯 |
| 直接 prism 血缘 | CAP/SWEPT/EDGE/BODY consumer | 基础输出 role | extent、fillet/chamfer、shell、hole 等 direct consumer | 扩展 | 仅 direct-prism allow-list,不覆盖复杂后继 |
| IMPRINT 拓扑历史 | 多 face/multi-region | 缺 final relation | 多区域 prism + fuse final-history bridge | 新增/修复 | deleted/missing fragment 保持拒绝 |
| Sweep 路径 | 单一受限路径 | 覆盖有限 | 同/跨草图 line、arc、B-spline spatial wire | 扩展 | 仅 source-only、open/non-branching path |
| Shell contract | face/offset/body source | 主路径有限 | SWEPT_FACE、OFFSET_FACE、parts、方向语义 | 扩展 | 复杂/多 body shell 未完成 |
| Body/COPY/Boolean | aggregate/current body 风险 | 来源校验不足 | pattern instance、sole-body copy、boolean target/tool provenance | 收紧 | 多 body/嵌套 pattern 生命周期未完成 |
| Datum/Hole/Section | 细分 source contract 缺失 | 覆盖有限 | `LINE_ANGLE``cPlane`、vertex-hole、`INTERSECT EDGE` | 扩展 | 仅 direct source/sole scoped-body |
| 运行时诊断 | OCC/selector 失败归因 | 信息不稳定 | sweep 异常规范化、strict replay 与 prefix 工件 | 完善 | 离线 query 搜索仅作诊断,不计生产能力 |
| 测试/能力台账 | 回归边界分散 | 证据不完整 | selector/runtime/lowering 与真实 shard 回归补齐 | 完善 | 仍按各能力的受限边界验收 |
### 主要缺口收敛(同名 capability gap 直接比较)
| 能力缺口 | 首次 | 本次 | 变化 |
|---|---:|---:|---:|
| `extrude` | 3,600 | 888 | -2,712 |
| `fillet` | 1,904 | 928 | -976 |
| `shell` | 696 | 42 | -654 |
| `revolve` | 662 | 201 | -461 |
| `extrude_profile_topology:intersect` | 383 | 34 | -349 |
| `chamfer` | 588 | 264 | -324 |
| `sweep` | 326 | 40 | -286 |
| `transform` | 252 | 13 | -239 |
| `booleanBodies` | 187 | 36 | -151 |
| `mirror` | 241 | 139 | -102 |
| `cPlane` | 168 | 92 | -76 |
注:gap 为受影响样本数,同一样本可命中多个 gap;只比较两次快照中同名键。`hole``shell_face_selector``sweep_path` 等已拆分为新子项,不能直接与首次单项相减。
+21 -2
View File
@@ -49,6 +49,12 @@ class Parser:
def primary(self) -> Any:
token = self.pop()
if token.value in {"+", "-"}:
# FeatureScript permits a signed parenthesized scalar such as
# ``-(138.6) / 2 * mm``. Keep it in the existing arithmetic AST
# so every downstream constant/units validator sees the same
# expression shape as a binary subtraction.
return Call("__binary__", [0.0, token.value, self.primary()], token.line)
if token.value == "(":
value = self.expression(); self.accept(")"); return value
if token.kind == "string": return _string(token.value)
@@ -151,6 +157,20 @@ def _arg_map(call: Call) -> dict[str, Any]:
return next((arg for arg in reversed(call.args) if isinstance(arg, dict)), {})
def _feature_id(call: Call) -> str | None:
"""Return the declared ID for one direct FeatureScript feature call.
FeatureScript operations share the ``operation(context, id + "F...",
definition)`` shape. Retaining this generic boundary makes an unknown
source operation visible to the capability registry and lowering instead
of silently omitting it because its name is absent from a parser list.
"""
if len(call.args) < 2 or call.args[0] != "context":
return None
feature_id = symbolic_string(call.args[1])
return feature_id if feature_id.startswith("F") else None
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)
@@ -175,8 +195,7 @@ def parse_featurescript(source: str, sample_id: str = "unknown") -> ModelIR:
if model.sketches:
args = _arg_map(call); eid = str(call.args[1]) if len(call.args) > 1 else f"E{len(model.sketches[-1].entities)}"
model.sketches[-1].entities.append(FeatureIR(eid, call.name, args, line_start=call.line, raw_source=call.name))
elif call.name in {"extrude", "revolve", "fillet", "chamfer", "hole", "linearPattern", "mirror", "cPlane", "referenceAxis", "shell", "loft", "sweep", "circularPattern", "booleanBodies", "deleteBodies", "transform", "draft", "thicken", "split", "moveFace", "replaceFace", "deleteFace", "derive"}:
fid = symbolic_string(call.args[1]) if len(call.args) > 1 else f"feature_{len(model.features)}"
elif (fid := _feature_id(call)) is not None:
feature_ir = FeatureIR(fid, call.name, _arg_map(call), line_start=call.line, raw_source=call.name)
model.features.append(feature_ir); model.steps.append(feature_ir)
return model
+5287 -242
View File
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
"""Source-derived FeatureScript operation coverage inventory.
The registry records what the current source corpus actually contains. It is
deliberately independent from conversion diagnostics: an old output directory
cannot make an operation appear supported, and a planned operation cannot be
reported as observed merely because its name is listed in the roadmap.
"""
from __future__ import annotations
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any, Iterable
from .dataset import Sample
from .featurescript_parser import parse_featurescript
REGISTRY_SCHEMA = "cadfs_to_cdsl.operation_registry.v1"
# These are roadmap work items, not parser aliases or support declarations.
P3_OPERATION_WORK_ITEMS = (
"draft",
"thicken",
"split",
"moveFace",
"deleteFace",
"replaceFace",
"transform",
"derive",
"import",
"bend_add",
)
def build_operation_registry(samples: Iterable[Sample]) -> dict[str, Any]:
"""Parse every available FeatureScript source and inventory feature calls.
Parse failures and missing FeatureScript modalities are recorded separately
instead of being treated as a zero-count result. Sample examples remain
bounded only for report size; the count is computed from every feature.
"""
counts: Counter[str] = Counter()
sample_ids: dict[str, set[str]] = defaultdict(set)
parse_failures: list[dict[str, str]] = []
source_sample_count = 0
for sample in samples:
source_path = sample.files.get("featurescript")
if not source_path:
continue
source_sample_count += 1
try:
model = parse_featurescript(Path(source_path).read_text(encoding="utf-8"), sample.sample_id)
except Exception as error:
parse_failures.append({"sample_id": sample.sample_id, "error": f"{type(error).__name__}: {error}"})
continue
for feature in model.features:
counts[feature.operation] += 1
sample_ids[feature.operation].add(sample.sample_id)
operations: dict[str, dict[str, Any]] = {}
for operation in sorted(counts):
operations[operation] = {
"state": "observed",
"feature_count": counts[operation],
"sample_count": len(sample_ids[operation]),
"sample_ids": sorted(sample_ids[operation]),
}
for operation in P3_OPERATION_WORK_ITEMS:
if operation in operations:
operations[operation]["roadmap_work_item"] = True
else:
operations[operation] = {
"state": "not_observed_in_current_source",
"feature_count": 0,
"sample_count": 0,
"sample_ids": [],
"roadmap_work_item": True,
}
return {
"schema": REGISTRY_SCHEMA,
"source_sample_count": source_sample_count,
"parse_failure_count": len(parse_failures),
"parse_failures": parse_failures,
"operations": operations,
}
+120 -18
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import hashlib, multiprocessing, random
from copy import deepcopy
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any
@@ -8,6 +9,7 @@ from .compare import compare_steps
from .dataset import Sample, scan_dataset
from .featurescript_parser import parse_featurescript
from .lowering import lower_model
from .operation_registry import build_operation_registry
from .rebuild import rebuild_candidate
from .reports import generate_reports, read_json, write_json, write_manifest
@@ -22,9 +24,34 @@ def _fingerprint(sample: Sample) -> str:
def _sample_dir(output: Path, sample_id: str) -> Path: return output / "samples" / sample_id
def _semantic_valid_prefix(
cdsl: dict[str, Any],
validate_semantic_cdsl: Any,
) -> tuple[dict[str, Any], dict[str, Any]] | None:
"""Return the longest contiguous semantically valid feature prefix.
This is an artifact-recovery boundary, not permission to execute an
invalid candidate. The selected prefix is independently validated and
saved under an explicit prefix filename; the complete rejected CDSL stays
diagnostic-only.
"""
features = cdsl.get("features") or []
for count in range(len(features) - 1, 0, -1):
prefix = deepcopy(cdsl)
prefix["features"] = prefix["features"][:count]
try:
semantic = validate_semantic_cdsl(prefix)
except Exception:
continue
return prefix, semantic
return None
def scan(input_root: Path, output: Path) -> list[dict[str, Any]]:
records = [sample.as_dict() for sample in scan_dataset(input_root)]
samples = scan_dataset(input_root)
records = [sample.as_dict() for sample in samples]
write_json(output / "dataset_index.json", {"schema": "cadfs_to_cdsl.dataset_index.v1", "input": str(input_root), "sample_count": len(records), "records": records})
write_json(output / "operation_registry.json", build_operation_registry(samples))
initial = [{"sample_id": item["sample_id"], "status": "scanned", "diagnostics": item["diagnostics"]} for item in records]
write_manifest(output / "manifest.jsonl", initial)
return records
@@ -56,26 +83,91 @@ def convert_one(sample: Sample, output: Path, *, force: bool = False) -> dict[st
if cached.get("input_fingerprint") == fingerprint and cached.get("conversion_status"):
return cached
if force:
for name in ("candidate.cdsl.json", "bound.cdsl.json", "rebuild.step", "rebuild.json", "rebuild.worker.json", "comparison.json", "comparison.worker.json"):
for name in (
"candidate.cdsl.json", "candidate.invalid.cdsl.json", "candidate.prefix.cdsl.json",
"bound.cdsl.json", "prefix.bound.cdsl.json", "rebuild.step", "prefix.rebuild.step",
"rebuild.json", "prefix.rebuild.json", "rebuild.worker.json", "comparison.json",
"comparison.worker.json",
):
(directory / name).unlink(missing_ok=True)
diagnostics = list(sample.diagnostics)
try:
feature_path = Path(sample.files["featurescript"])
model = parse_featurescript(feature_path.read_text(encoding="utf-8"), sample.sample_id)
provenance = {"source_featurescript": str(feature_path), **{f"source_{key}_sha256": value for key, value in sample.hashes.items()}, "jsonl": sample.metadata}
result = lower_model(model, provenance); diagnostics.extend(result.diagnostics)
write_json(directory / "history.json", result.history); write_json(directory / "diagnostics.json", diagnostics)
if result.cdsl is not None:
from engine.cdsl_engine.semantic_validation import validate_semantic_cdsl
semantic = validate_semantic_cdsl(result.cdsl); write_json(directory / "candidate.cdsl.json", result.cdsl)
else: semantic = None
status = {"schema": "cadfs_to_cdsl.status.v1", "sample_id": sample.sample_id, "status": result.status, "conversion_status": result.status, "input_fingerprint": fingerprint, "semantic_validation": semantic, "diagnostic_count": len(diagnostics)}
except Exception as exc:
missing = isinstance(exc, FileNotFoundError)
diagnostics.append({"code": "source_missing" if missing else "parse_or_lowering_failed", "message": str(exc), "type": type(exc).__name__})
diagnostics.append({"code": "source_missing" if missing else "parse_failed", "message": str(exc), "type": type(exc).__name__})
write_json(directory / "diagnostics.json", diagnostics)
final_status = "source_missing" if missing else "parse_failed"
status = {"schema": "cadfs_to_cdsl.status.v1", "sample_id": sample.sample_id, "status": final_status, "conversion_status": final_status, "input_fingerprint": fingerprint, "diagnostic_count": len(diagnostics)}
write_json(status_path, status); return status
provenance = {
"source_featurescript": str(feature_path),
**{f"source_{key}_sha256": value for key, value in sample.hashes.items()},
"jsonl": sample.metadata,
}
try:
result = lower_model(model, provenance)
except Exception as exc:
diagnostics.append({"code": "lowering_failed", "message": str(exc), "type": type(exc).__name__})
write_json(directory / "diagnostics.json", diagnostics)
status = {
"schema": "cadfs_to_cdsl.status.v1", "sample_id": sample.sample_id,
"status": "lowering_failed", "conversion_status": "lowering_failed",
"input_fingerprint": fingerprint, "diagnostic_count": len(diagnostics),
}
write_json(status_path, status); return status
diagnostics.extend(result.diagnostics)
write_json(directory / "history.json", result.history)
if result.cdsl is None:
write_json(directory / "diagnostics.json", diagnostics)
status = {
"schema": "cadfs_to_cdsl.status.v1", "sample_id": sample.sample_id,
"status": result.status, "conversion_status": result.status,
"input_fingerprint": fingerprint, "semantic_validation": None, "diagnostic_count": len(diagnostics),
}
write_json(status_path, status); return status
try:
from engine.cdsl_engine.semantic_validation import validate_semantic_cdsl
semantic = validate_semantic_cdsl(result.cdsl)
except Exception as exc:
diagnostics.append({"code": "semantic_validation_failed", "message": str(exc), "type": type(exc).__name__})
write_json(directory / "candidate.invalid.cdsl.json", result.cdsl)
prefix_result = _semantic_valid_prefix(result.cdsl, validate_semantic_cdsl)
prefix_status: dict[str, Any] | None = None
if prefix_result is not None:
prefix, prefix_semantic = prefix_result
prefix_features = prefix.get("features") or []
write_json(directory / "candidate.prefix.cdsl.json", prefix)
prefix_status = {
"feature_count": len(prefix_features),
"last_feature_id": prefix_features[-1].get("id"),
"semantic_validation": prefix_semantic,
}
diagnostics.append({
"code": "semantic_valid_prefix_preserved",
"feature_count": len(prefix_features),
"last_feature_id": prefix_features[-1].get("id"),
"message": "longest contiguous semantic-valid CDSL prefix was preserved separately",
})
write_json(directory / "diagnostics.json", diagnostics)
status = {
"schema": "cadfs_to_cdsl.status.v1", "sample_id": sample.sample_id,
"status": "semantic_validation_failed", "conversion_status": "semantic_validation_failed",
"input_fingerprint": fingerprint,
"semantic_validation": {"valid": False, "error": {"type": type(exc).__name__, "message": str(exc)}},
"diagnostic_count": len(diagnostics),
}
if prefix_status is not None:
status["semantic_valid_prefix"] = prefix_status
write_json(status_path, status); return status
write_json(directory / "candidate.cdsl.json", result.cdsl)
write_json(directory / "diagnostics.json", diagnostics)
status = {
"schema": "cadfs_to_cdsl.status.v1", "sample_id": sample.sample_id,
"status": result.status, "conversion_status": result.status,
"input_fingerprint": fingerprint, "semantic_validation": semantic, "diagnostic_count": len(diagnostics),
}
write_json(status_path, status); return status
@@ -108,12 +200,19 @@ def rebuild_one(sample: Sample, output: Path, *, force: bool = False, timeout_se
# must be rebuilt instead of being hidden behind the conversion label.
# ``rebuild_candidate`` keeps the two outcomes distinct by reporting a
# runtime-ineligible candidate when no executable body can be produced.
if not (directory / "candidate.cdsl.json").exists(): return status
rebuild_path = directory / "rebuild.json"; step_path = directory / "rebuild.step"
if not force and rebuild_path.exists() and status.get("rebuild_status"):
if status.get("rebuild_status") != "rebuilt" or step_path.exists(): return status
candidate_path = directory / "candidate.cdsl.json"
is_prefix = False
if not candidate_path.exists():
candidate_path = directory / "candidate.prefix.cdsl.json"
is_prefix = candidate_path.exists()
if not candidate_path.exists(): return status
rebuild_path = directory / ("prefix.rebuild.json" if is_prefix else "rebuild.json")
step_path = directory / ("prefix.rebuild.step" if is_prefix else "rebuild.step")
status_key = "prefix_rebuild_status" if is_prefix else "rebuild_status"
if not force and rebuild_path.exists() and status.get(status_key):
if status.get(status_key) != "rebuilt" or step_path.exists(): return status
worker_result = directory / "rebuild.worker.json"
outcome = _isolated(_rebuild_worker, (str(directory / "candidate.cdsl.json"), str(step_path), str(worker_result)), worker_result, timeout_seconds)
outcome = _isolated(_rebuild_worker, (str(candidate_path), str(step_path), str(worker_result)), worker_result, timeout_seconds)
if outcome == "completed":
result = read_json(worker_result); worker_result.unlink(missing_ok=True)
bound_cdsl = result.pop("bound_cdsl", None)
@@ -123,14 +222,17 @@ def rebuild_one(sample: Sample, output: Path, *, force: bool = False, timeout_se
if bound_cdsl is None and prefix_bound_cdsl is not None:
bound_cdsl = prefix_bound_cdsl
prefix["bound_feature_count"] = prefix.get("feature_count")
if bound_cdsl is not None: write_json(directory / "bound.cdsl.json", bound_cdsl)
if bound_cdsl is not None:
write_json(directory / ("prefix.bound.cdsl.json" if is_prefix else "bound.cdsl.json"), bound_cdsl)
else:
step_path.unlink(missing_ok=True)
error_type = "TimeoutError" if outcome == "timeout" else "WorkerProcessError"
message = f"rebuild exceeded {timeout_seconds:g} seconds" if outcome == "timeout" else "rebuild worker exited without a result"
result = {"status": "rebuild_timeout" if outcome == "timeout" else "rebuild_failed", "error": {"type": error_type, "message": message}}
write_json(rebuild_path, result)
status["rebuild_status"] = result["status"]; status["status"] = result["status"]
status[status_key] = result["status"]
if not is_prefix:
status["status"] = result["status"]
write_json(status_path, status); return status
+99 -2
View File
@@ -54,17 +54,109 @@ def query_ast(value: Any) -> dict[str, Any] | list[Any] | str | float | bool | N
return str(value)
_SET_COMBINATORS = {
"qUnion": "union",
"qIntersection": "intersection",
"qSubtraction": "subtraction",
}
_FILTERS = {
"qAdjacent": "adjacent",
"qOwnerBody": "owner_body",
"qBodyType": "body_type",
"qConstructionFilter": "construction",
}
def _query_expression_node(value: Any) -> dict[str, Any]:
"""Return a typed, lossless-enough representation of a source query.
``source_query.ast`` remains the source-level record, including parser line
numbers. This second representation makes set boundaries and filters
explicit so a later resolver can interpret them without re-parsing
FeatureScript text. Unknown calls stay opaque instead of being flattened
into a nearby supported topology query.
"""
if isinstance(value, Call):
name = value.name
args = value.args
if name in _SET_COMBINATORS:
operands = args[0] if len(args) == 1 and isinstance(args[0], list) else args
return {
"node": "set",
"operator": _SET_COMBINATORS[name],
"operands": [_query_expression_node(item) for item in operands],
}
if name in _FILTERS:
return {
"node": "filter",
"filter": _FILTERS[name],
"input": _query_expression_node(args[0]) if args else {"node": "literal", "value": None},
"arguments": [_query_expression_node(item) for item in args[1:]],
}
if name == "makeQuery":
return {
"node": "topology_query",
"owner": _query_expression_node(args[0]) if len(args) > 0 else {"node": "literal", "value": None},
"topology_type": _query_expression_node(args[1]) if len(args) > 1 else {"node": "literal", "value": None},
"entity_type": _query_expression_node(args[2]) if len(args) > 2 else {"node": "literal", "value": None},
"arguments": [_query_expression_node(item) for item in args[3:]],
}
if name == "qCreatedBy":
return {
"node": "created_by",
"owner": _query_expression_node(args[0]) if args else {"node": "literal", "value": None},
"arguments": [_query_expression_node(item) for item in args[1:]],
}
if name in {"sQuery", "sketchEntityQuery"}:
return {
"node": "source_entity",
"sketch": _query_expression_node(args[0]) if len(args) > 0 else {"node": "literal", "value": None},
"entity_type": _query_expression_node(args[1]) if len(args) > 1 else {"node": "literal", "value": None},
"entity": _query_expression_node(args[2]) if len(args) > 2 else {"node": "literal", "value": None},
"arguments": [_query_expression_node(item) for item in args[3:]],
}
if name == "qSketchRegion":
return {
"node": "sketch_region",
"sketch": _query_expression_node(args[0]) if args else {"node": "literal", "value": None},
"arguments": [_query_expression_node(item) for item in args[1:]],
}
return {
"node": "opaque_call",
"name": name,
"arguments": [_query_expression_node(item) for item in args],
}
if isinstance(value, list):
return {"node": "list", "items": [_query_expression_node(item) for item in value]}
if isinstance(value, dict):
return {
"node": "map",
"entries": [
{"key": str(key), "value": _query_expression_node(item)}
for key, item in value.items()
],
}
if value is None or isinstance(value, (str, float, bool, int)):
return {"node": "literal", "value": value}
return {"node": "literal", "value": str(value)}
def query_expr(value: Any) -> dict[str, Any]:
"""Produce the versioned CDSL query-expression contract for one query."""
return {"version": "1.0", "root": _query_expression_node(value)}
def parse_query(value: Any) -> QueryInfo:
info = QueryInfo(ast=query_ast(value))
for call in walk_calls(value):
info.calls.append(call.name)
if call.name in {"qUnion", "qIntersection", "qSubtraction", "qAdjacent"}:
if call.name in {"qUnion", "qIntersection", "qSubtraction"}:
info.query_combinators.append(call.name)
if call.name in {"qBodyType", "qOwnerBody"}:
info.body_scope.append(call.name)
if call.name in {"TDD", "trueDependencyDisambiguation"}:
info.disambiguation.append(call.name)
if call.name in {"qBodyType", "qOwnerBody", "qAdjacent"}:
if call.name in {"qBodyType", "qOwnerBody", "qAdjacent", "qConstructionFilter"}:
info.filters.append(call.name)
if call.name in {"makeQuery", "qCreatedBy"} and call.args:
owner = symbolic_string(call.args[0])
@@ -76,6 +168,11 @@ def parse_query(value: Any) -> QueryInfo:
definition = next((arg for arg in call.args if isinstance(arg, dict)), {})
if isinstance(definition.get("isStart"), str): info.is_start = definition["isStart"].lower() == "true"
elif "isStart" in definition: info.is_start = bool(definition["isStart"])
elif call.name == "qCreatedBy" and len(call.args) > 1 and info.topology_type is None:
# qCreatedBy is itself a typed FeatureScript query. Keep its
# requested kind so consumers can distinguish a datum plane
# from a point without inspecting feature IDs or geometry.
info.kind = str(call.args[1]).lower()
if call.name in {"sQuery", "sketchEntityQuery"} and len(call.args) >= 3:
sketch = symbolic_string(call.args[0]); info.source_sketch = sketch.split(".", 1)[0]
if info.topology_type is None: info.kind = str(call.args[1]).lower()
+41 -1
View File
@@ -49,6 +49,42 @@ def _failed_feature_id(error: Exception) -> str | None:
return match.group(1) if match else None
def _last_preflight_executable_prefix(
cdsl: dict[str, Any], analysis: Any, output: Path,
) -> dict[str, Any] | None:
"""Persist the contiguous executable prefix rejected by later preflight."""
feature_results = list(getattr(analysis, "feature_results", ()) or ())
first_blocked = next(
(index for index, result in enumerate(feature_results) if not result.executable),
None,
)
if first_blocked in (None, 0):
return None
features = list(cdsl.get("features") or [])
if first_blocked > len(features):
return None
prefix = deepcopy(cdsl)
prefix["features"] = features[:first_blocked]
try:
prefix_result = rebuild_candidate(prefix, output)
except Exception:
return None
if prefix_result.get("status") != "rebuilt":
nested_prefix = prefix_result.get("last_executable_prefix")
return nested_prefix if isinstance(nested_prefix, dict) else None
result = prefix_result.get("result")
if not isinstance(result, dict):
return None
return {
"failed_feature_id": feature_results[first_blocked].feature_id,
"feature_count": first_blocked,
"last_feature_id": features[first_blocked - 1].get("id"),
"bound_cdsl": prefix_result.get("bound_cdsl", prefix),
"result": result,
}
def rebuild_candidate(cdsl: dict[str, Any], output: Path) -> dict[str, Any]:
from engine.cdsl_engine.runtime import analyze_cdsl, finalize_cdsl_execution
from .selector_binding import bind_and_execute_candidate_selectors
@@ -56,7 +92,11 @@ def rebuild_candidate(cdsl: dict[str, Any], output: Path) -> dict[str, Any]:
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}
result = {"status": "runtime_ineligible", "analysis": analysis_dict}
prefix = _last_preflight_executable_prefix(cdsl, analysis, output)
if prefix is not None:
result["last_executable_prefix"] = prefix
return result
try:
replay = bind_and_execute_candidate_selectors(cdsl)
result = finalize_cdsl_execution(replay.execution, output)
+11 -6
View File
@@ -190,10 +190,14 @@ def generate_markdown_report(
key=lambda item: (-item[1], item[0]),
)
unsupported_ops = {
"shell", "sweep", "draft", "thicken", "split", "booleanBodies", "circularPattern",
"moveFace", "replaceFace", "deleteFace", "import", "derive",
}
operation_registry = _read_optional_json(output / "operation_registry.json") or {}
registry_operations = operation_registry.get("operations") or {}
planned_unobserved_operations = sorted(
name for name, value in registry_operations.items()
if isinstance(value, dict)
and value.get("roadmap_work_item") is True
and value.get("state") == "not_observed_in_current_source"
)
exact_mappings = {
"extrude": "extrude_add_blind / extrude_add_two_sided / extrude_cut_blind",
"loft": "loft_add (simple closed sketch profiles only)",
@@ -312,9 +316,9 @@ def generate_markdown_report(
"",
*_table(["FeatureScript operation", "CDSL atomic policy"], sorted(exact_mappings.items())),
"",
"Known unsupported FeatureScript operations recorded as capability gaps:",
"Roadmap operation work items absent from the scanned FeatureScript source:",
"",
", ".join(sorted(unsupported_ops)),
", ".join(planned_unobserved_operations) if planned_unobserved_operations else "- None recorded; inspect operation_registry.json for observed work items.",
"",
"Engine executor atomic IDs:",
"",
@@ -345,6 +349,7 @@ def generate_markdown_report(
f"- Manifest: `{output / 'manifest.jsonl'}`",
f"- Summary JSON: `{output / 'summary.json'}`",
f"- Capability gaps JSON: `{output / 'capability_gaps.json'}`",
f"- Source operation registry: `{output / 'operation_registry.json'}`",
f"- Unsupported capabilities Markdown: `{output / 'unsupported_capabilities.md'}`",
f"- Comparison CSV: `{output / 'comparison_summary.csv'}`",
"",
+4 -1
View File
@@ -123,7 +123,10 @@ def _selector_roots(feature: dict[str, Any]) -> list[dict[str, Any]]:
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):
# A source-sketch vertex is an immutable extent datum, not a topology
# selector. It has no runtime record to bind; the extent executor
# consumes its validated point directly.
if isinstance(reference, dict) and reference.get("kind") != "source_vertex":
roots.append(reference)
return roots
+699 -54
View File
@@ -13,6 +13,7 @@ from __future__ import annotations
import argparse
from copy import deepcopy
import json
import math
import multiprocessing
from pathlib import Path
from typing import Any
@@ -29,10 +30,19 @@ def strip_provenance_intents(value: Any) -> int:
"""Remove provenance-only selector fields from one copied CDSL value."""
removed = 0
if isinstance(value, dict):
removed_intent = False
for key in ("selector_intent", "selector_intent_version"):
if key in value:
value.pop(key)
removed += 1
removed_intent = True
# Feature output roles are semantic builder evidence, just like a
# selector intent. The geometry-only probe must not leave an output
# role in place after discarding the evidence that validates it.
if removed_intent:
for key in ("output_role", "output_role_source"):
if key in value:
value.pop(key)
for child in value.values():
removed += strip_provenance_intents(child)
elif isinstance(value, list):
@@ -63,7 +73,7 @@ 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:
def _isolated(target: Any, args: tuple[Any, ...], 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)
@@ -134,6 +144,41 @@ def _strict_passed(comparison: dict[str, Any] | None) -> bool:
return bool(isinstance(comparison, dict) and (comparison.get("strict") or {}).get("passed"))
_SEARCH_TOKEN = "_selector_search_token"
def _selector_search_token(feature_id: str, location: str) -> str:
"""Use a structural token while a group expands a public selector list."""
return f"{feature_id}:{location}"
def _annotate_selector_search_tokens(cdsl: dict[str, Any]) -> int:
"""Mark copied diagnostic selectors without creating a durable CDSL field."""
count = 0
for feature in cdsl.get("features") or []:
if not isinstance(feature, dict) or not isinstance(feature.get("id"), str):
continue
for location, selector in _selector_sites(feature):
selector[_SEARCH_TOKEN] = _selector_search_token(feature["id"], location)
count += 1
return count
def _strip_selector_search_tokens(value: Any) -> int:
"""Remove experiment-only branch tokens before a CDSL is persisted."""
removed = 0
if isinstance(value, dict):
if _SEARCH_TOKEN in value:
value.pop(_SEARCH_TOKEN)
removed += 1
for child in value.values():
removed += _strip_selector_search_tokens(child)
elif isinstance(value, list):
for child in value:
removed += _strip_selector_search_tokens(child)
return removed
def _selector_sites(feature: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
"""Return the public selector positions of one CDSL feature.
@@ -270,6 +315,8 @@ def _emit_strict_selector_record(
*,
rebuild_timeout_seconds: float,
comparison_timeout_seconds: float,
query_groups: list[dict[str, Any]] | None = None,
candidate_replay_kind: str = "geometry_probe",
) -> dict[str, Any]:
"""Create and independently verify a compiled CDSL only after strict proof."""
if not _strict_passed(geometry_comparison):
@@ -286,6 +333,7 @@ def _emit_strict_selector_record(
}
compiled_candidate = deepcopy(bound_cdsl)
_strip_selector_search_tokens(compiled_candidate)
compiled_path = directory / "selector_record.compiled.cdsl.json"
write_json(compiled_path, compiled_candidate)
compiled = _run_rebuild(
@@ -322,20 +370,29 @@ def _emit_strict_selector_record(
result["reason"] = "no_featurescript_selector_binding"
return result
record_path = directory / "selector-record.json"
write_json(record_path, {
verification = {
"compiled_cdsl_strict": True,
"compiled_cdsl": str(compiled_path),
}
if candidate_replay_kind == "geometry_probe":
verification["geometry_probe_strict"] = True
else:
verification["selector_search_branch_strict"] = True
record_payload: dict[str, Any] = {
"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),
},
"verification": verification,
"records": records,
})
}
if query_groups:
# These are the query-to-record *sets* selected by the offline search.
# A FeatureScript query is not necessarily a single OCC record.
record_payload["query_groups"] = deepcopy(query_groups)
write_json(record_path, record_payload)
result.update({
"status": "strict_replayed",
"record_count": len(records),
@@ -359,41 +416,58 @@ def _searchable_selector_sites(feature: dict[str, Any]) -> list[tuple[str, dict[
]
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."""
def _replace_selector_group(
cdsl: dict[str, Any],
feature_id: str,
token: str,
selectors: list[dict[str, Any]],
) -> None:
"""Expand one selector root to an explicit branch-local record set."""
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)
roots = feature.get("selectors") or []
index = next(
(
item_index
for item_index, selector in enumerate(roots)
if isinstance(selector, dict) and selector.get(_SEARCH_TOKEN) == token
),
None,
)
if index is not None:
roots[index:index + 1] = [deepcopy(selector) for selector in selectors]
feature["selectors"] = roots
return
prefix = "params."
suffix = ".reference"
if location.startswith(prefix) and location.endswith(suffix):
name = location[len(prefix):-len(suffix)]
for name in ("end_condition", "reverse_end_condition"):
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}")
reference = condition.get("reference") if isinstance(condition, dict) else None
if isinstance(reference, dict) and reference.get(_SEARCH_TOKEN) == token:
if len(selectors) != 1:
raise ValueError("search group cannot expand an extent reference")
condition["reference"] = deepcopy(selectors[0])
return
raise ValueError(f"search branch selector token is unavailable: {feature_id}:{token}")
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."""
def _forced_candidate_selectors(placeholder: dict[str, Any], candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Turn a query's runtime record set into explicit branch-local roots."""
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
selectors: list[dict[str, Any]] = []
for candidate in candidates:
selector, _bound = _bound_selector(placeholder, [candidate])
if not isinstance(selector, dict):
raise ValueError("search branch produced an invalid explicit selector")
if placeholder.get(_SEARCH_TOKEN) is not None:
selector[_SEARCH_TOKEN] = placeholder[_SEARCH_TOKEN]
selectors.append(selector)
if not selectors:
raise ValueError("search group has no runtime records")
return selectors
def _resolve_search_selector(placeholder: dict[str, Any], *, registry: Any, active_body_id: str | None) -> Any:
@@ -432,12 +506,552 @@ def _search_candidate_records(
return [deepcopy(item) for item in candidates[:maximum]], len(candidates)
def _search_replay_worker(candidate: str, step: str, result: str, maximum_candidates: int) -> None:
def _vector(value: Any) -> list[float] | None:
if not isinstance(value, (list, tuple)) or len(value) != 3:
return None
try:
result = [float(component) for component in value]
except (TypeError, ValueError):
return None
return result if all(math.isfinite(component) for component in result) else None
def _sub(left: list[float], right: list[float]) -> list[float]:
return [left[index] - right[index] for index in range(3)]
def _dot(left: list[float], right: list[float]) -> float:
return sum(left[index] * right[index] for index in range(3))
def _cross(left: list[float], right: list[float]) -> list[float]:
return [
left[1] * right[2] - left[2] * right[1],
left[2] * right[0] - left[0] * right[2],
left[0] * right[1] - left[1] * right[0],
]
def _norm(value: list[float]) -> float:
return math.sqrt(_dot(value, value))
def _unit(value: list[float] | None) -> list[float] | None:
if value is None or len(value) != 3:
return None
length = _norm(value)
if length <= 1e-9:
return None
return [component / length for component in value]
def _distance(left: list[float], right: list[float]) -> float:
return _norm(_sub(left, right))
def _score_distance(distance_mm: float, *, tolerance_mm: float = 0.05) -> float:
return max(0.0, 1.0 - distance_mm / tolerance_mm)
def _source_selector_map(provenance_candidate: dict[str, Any]) -> dict[str, dict[str, Any]]:
"""Index source selectors by an experiment-only structural token."""
selectors: dict[str, dict[str, Any]] = {}
for feature in provenance_candidate.get("features") or []:
if not isinstance(feature, dict) or not isinstance(feature.get("id"), str):
continue
for location, selector in _selector_sites(feature):
if isinstance(selector.get("selector_intent"), dict):
selectors[_selector_search_token(feature["id"], location)] = deepcopy(selector)
return selectors
def _source_feature(provenance_candidate: dict[str, Any], feature_id: Any) -> dict[str, Any] | None:
if not isinstance(feature_id, str):
return None
return next(
(
feature
for feature in provenance_candidate.get("features") or []
if isinstance(feature, dict) and feature.get("id") == feature_id
),
None,
)
def _source_sketch(provenance_candidate: dict[str, Any], producer: dict[str, Any], source_sketch_id: str | None) -> dict[str, Any] | None:
sketches = (provenance_candidate.get("geometry") or {}).get("sketches") or []
sketch_id = producer.get("sketch_id")
if isinstance(sketch_id, str):
sketch = next((item for item in sketches if isinstance(item, dict) and item.get("id") == sketch_id), None)
if isinstance(sketch, dict):
return sketch
if isinstance(source_sketch_id, str):
sketch = next(
(
item
for item in sketches
if isinstance(item, dict) and item.get("source_sketch_id") == source_sketch_id
),
None,
)
if isinstance(sketch, dict):
return sketch
return None
def _source_curve(sketch: dict[str, Any], entity_id: str) -> dict[str, Any] | None:
profile = sketch.get("profile") or {}
for entry in profile.get("source_entities") or []:
if isinstance(entry, dict) and entry.get("id") == entity_id and isinstance(entry.get("curve"), dict):
return entry["curve"]
for contour in profile.get("contours") or []:
if not isinstance(contour, dict):
continue
for segment in contour.get("segments") or []:
if isinstance(segment, dict) and segment.get("source_entity_id") == entity_id:
return segment
for segment in profile.get("construction") or []:
if isinstance(segment, dict) and segment.get("source_entity_id") == entity_id:
return segment
return None
def _producer_frame(
provenance_candidate: dict[str, Any],
source_selector: dict[str, Any],
) -> tuple[dict[str, Any], dict[str, Any], list[float], float] | None:
"""Recover only the direct sketch/extrude frame needed for candidate rank."""
intent = source_selector.get("selector_intent") or {}
source_entity = intent.get("source_entity") if isinstance(intent.get("source_entity"), dict) else None
source_entities = intent.get("source_entities") or []
source_sketch_id = (
source_entity.get("sketch_id")
if isinstance(source_entity, dict)
else next(
(
item.get("sketch_id")
for item in source_entities
if isinstance(item, dict) and isinstance(item.get("sketch_id"), str)
),
None,
)
)
producer = _source_feature(provenance_candidate, source_selector.get("owner_feature_id"))
if producer is None or not str(producer.get("atomic_id") or "").startswith("extrude_"):
return None
sketch = _source_sketch(provenance_candidate, producer, source_sketch_id)
if sketch is None:
return None
plane = sketch.get("workplane") or {}
origin = _vector(plane.get("origin_mm"))
normal = _unit(_vector(plane.get("normal")) or [])
if origin is None or normal is None:
return None
params = producer.get("params") or {}
try:
span = abs(float(params["distance_mm"]))
except (KeyError, TypeError, ValueError):
return None
if span <= 1e-9:
return None
if bool(params.get("reverse") or params.get("opposite_direction")):
normal = [-component for component in normal]
return producer, sketch, normal, span
def _world_point(sketch: dict[str, Any], point: Any) -> list[float] | None:
local = point if isinstance(point, (list, tuple)) and len(point) in {2, 3} else None
if local is None:
return None
try:
coordinates = [float(value) for value in local]
except (TypeError, ValueError):
return None
plane = sketch.get("workplane") or {}
origin = _vector(plane.get("origin_mm"))
x_axis = _unit(_vector(plane.get("x_dir")) or [])
normal = _unit(_vector(plane.get("normal")) or [])
if origin is None or x_axis is None or normal is None:
return None
y_axis = _unit(_cross(normal, x_axis))
if y_axis is None:
return None
z = coordinates[2] if len(coordinates) == 3 else 0.0
return [
origin[index] + x_axis[index] * coordinates[0] + y_axis[index] * coordinates[1] + normal[index] * z
for index in range(3)
]
def _source_entity_ids(source_selector: dict[str, Any]) -> list[str]:
intent = source_selector.get("selector_intent") or {}
entity = intent.get("source_entity")
if isinstance(entity, dict) and isinstance(entity.get("entity_id"), str):
return [entity["entity_id"]]
return sorted({
item["entity_id"]
for item in intent.get("source_entities") or []
if isinstance(item, dict) and isinstance(item.get("entity_id"), str)
})
def _cap_role_hint(source_selector: dict[str, Any]) -> str | None:
intent = source_selector.get("selector_intent") or {}
role = source_selector.get("output_role") or intent.get("output_role")
if isinstance(role, str) and role in {"extrude.start", "extrude.end"}:
return role.rsplit(".", 1)[1]
ast = ((intent.get("source_query") or {}).get("ast") or {})
args = ast.get("args") if isinstance(ast, dict) else None
options = args[3] if isinstance(args, list) and len(args) >= 4 and isinstance(args[3], dict) else {}
is_start = options.get("isStart")
if is_start in {True, "true"}:
return "start"
if is_start in {False, "false"}:
return "end"
return None
def _query_group_descriptor(token: str, source_selector: dict[str, Any]) -> dict[str, Any]:
intent = source_selector.get("selector_intent") or {}
policy = intent.get("derivation_policy") or {}
return {
"token": token,
"feature_id": token.split(":", 1)[0],
"source_location": token.split(":", 1)[1] if ":" in token else token,
"kind": source_selector.get("kind"),
"owner_feature_id": source_selector.get("owner_feature_id"),
"query_family": intent.get("query_family"),
"multiplicity": policy.get("multiplicity", "one"),
"source_entity_ids": _source_entity_ids(source_selector),
"source_make_query": deepcopy((intent.get("source_query") or {}).get("ast")),
}
def _rank_cap_face(
provenance_candidate: dict[str, Any],
source_selector: dict[str, Any],
candidate: dict[str, Any],
) -> tuple[float, dict[str, Any]] | None:
frame = _producer_frame(provenance_candidate, source_selector)
role = _cap_role_hint(source_selector)
if frame is None or role is None:
return None
_producer, sketch, direction, span = frame
origin = _vector((sketch.get("workplane") or {}).get("origin_mm"))
geometry = candidate.get("geometry") or {}
actual_normal = _unit(_vector(geometry.get("plane_normal") or geometry.get("normal")) or [])
if origin is None or actual_normal is None or geometry.get("surface_type") != "plane":
return None
point = origin if role == "start" else [origin[index] + direction[index] * span for index in range(3)]
expected_offset = _dot(direction, point)
actual_offset = geometry.get("plane_offset_mm")
try:
actual_offset = float(actual_offset)
except (TypeError, ValueError):
return None
alignment = abs(_dot(direction, actual_normal))
signed_offset = actual_offset if _dot(direction, actual_normal) >= 0 else -actual_offset
offset_error = abs(expected_offset - signed_offset)
return (alignment + _score_distance(offset_error)) / 2.0, {
"method": "extrude_cap_plane",
"cap_role": role,
"normal_alignment": round(alignment, 6),
"plane_offset_error_mm": round(offset_error, 9),
}
def _rank_swept_face(
provenance_candidate: dict[str, Any],
source_selector: dict[str, Any],
candidate: dict[str, Any],
) -> tuple[float, dict[str, Any]] | None:
frame = _producer_frame(provenance_candidate, source_selector)
entity_ids = _source_entity_ids(source_selector)
if frame is None or len(entity_ids) != 1:
return None
_producer, sketch, direction, _span = frame
curve = _source_curve(sketch, entity_ids[0])
geometry = candidate.get("geometry") or {}
if not isinstance(curve, dict):
return None
if curve.get("type") == "line":
start, end = _world_point(sketch, curve.get("start")), _world_point(sketch, curve.get("end"))
actual_normal = _unit(_vector(geometry.get("plane_normal") or geometry.get("normal")) or [])
if start is None or end is None or actual_normal is None or geometry.get("surface_type") != "plane":
return None
expected_normal = _unit(_cross(_sub(end, start), direction))
if expected_normal is None:
return None
try:
actual_offset = float(geometry["plane_offset_mm"])
except (KeyError, TypeError, ValueError):
return None
alignment = abs(_dot(expected_normal, actual_normal))
expected_offset = _dot(expected_normal, start)
signed_offset = actual_offset if _dot(expected_normal, actual_normal) >= 0 else -actual_offset
offset_error = abs(expected_offset - signed_offset)
return (alignment + _score_distance(offset_error)) / 2.0, {
"method": "source_line_supporting_plane",
"normal_alignment": round(alignment, 6),
"plane_offset_error_mm": round(offset_error, 9),
}
if curve.get("type") not in {"arc", "circle"}:
return None
center = _world_point(sketch, curve.get("center"))
actual_origin = _vector(geometry.get("axis_origin_mm"))
actual_direction = _unit(_vector(geometry.get("axis_direction")) or [])
try:
radius_error = abs(float(curve["radius_mm"]) - float(geometry["radius_mm"]))
except (KeyError, TypeError, ValueError):
return None
if center is None or actual_origin is None or actual_direction is None or geometry.get("surface_type") != "cylinder":
return None
alignment = abs(_dot(direction, actual_direction))
radial_offset = _norm(_cross(_sub(center, actual_origin), actual_direction))
return (
alignment + _score_distance(radial_offset) + _score_distance(radius_error)
) / 3.0, {
"method": "source_arc_cylinder",
"axis_alignment": round(alignment, 6),
"axis_offset_mm": round(radial_offset, 9),
"radius_error_mm": round(radius_error, 9),
}
def _point2(value: Any) -> tuple[float, float] | None:
if not isinstance(value, (list, tuple)) or len(value) < 2:
return None
try:
point = float(value[0]), float(value[1])
except (TypeError, ValueError):
return None
return point if all(math.isfinite(component) for component in point) else None
def _curve_contains_point(curve: dict[str, Any], point: tuple[float, float], *, tolerance_mm: float = 0.05) -> bool:
curve_type = curve.get("type")
if curve_type == "line":
start, end = _point2(curve.get("start")), _point2(curve.get("end"))
if start is None or end is None:
return False
direction = end[0] - start[0], end[1] - start[1]
length = math.hypot(*direction)
if length <= 1e-9:
return False
offset = point[0] - start[0], point[1] - start[1]
distance = abs(direction[0] * offset[1] - direction[1] * offset[0]) / length
parameter = (direction[0] * offset[0] + direction[1] * offset[1]) / (length * length)
return distance <= tolerance_mm and -tolerance_mm / length <= parameter <= 1.0 + tolerance_mm / length
center = _point2(curve.get("center"))
try:
radius = float(curve["radius_mm"])
except (KeyError, TypeError, ValueError):
return False
if center is None or radius <= 1e-9 or abs(math.dist(center, point) - radius) > tolerance_mm:
return False
if curve_type == "circle":
return True
if curve_type != "arc":
return False
start, end = _point2(curve.get("start")), _point2(curve.get("end"))
if start is None or end is None:
return False
angle = math.atan2(point[1] - center[1], point[0] - center[0])
start_angle = math.atan2(start[1] - center[1], start[0] - center[0])
end_angle = math.atan2(end[1] - center[1], end[0] - center[0])
full_turn = 2.0 * math.pi
if bool(curve.get("clockwise")):
travelled = (start_angle - angle) % full_turn
total = (start_angle - end_angle) % full_turn
else:
travelled = (angle - start_angle) % full_turn
total = (end_angle - start_angle) % full_turn
angular_tolerance = tolerance_mm / radius
return travelled <= total + angular_tolerance
def _curve_intersections_2d(left: dict[str, Any], right: dict[str, Any]) -> list[tuple[float, float]]:
"""Intersect line/arc/circle supports, retaining only source curve spans."""
left_type, right_type = left.get("type"), right.get("type")
points: list[tuple[float, float]] = []
if left_type == "line" and right_type == "line":
left_start, left_end = _point2(left.get("start")), _point2(left.get("end"))
right_start, right_end = _point2(right.get("start")), _point2(right.get("end"))
if None not in (left_start, left_end, right_start, right_end):
left_dx, left_dy = left_end[0] - left_start[0], left_end[1] - left_start[1]
right_dx, right_dy = right_end[0] - right_start[0], right_end[1] - right_start[1]
determinant = left_dx * right_dy - left_dy * right_dx
if abs(determinant) > 1e-9:
offset_x, offset_y = right_start[0] - left_start[0], right_start[1] - left_start[1]
parameter = (offset_x * right_dy - offset_y * right_dx) / determinant
points.append((left_start[0] + parameter * left_dx, left_start[1] + parameter * left_dy))
else:
line, circular = (left, right) if left_type == "line" else (right, left)
if line.get("type") == "line" and circular.get("type") in {"arc", "circle"}:
start, end, center = _point2(line.get("start")), _point2(line.get("end")), _point2(circular.get("center"))
try:
radius = float(circular["radius_mm"])
except (KeyError, TypeError, ValueError):
radius = 0.0
if start is not None and end is not None and center is not None and radius > 1e-9:
dx, dy = end[0] - start[0], end[1] - start[1]
ox, oy = start[0] - center[0], start[1] - center[1]
quadratic_a = dx * dx + dy * dy
quadratic_b = 2.0 * (dx * ox + dy * oy)
quadratic_c = ox * ox + oy * oy - radius * radius
discriminant = quadratic_b * quadratic_b - 4.0 * quadratic_a * quadratic_c
if quadratic_a > 1e-12 and discriminant >= -1e-9:
root = math.sqrt(max(0.0, discriminant))
for parameter in ((-quadratic_b - root) / (2.0 * quadratic_a), (-quadratic_b + root) / (2.0 * quadratic_a)):
points.append((start[0] + parameter * dx, start[1] + parameter * dy))
elif left_type in {"arc", "circle"} and right_type in {"arc", "circle"}:
left_center, right_center = _point2(left.get("center")), _point2(right.get("center"))
try:
left_radius, right_radius = float(left["radius_mm"]), float(right["radius_mm"])
except (KeyError, TypeError, ValueError):
left_radius = right_radius = 0.0
if left_center is not None and right_center is not None and left_radius > 1e-9 and right_radius > 1e-9:
dx, dy = right_center[0] - left_center[0], right_center[1] - left_center[1]
center_distance = math.hypot(dx, dy)
if 1e-9 < center_distance <= left_radius + right_radius + 0.05:
along = (left_radius * left_radius - right_radius * right_radius + center_distance * center_distance) / (2.0 * center_distance)
height_squared = left_radius * left_radius - along * along
if height_squared >= -1e-9:
height = math.sqrt(max(0.0, height_squared))
base_x, base_y = left_center[0] + along * dx / center_distance, left_center[1] + along * dy / center_distance
offset_x, offset_y = -dy * height / center_distance, dx * height / center_distance
points.extend(((base_x + offset_x, base_y + offset_y), (base_x - offset_x, base_y - offset_y)))
valid: list[tuple[float, float]] = []
for point in points:
if not _curve_contains_point(left, point) or not _curve_contains_point(right, point):
continue
if not any(math.dist(point, existing) <= 1e-6 for existing in valid):
valid.append(point)
return valid
def _source_curve_intersection(sketch: dict[str, Any], entity_ids: list[str]) -> list[float] | None:
if len(entity_ids) != 2:
return None
curves = [_source_curve(sketch, entity_id) for entity_id in entity_ids]
if any(not isinstance(curve, dict) for curve in curves):
return None
intersections = _curve_intersections_2d(curves[0], curves[1])
if len(intersections) != 1:
return None
return _world_point(sketch, intersections[0])
def _rank_swept_edge(
provenance_candidate: dict[str, Any],
source_selector: dict[str, Any],
candidate: dict[str, Any],
) -> tuple[float, dict[str, Any]] | None:
frame = _producer_frame(provenance_candidate, source_selector)
entity_ids = _source_entity_ids(source_selector)
if frame is None:
return None
_producer, sketch, direction, _span = frame
source_point = _source_curve_intersection(sketch, entity_ids)
geometry = candidate.get("geometry") or {}
start, end = _vector(geometry.get("start_mm")), _vector(geometry.get("end_mm"))
if source_point is None or start is None or end is None or geometry.get("curve_type") != "line":
return None
edge_direction = _unit(_sub(end, start))
if edge_direction is None:
return None
alignment = abs(_dot(direction, edge_direction))
supporting_line_error = _norm(_cross(_sub(source_point, start), direction))
return (alignment + _score_distance(supporting_line_error)) / 2.0, {
"method": "source_pair_extrusion_vertex",
"axis_alignment": round(alignment, 6),
"source_vertex_offset_mm": round(supporting_line_error, 9),
}
def _query_candidate_groups(
provenance_candidate: dict[str, Any],
token: str,
source_selector: dict[str, Any] | None,
resolution: Any,
*,
maximum: int,
) -> tuple[dict[str, Any], list[dict[str, Any]], int]:
"""Rank branch alternatives as FeatureScript query result sets.
This is deliberately an offline geometric heuristic. It gets one chance
to reduce a query to record *sets*, then final STEP strict replay decides
whether that set is useful. It never adds a geometry relation to the
production selector graph.
"""
normal_candidates, normal_count = _search_candidate_records(resolution, maximum=maximum)
if not isinstance(source_selector, dict):
descriptor = {"token": token, "ranking_status": "source_query_missing"}
return descriptor, [
{"records": [candidate], "score": candidate["score"], "ranking": {"method": "resolver_candidate"}}
for candidate in normal_candidates
], normal_count
descriptor = _query_group_descriptor(token, source_selector)
family = descriptor.get("query_family")
ranker = {
"CAP_FACE": _rank_cap_face,
"SWEPT_FACE": _rank_swept_face,
"SWEPT_EDGE": _rank_swept_edge,
}.get(family)
if ranker is None:
descriptor["ranking_status"] = "query_family_not_ranked"
return descriptor, [
{"records": [candidate], "score": candidate["score"], "ranking": {"method": "resolver_candidate"}}
for candidate in normal_candidates
], normal_count
ranked: list[tuple[float, dict[str, Any], dict[str, Any]]] = []
for candidate in resolution.candidates:
if not isinstance(candidate, dict) or not candidate.get("record_id") or float(candidate.get("score") or 0.0) < 0.8:
continue
result = ranker(provenance_candidate, source_selector, candidate)
if result is None:
continue
score, reason = result
if score >= 0.98:
ranked.append((score, deepcopy(candidate), reason))
ranked.sort(key=lambda item: (-item[0], str(item[1].get("record_id"))))
if not ranked:
descriptor["ranking_status"] = "source_geometry_unavailable"
return descriptor, [
{"records": [candidate], "score": candidate["score"], "ranking": {"method": "resolver_candidate"}}
for candidate in normal_candidates
], normal_count
descriptor["ranking_status"] = "source_geometry_ranked"
descriptor["ranked_record_count"] = len(ranked)
if descriptor.get("multiplicity") == "all_fragments":
records = [candidate for _score, candidate, _reason in ranked]
descriptor["selected_set_cardinality"] = len(records)
return descriptor, [{
"records": records,
"score": round(sum(score for score, _candidate, _reason in ranked) / len(ranked), 6),
"ranking": {"method": "all_fragments_source_geometry", "records": [reason for _score, _candidate, reason in ranked]},
}], 1
groups = [
{"records": [candidate], "score": round(score, 6), "ranking": reason}
for score, candidate, reason in ranked[:maximum]
]
return descriptor, groups, len(ranked)
def _search_replay_worker(
candidate: str,
provenance: 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))
provenance_candidate = read_json(Path(provenance))
source_selectors = _source_selector_map(provenance_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)
@@ -485,19 +1099,25 @@ def _search_replay_worker(candidate: str, step: str, result: str, maximum_candid
active_body_id=active_body_id,
)
if resolution.status != "resolved":
candidates, eligible_count = _search_candidate_records(
token = placeholder.get(_SEARCH_TOKEN)
descriptor, candidate_groups, group_count = _query_candidate_groups(
provenance_candidate,
str(token or f"{node.feature_id}:{location}"),
source_selectors.get(str(token)),
resolution,
maximum=maximum_candidates,
)
if candidates:
if candidate_groups:
write_json(Path(result), {
"status": "branchable_selector",
"status": "branchable_query_group",
"feature_id": node.feature_id,
"source_location": location,
"selector_token": token,
"resolution_status": resolution.status,
"diagnostic": resolution.diagnostic.as_dict() if resolution.diagnostic is not None else None,
"candidates": candidates,
"candidate_count": eligible_count,
"query_group": descriptor,
"candidate_groups": candidate_groups,
"candidate_group_count": group_count,
"bound_cdsl": bound,
"selector_binding": evidence,
})
@@ -572,11 +1192,18 @@ def _search_replay_worker(candidate: str, step: str, result: str, maximum_candid
})
def _run_search_replay(candidate_path: Path, step: Path, *, timeout_seconds: float, maximum_candidates: int) -> dict[str, Any]:
def _run_search_replay(
candidate_path: Path,
provenance_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),
(str(candidate_path), str(provenance_path), str(step), str(worker), maximum_candidates),
worker,
timeout_seconds,
)
@@ -596,6 +1223,8 @@ def _public_branch_summary(result: dict[str, Any], choices: list[dict[str, Any]]
"status": result.get("status"),
"feature_id": result.get("feature_id"),
"source_location": result.get("source_location"),
"query_group": result.get("query_group"),
"candidate_group_count": result.get("candidate_group_count"),
"reason": result.get("reason"),
"resolution_status": result.get("resolution_status"),
"choice_path": choices,
@@ -630,7 +1259,10 @@ def _run_selector_search(
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), [])]
search_candidate = deepcopy(geometry_candidate)
_annotate_selector_search_tokens(search_candidate)
provenance_path = directory / "provenance.candidate.cdsl.json"
pending: list[tuple[dict[str, Any], list[dict[str, Any]]]] = [(search_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]] = []
@@ -653,32 +1285,43 @@ def _run_selector_search(
write_json(candidate_path, candidate)
outcome = _run_search_replay(
candidate_path,
provenance_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):
if outcome.get("status") == "branchable_query_group":
groups = [item for item in outcome.get("candidate_groups") or [] if isinstance(item, dict)]
total = int(outcome.get("candidate_group_count") or len(groups))
if total > len(groups):
candidates_truncated = True
branch_summaries.append(_public_branch_summary(outcome, choices))
for candidate_record in candidates:
for group in groups:
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)
token = outcome.get("selector_token")
source_selector = next(
(
selector
for _location, selector in _searchable_selector_sites(source_feature)
if selector.get(_SEARCH_TOKEN) == token
),
None,
)
if not isinstance(source_selector, dict):
raise ValueError(f"search branch selector token is unavailable: {feature_id}:{token}")
records = [item for item in group.get("records") or [] if isinstance(item, dict)]
forced = _forced_candidate_selectors(source_selector, records)
_replace_selector_group(child, feature_id, str(token), forced)
pending.append((child, [*choices, {
"feature_id": feature_id,
"source_location": location,
"record_id": candidate_record.get("record_id"),
"score": candidate_record.get("score"),
"source_location": outcome.get("source_location"),
"query_group": deepcopy(outcome.get("query_group") or {}),
"resolved_runtime_records": records,
"score": group.get("score"),
"ranking": deepcopy(group.get("ranking") or {}),
}]))
continue
comparison = None
@@ -731,6 +1374,8 @@ def _run_selector_search(
gold_step,
rebuild_timeout_seconds=rebuild_timeout_seconds,
comparison_timeout_seconds=comparison_timeout_seconds,
query_groups=choices,
candidate_replay_kind="selector_search_branch",
)
report["selector_record"] = selector_record
return report, selector_record if selector_record.get("status") == "strict_replayed" else None
+41 -25
View File
@@ -59,7 +59,8 @@ class IntegrationTests(unittest.TestCase):
])
with tempfile.TemporaryDirectory() as tmp:
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
rebuilt = Path(tmp) / "rebuild.step"
outcome = rebuild_candidate(candidate, rebuilt)
self.assertEqual(outcome["status"], "rebuilt")
self.assertEqual(
[item["feature_id"] for item in outcome["result"]["feature_results"]],
@@ -123,7 +124,10 @@ class IntegrationTests(unittest.TestCase):
[4.48, 2.36],
)
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_F7")["params"]["result_mode"],
"new_body",
)
self.assertEqual(next(item for item in result.cdsl["features"] if item["id"] == "f_F9")["params"]["result_mode"], "new_body")
with tempfile.TemporaryDirectory() as tmp:
@@ -140,7 +144,8 @@ class IntegrationTests(unittest.TestCase):
candidate = lower_model(parse_featurescript(feature.read_text(), "00925274"), {}).cdsl
with tempfile.TemporaryDirectory() as tmp:
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
rebuilt = Path(tmp) / "rebuild.step"
outcome = rebuild_candidate(candidate, rebuilt)
self.assertEqual(outcome["status"], "rebuilt")
result = outcome["result"]
@@ -166,9 +171,13 @@ class IntegrationTests(unittest.TestCase):
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.assertEqual(len(fillet["selectors"]), 1)
query_set = fillet["selectors"][0]
self.assertEqual(query_set["selector_intent"]["query_family"], "QUERY_SET")
self.assertEqual(query_set["selector_intent"]["query_set_contract"], "proven_operand_union")
self.assertTrue(all(
selector["selector_intent"]["query_family"] == "SWEPT_EDGE"
for selector in fillet["selectors"]
operand["selector_intent"]["query_family"] == "SWEPT_EDGE"
for operand in query_set["query_operands"]
))
with tempfile.TemporaryDirectory() as tmp:
@@ -176,6 +185,8 @@ class IntegrationTests(unittest.TestCase):
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)
resolution = next(item for item in outcome["result"]["selector_resolution"] if item["feature_id"] == "f_F2")
self.assertEqual(resolution["resolution_mode"], "query_set_union")
vertical_edges = [
relation
for delta in outcome["result"]["topology_deltas"]
@@ -287,11 +298,13 @@ class IntegrationTests(unittest.TestCase):
self.assertNotIn("stable_id", selector)
with tempfile.TemporaryDirectory() as tmp:
outcome = rebuild_candidate(candidate, Path(tmp) / "rebuild.step")
rebuilt = Path(tmp) / "rebuild.step"
outcome = rebuild_candidate(candidate, rebuilt)
self.assertTrue(rebuilt.exists())
self.assertEqual(outcome["status"], "rebuild_failed")
self.assertEqual(outcome["error"]["message"], "f_F4: selector_query_unsupported during incremental replay")
self.assertEqual(outcome["status"], "runtime_ineligible")
prefix = outcome["last_executable_prefix"]["result"]
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F3")
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")
@@ -359,7 +372,7 @@ class IntegrationTests(unittest.TestCase):
result = lower_model(parse_featurescript(feature.read_text(), "00542223"), {})
sweep = next(item for item in result.cdsl["features"] if item["id"] == "f_F5")
segment = sweep["params"]["path"]["segment"]
self.assertEqual(result.status, "converted_complete")
self.assertEqual(result.status, "converted_partial")
self.assertEqual(sweep["atomic_id"], "sweep_add")
self.assertEqual(sweep["sketch_id"], "sketch_F4")
self.assertEqual(segment["points"], [[-40.0, -50.0], [-20.0, -14.96], [0.0, 5.0]])
@@ -367,6 +380,7 @@ class IntegrationTests(unittest.TestCase):
self.assertEqual(segment["end_tangent"], [52.88, 49.52])
self.assertNotIn("F2", [item.get("feature_id") for item in result.diagnostics])
self.assertNotIn("F5", [item.get("feature_id") for item in result.diagnostics])
self.assertIn("F7", [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"
@@ -390,17 +404,18 @@ class IntegrationTests(unittest.TestCase):
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):
def test_multi_entity_sketch_wire_qbodytype_path_lowers_as_a_source_ordered_wire_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"]})
self.assertEqual(result.status, "converted_complete")
first_sweep = next(item for item in result.cdsl["features"] if item["id"] == "f_F2")
self.assertEqual(
[(item["source_entity_id"], item["type"]) for item in first_sweep["params"]["path"]["segments"]],
[("E1", "arc"), ("E0", "line"), ("E2.MirrorCS", "arc")],
)
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")
@@ -417,14 +432,13 @@ class IntegrationTests(unittest.TestCase):
with tempfile.TemporaryDirectory() as tmp:
rebuilt = rebuild_cdsl(candidate, Path(tmp) / "pattern.step")
self.assertEqual(rebuilt["solid_count"], 1)
bbox = rebuilt["bbox_mm"]
self.assertAlmostEqual(bbox["min"][0], -52.14101625137758)
self.assertAlmostEqual(bbox["min"][1], -57.500000100000065)
self.assertAlmostEqual(bbox["max"][0], 52.14101625137762)
self.assertAlmostEqual(bbox["max"][1], 37.5000001000001)
self.assertEqual(rebuilt["solid_count"], 2)
self.assertEqual(
[item["feature_id"] for item in rebuilt["feature_results"]],
["f_F1", "f_F3", "f_F5", "f_F6"],
)
def test_fused_body_circular_copy_faces_preserve_shell_prefix_00542223(self):
def test_deferred_pattern_shell_keeps_later_executable_feature_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")
@@ -433,9 +447,11 @@ class IntegrationTests(unittest.TestCase):
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_F7: selector_query_unsupported during incremental replay")
self.assertEqual(outcome["last_executable_prefix"]["last_feature_id"], "f_F6")
self.assertEqual(outcome["status"], "rebuilt")
self.assertEqual(
[item["feature_id"] for item in outcome["result"]["feature_results"]],
["f_F1", "f_F3", "f_F5", "f_F6", "f_F10"],
)
def test_face_chamfer_source_query_is_not_geometry_bound_00111611(self):
root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test"
File diff suppressed because it is too large Load Diff
+51 -2
View File
@@ -4,8 +4,8 @@ import unittest
from cadfs_to_cdsl.featurescript_lexer import lex
from cadfs_to_cdsl.featurescript_parser import parse_featurescript
from cadfs_to_cdsl.ir import Call
from cadfs_to_cdsl.lowering import _queries, _source_refs
from cadfs_to_cdsl.query_parser import parse_query
from cadfs_to_cdsl.lowering import _number, _queries, _source_refs
from cadfs_to_cdsl.query_parser import parse_query, query_expr
from cadfs_to_cdsl.units import length_mm
@@ -28,6 +28,14 @@ TRANSFORM_SOURCE = SOURCE.replace(
''',
)
UNKNOWN_FEATURE_SOURCE = SOURCE.replace(
'\n});\n',
'''
assignVariable(context, id + "F2", {"name" : "diameter", "value" : 10 * mm});
});
''',
)
class ParserTests(unittest.TestCase):
def test_lexer_ignores_comments_and_preserves_lines(self):
@@ -47,6 +55,10 @@ class ParserTests(unittest.TestCase):
self.assertEqual(model.features[-1].operation, "transform")
self.assertEqual(model.features[-1].params["transformType"], "TransformType.TRANSLATION_3D")
def test_unknown_direct_context_feature_is_preserved_for_capability_diagnostics(self):
model = parse_featurescript(UNKNOWN_FEATURE_SOURCE, "unknown-feature")
self.assertEqual([(feature.feature_id, feature.operation) for feature in model.features], [("F1", "extrude"), ("F2", "assignVariable")])
def test_query_parser(self):
query = Call("makeQuery", [Call("__binary__", ["id", "+", "F1.opExtrude"]), "CAP_EDGE", "EDGE", {"isStart": False, "x": Call("sQuery", [Call("__binary__", ["id", "+", "F0.wireOp"]), "EDGE", "E0"])}])
value = parse_query(query)
@@ -55,6 +67,43 @@ class ParserTests(unittest.TestCase):
self.assertEqual(value.ast["call"], "makeQuery")
self.assertEqual(value.ast["args"][0]["call"], "__binary__")
def test_signed_parenthesized_scalar_preserves_unit_expression_semantics(self):
model = parse_featurescript(SOURCE.replace(
'"depth":120 * mm', '"depth":-(10 + 2) / 2 * mm', 1,
), "signed-scalar")
depth = model.features[-1].params["depth"]
self.assertEqual((depth.name, depth.args[1]), ("__binary__", "*"))
self.assertEqual(_number(depth), -6.0)
def test_query_expression_preserves_nested_set_and_filter_boundaries(self):
source = Call("qUnion", [[
Call("qConstructionFilter", [
Call("qBodyType", [
Call("qCreatedBy", [Call("__binary__", ["id", "+", "F1"]), "EDGE"]),
"BodyType.WIRE",
]),
"ConstructionObject.NO",
]),
Call("qSubtraction", [
Call("sQuery", [Call("__binary__", ["id", "+", "F0.wireOp"]), "EDGE", "E0"]),
Call("sQuery", [Call("__binary__", ["id", "+", "F0.wireOp"]), "EDGE", "E1"]),
]),
]])
expression = query_expr(source)
self.assertEqual(expression["version"], "1.0")
self.assertEqual(expression["root"]["node"], "set")
self.assertEqual(expression["root"]["operator"], "union")
filtered, subtraction = expression["root"]["operands"]
self.assertEqual((filtered["node"], filtered["filter"]), ("filter", "construction"))
self.assertEqual((filtered["input"]["node"], filtered["input"]["filter"]), ("filter", "body_type"))
self.assertEqual((subtraction["node"], subtraction["operator"]), ("set", "subtraction"))
parsed = parse_query(source)
self.assertEqual(parsed.query_combinators, ["qUnion", "qSubtraction"])
self.assertEqual(
parsed.filters,
["qConstructionFilter", "qBodyType"],
)
def test_source_version_and_standard_library_are_retained(self):
source = '''FeatureScript 1511;
import(path : "onshape/std/geometry.fs", version : "1511.0");
+34 -1
View File
@@ -3,7 +3,9 @@ from __future__ import annotations
import json, tempfile, unittest
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from cadfs_to_cdsl.reports import write_json
from cadfs_to_cdsl.dataset import Sample
from cadfs_to_cdsl.operation_registry import build_operation_registry
from cadfs_to_cdsl.reports import generate_markdown_report, write_json
class ReportTests(unittest.TestCase):
@@ -17,5 +19,36 @@ class ReportTests(unittest.TestCase):
self.assertIn(payload["index"], range(64))
self.assertEqual(list(path.parent.glob(".summary.json.*.tmp")), [])
def test_operation_registry_distinguishes_absent_roadmap_work_items(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
source = root / "00000001.txt"
source.write_text(
'FeatureScript 1511; export const f = defineFeature(function(context, id, definition) {'
' extrude(context, id + "F1", {"depth": 1 * mm}); });',
encoding="utf-8",
)
registry = build_operation_registry([Sample("00000001", files={"featurescript": str(source)})])
self.assertEqual(registry["schema"], "cadfs_to_cdsl.operation_registry.v1")
self.assertEqual(registry["operations"]["extrude"]["feature_count"], 1)
self.assertEqual(registry["operations"]["transform"]["state"], "not_observed_in_current_source")
self.assertEqual(registry["operations"]["thicken"]["feature_count"], 0)
def test_markdown_report_uses_source_registry_instead_of_static_operation_claims(self):
with tempfile.TemporaryDirectory() as temporary:
output = Path(temporary)
write_json(output / "operation_registry.json", {
"operations": {
"transform": {"roadmap_work_item": True, "state": "observed"},
"thicken": {"roadmap_work_item": True, "state": "not_observed_in_current_source"},
},
})
report = generate_markdown_report(output, [])
text = report.read_text(encoding="utf-8")
self.assertIn("thicken", text)
self.assertNotIn("draft, thicken, split", text)
if __name__ == "__main__": unittest.main()
@@ -6,7 +6,13 @@ 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
from cadfs_to_cdsl.selector_candidate_demo import (
_query_candidate_groups,
_search_candidate_records,
_strip_selector_search_tokens,
run_geometry_probe,
strip_provenance_intents,
)
def _contains_selector_intent(value: object) -> bool:
@@ -18,6 +24,39 @@ def _contains_selector_intent(value: object) -> bool:
class SelectorCandidateDemoTests(unittest.TestCase):
@staticmethod
def _query_group_candidate(query_family: str, source_entity: str | None = None, source_entities: list[str] | None = None) -> dict:
intent = {
"version": "1.0",
"kind": "edge" if query_family == "SWEPT_EDGE" else "face",
"query_family": query_family,
"source_query": {"ast": {"call": "makeQuery", "args": ["owner", query_family]}},
"derivation_policy": {"allowed": ["boundary"], "multiplicity": "all_fragments" if query_family == "SWEPT_FACE" else "one"},
}
if source_entity is not None:
intent["source_entity"] = {"sketch_id": "S", "entity_id": source_entity}
if source_entities is not None:
intent["source_entities"] = [{"sketch_id": "S", "entity_id": entity_id} for entity_id in source_entities]
return {"kind": intent["kind"], "owner_feature_id": "f_extrude", "selector_intent": intent}
def _query_group_provenance(self, selector: dict) -> dict:
return {
"geometry": {
"sketches": [{
"id": "sketch_S", "source_sketch_id": "S",
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
"profile": {"type": "planar_imprint", "source_entities": [
{"id": "E1", "curve": {"type": "line", "start": [0, 0], "end": [3, 0]}},
{"id": "E2", "curve": {"type": "line", "start": [3, 0], "end": [3, 2]}},
]},
}],
},
"features": [
{"id": "f_extrude", "atomic_id": "extrude_add_blind", "sketch_id": "sketch_S", "params": {"distance_mm": 5}},
{"id": "f_consumer", "selectors": [selector]},
],
}
def test_search_candidates_excludes_records_below_normal_resolution_threshold(self) -> None:
resolution = type("Resolution", (), {
"candidates": (
@@ -34,11 +73,85 @@ class SelectorCandidateDemoTests(unittest.TestCase):
def test_strip_provenance_intents_removes_nested_selector_metadata(self) -> None:
value = {
"selector_intent": {"version": "1.0"},
"output_role": "extrude.end",
"output_role_source": {"owner_feature_id": "f0", "output_role": "extrude.end"},
"params": {"reference": {"selector_intent_version": "1.0"}},
}
self.assertEqual(strip_provenance_intents(value), 2)
self.assertEqual(value, {"params": {"reference": {}}})
def test_query_group_keeps_all_ranked_swept_face_fragments_together(self) -> None:
selector = self._query_group_candidate("SWEPT_FACE", source_entity="E1")
provenance = self._query_group_provenance(selector)
resolution = type("Resolution", (), {"candidates": (
{"record_id": "fragment-a", "score": 1.0, "geometry": {"surface_type": "plane", "plane_normal": [0, -1, 0], "plane_offset_mm": 0}},
{"record_id": "fragment-b", "score": 1.0, "geometry": {"surface_type": "plane", "plane_normal": [0, 1, 0], "plane_offset_mm": 0}},
{"record_id": "wrong-wall", "score": 1.0, "geometry": {"surface_type": "plane", "plane_normal": [1, 0, 0], "plane_offset_mm": 3}},
)})()
descriptor, groups, count = _query_candidate_groups(
provenance, "f_consumer:selectors[0]", selector, resolution, maximum=8,
)
self.assertEqual(count, 1)
self.assertEqual(descriptor["ranking_status"], "source_geometry_ranked")
self.assertEqual(descriptor["multiplicity"], "all_fragments")
self.assertEqual([record["record_id"] for record in groups[0]["records"]], ["fragment-a", "fragment-b"])
self.assertEqual(groups[0]["ranking"]["method"], "all_fragments_source_geometry")
def test_query_group_ranks_the_source_pair_vertical_edge(self) -> None:
selector = self._query_group_candidate("SWEPT_EDGE", source_entities=["E1", "E2"])
provenance = self._query_group_provenance(selector)
resolution = type("Resolution", (), {"candidates": (
{"record_id": "source-vertex", "score": 1.0, "geometry": {"curve_type": "line", "start_mm": [3, 0, 0], "end_mm": [3, 0, 5]}},
{"record_id": "other-vertex", "score": 1.0, "geometry": {"curve_type": "line", "start_mm": [0, 0, 0], "end_mm": [0, 0, 5]}},
)})()
descriptor, groups, count = _query_candidate_groups(
provenance, "f_consumer:selectors[0]", selector, resolution, maximum=8,
)
self.assertEqual(count, 1)
self.assertEqual(descriptor["query_family"], "SWEPT_EDGE")
self.assertEqual(groups[0]["records"][0]["record_id"], "source-vertex")
self.assertEqual(groups[0]["ranking"]["method"], "source_pair_extrusion_vertex")
def test_query_group_ranks_the_line_arc_intersection_edge(self) -> None:
selector = self._query_group_candidate("SWEPT_EDGE", source_entities=["E1", "E2"])
provenance = self._query_group_provenance(selector)
provenance["geometry"]["sketches"][0]["profile"]["source_entities"][0]["curve"] = {
"type": "arc", "start": [-1, 0], "end": [1, 0], "center": [0, 0], "radius_mm": 1, "clockwise": True,
}
provenance["geometry"]["sketches"][0]["profile"]["source_entities"][1]["curve"] = {
"type": "line", "start": [0, -2], "end": [0, 2],
}
resolution = type("Resolution", (), {"candidates": (
{"record_id": "arc-line-intersection", "score": 1.0, "geometry": {"curve_type": "line", "start_mm": [0, 1, 0], "end_mm": [0, 1, 5]}},
{"record_id": "unrelated-vertical", "score": 1.0, "geometry": {"curve_type": "line", "start_mm": [0, -1, 0], "end_mm": [0, -1, 5]}},
)})()
_descriptor, groups, count = _query_candidate_groups(
provenance, "f_consumer:selectors[0]", selector, resolution, maximum=8,
)
self.assertEqual(count, 1)
self.assertEqual(groups[0]["records"][0]["record_id"], "arc-line-intersection")
def test_query_group_ignores_a_candidate_missing_cylinder_axis_evidence(self) -> None:
selector = self._query_group_candidate("SWEPT_FACE", source_entity="E1")
provenance = self._query_group_provenance(selector)
provenance["geometry"]["sketches"][0]["profile"]["source_entities"][0]["curve"] = {
"type": "arc", "start": [-1, 0], "end": [1, 0], "center": [0, 0], "radius_mm": 1, "clockwise": True,
}
resolution = type("Resolution", (), {"candidates": (
{"record_id": "missing-axis", "score": 1.0, "geometry": {"surface_type": "cylinder", "radius_mm": 1}},
)})()
descriptor, groups, count = _query_candidate_groups(
provenance, "f_consumer:selectors[0]", selector, resolution, maximum=8,
)
self.assertEqual(count, 1)
self.assertEqual(descriptor["ranking_status"], "source_geometry_unavailable")
self.assertEqual(groups[0]["records"][0]["record_id"], "missing-axis")
def test_experiment_search_tokens_are_not_persisted(self) -> None:
value = {"selectors": [{"_selector_search_token": "f:selectors[0]"}]}
self.assertEqual(_strip_selector_search_tokens(value), 1)
self.assertEqual(value, {"selectors": [{}]})
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"