"""Runtime-neutral CDSL diagnostics, planning, and topology contracts. This module deliberately has no build123d dependency. The planner and selector resolver can therefore be used by validation, batch reporting, and any geometry adapter without importing OCC objects. """ from __future__ import annotations from dataclasses import dataclass, field from math import sqrt from typing import Any, Iterable from .selector_capabilities import ( known_selector_query_standard_library_versions, known_selector_query_versions, blend_face_selector_contract_error, copy_selector_contract_error, owner_body_selector_contract_error, proven_operand_set_contract_error, selector_query_capability, ) from .specs import AxisSpec, PlaneSpec, Vector3, _length, _vector3, normalize_selector_geometry @dataclass(frozen=True) class RuntimeDiagnostic: code: str message: str feature_id: str | None = None detail: dict[str, Any] = field(default_factory=dict) def as_dict(self) -> dict[str, Any]: output: dict[str, Any] = {"code": self.code, "message": self.message} if self.feature_id is not None: output["feature_id"] = self.feature_id if self.detail: output["detail"] = self.detail return output _SELECTOR_PROVENANCE_DERIVATIONS = frozenset({ "continuation", "fragment", "merge", "intersection", "boundary", "replacement", }) def selector_has_provenance_intent(selector: dict[str, Any]) -> bool: """Whether ``selector`` must use the FeatureScript provenance contract.""" intent = selector.get("selector_intent") return isinstance(intent, dict) and intent.get("query_family") != "GEOMETRIC" def selector_intent_policy(intent: dict[str, Any]) -> tuple[set[str], str]: """Return the supported derivations and requested cardinality policy.""" policy = intent.get("derivation_policy") if not isinstance(policy, dict): policy = {} return ( { value for value in policy.get("allowed") or () if value in _SELECTOR_PROVENANCE_DERIVATIONS }, str(policy.get("multiplicity") or "one"), ) def validate_selector_provenance_intent(selector: dict[str, Any]) -> RuntimeDiagnostic | None: """Return the common provenance gate failure before resolving topology.""" if not selector_has_provenance_intent(selector): return None intent = selector["selector_intent"] legacy_version = selector.get("selector_intent_version") if legacy_version is not None and legacy_version != intent.get("version"): return RuntimeDiagnostic( "selector_query_unsupported", "Selector intent has conflicting nested and legacy version fields", detail={"nested_version": intent.get("version"), "legacy_version": legacy_version}, ) source_query = intent.get("source_query") source_version = source_query.get("featurescript_version") if isinstance(source_query, dict) else None if not isinstance(source_version, str) or not source_version or source_version == "0": return RuntimeDiagnostic( "selector_query_version_unknown", "FeatureScript query source version is unavailable", detail={"query_family": intent.get("query_family")}, ) standard_library = source_query.get("standard_library") if isinstance(source_query, dict) else None standard_library_version = source_query.get("standard_library_version") if isinstance(source_query, dict) else None if not isinstance(standard_library, str) or not standard_library or not isinstance(standard_library_version, str) or not standard_library_version: return RuntimeDiagnostic( "selector_query_version_unknown", "FeatureScript query standard-library import or version is unavailable", detail={"query_family": intent.get("query_family")}, ) query_set_error = proven_operand_set_contract_error(selector) if query_set_error is not None: return RuntimeDiagnostic( "selector_query_set_invalid", "The FeatureScript query-set selector contract is invalid", detail={"reason": query_set_error}, ) owner_body_error = owner_body_selector_contract_error(selector) if owner_body_error is not None: return RuntimeDiagnostic( "selector_owner_body_invalid", "The FeatureScript qOwnerBody selector contract is invalid", detail={"reason": owner_body_error}, ) copy_error = copy_selector_contract_error(selector) if copy_error is not None: return RuntimeDiagnostic( "selector_copy_invalid", "The FeatureScript COPY selector contract is invalid", detail={"reason": copy_error}, ) blend_face_error = blend_face_selector_contract_error(selector) if blend_face_error is not None: return RuntimeDiagnostic( "selector_blend_face_invalid", "The FeatureScript BLEND_FACE selector contract is invalid", detail={"reason": blend_face_error}, ) if intent.get("query_family") == "QUERY_SET": return None if intent.get("query_family") == "OWNER_BODY": return None capability = selector_query_capability(intent) if capability is None: family = str(intent.get("query_family") or "") return RuntimeDiagnostic( "selector_query_unsupported", "FeatureScript query semantics are not registered for this source version", detail={ "query_family": family, "featurescript_version": source_version, "verified_versions": list(known_selector_query_versions(family)), "standard_library": standard_library, "standard_library_version": standard_library_version, "verified_standard_libraries": [ {"path": path, "version": version} for path, version in known_selector_query_standard_library_versions(family) ], }, ) allowed, multiplicity = selector_intent_policy(intent) if multiplicity == "none" or not allowed: return RuntimeDiagnostic( "selector_query_unsupported", "Selector derivation policy does not permit execution", detail={ "query_family": intent.get("query_family"), "multiplicity": multiplicity, "allowed": sorted(allowed), }, ) return None @dataclass(frozen=True) class CapabilityResult: feature_id: str atomic_id: str resolved_status: str required_capabilities: tuple[str, ...] = () blockers: tuple[RuntimeDiagnostic, ...] = () @property def executable(self) -> bool: return self.resolved_status == "executable" def as_dict(self) -> dict[str, Any]: return { "feature_id": self.feature_id, "atomic_id": self.atomic_id, "resolved_status": self.resolved_status, "required_capabilities": list(self.required_capabilities), "blockers": [blocker.as_dict() for blocker in self.blockers], } @dataclass(frozen=True) class FeaturePlanNode: feature_id: str atomic_id: str name: str | None depends_on: tuple[str, ...] params: dict[str, Any] selectors: tuple[dict[str, Any], ...] sketch_id: str | None declared_status: str | None source_feature: dict[str, Any] @dataclass class FeatureResult: feature_id: str atomic_id: str status: str body_id: str | None = None surface_id: str | None = None context: PlaneSpec | AxisSpec | None = None replay_definition: dict[str, Any] | None = None diagnostics: list[RuntimeDiagnostic] = field(default_factory=list) def as_dict(self) -> dict[str, Any]: output: dict[str, Any] = { "feature_id": self.feature_id, "atomic_id": self.atomic_id, "status": self.status, "diagnostics": [diagnostic.as_dict() for diagnostic in self.diagnostics], } if self.body_id is not None: output["body_id"] = self.body_id if self.surface_id is not None: output["surface_id"] = self.surface_id if self.context is not None: output["context"] = self.context.as_dict() if self.replay_definition is not None: output["replay_definition"] = self.replay_definition return output @dataclass(frozen=True) class TopologyRecord: """Runtime-side signature of a topology item or context object. ``feature_id`` identifies the feature that produced this *snapshot*. ``owner_feature_ids`` is durable semantic provenance for an unchanged current B-rep item. Boolean and dress-up operations replace OCC objects, so keeping these concepts separate prevents a later mutation from making every surviving face appear to be owned by that mutation. """ record_id: str kind: str feature_id: str body_id: str | None = None geometry: dict[str, Any] = field(default_factory=dict) value: Any = None owner_feature_ids: tuple[str, ...] = () # Builder-produced roles describe a particular result subshape. They are # deliberately separate from the geometric signature: equal geometry does # not prove that two faces have the same feature-output meaning. output_roles: tuple[str, ...] = () # Generated roles may carry the direct feature-output role that the kernel # operation transformed. This is semantic provenance, not a stable-id # shortcut: the resolver still requires the exact active result snapshot. output_role_sources: tuple[tuple[str, str, str], ...] = () # A source profile anchor is a transient, exact B-rep subshape built from # one declared FeatureScript sketch entity. It is not active topology and # never participates in geometry fallback selection. source_entity: tuple[str, str] | None = None # A direct prism can generate an edge from a profile vertex jointly named # by its incident source entities. The tuple is canonicalized by the # adapter before registration; a partial set is intentionally not equal. source_entities: tuple[tuple[str, str], ...] = () # A transient record is retained solely as a historical boolean input. # It can supply exact source handles to a later builder relation, but can # never be returned as current/selectable model topology. transient: bool = False @property def owners(self) -> tuple[str, ...]: """Return durable provenance, retaining compatibility for contexts.""" return self.owner_feature_ids or (self.feature_id,) def public_dict(self) -> dict[str, Any]: output: dict[str, Any] = { "record_id": self.record_id, "kind": self.kind, "feature_id": self.feature_id, "geometry": self.geometry, } if self.body_id is not None: output["body_id"] = self.body_id if self.owner_feature_ids: output["owner_feature_ids"] = list(self.owner_feature_ids) if self.output_roles: output["output_roles"] = list(self.output_roles) if self.output_role_sources: output["output_role_sources"] = [ {"output_role": role, "owner_feature_id": owner, "source_output_role": source_role} for role, owner, source_role in self.output_role_sources ] if self.source_entity is not None: output["source_entity"] = { "sketch_id": self.source_entity[0], "entity_id": self.source_entity[1], } if self.source_entities: output["source_entities"] = [ {"sketch_id": sketch_id, "entity_id": entity_id} for sketch_id, entity_id in self.source_entities ] if self.transient: output["transient"] = True return output @dataclass(frozen=True) class TopologyDeltaRelation: """One opaque kernel-history relationship for a topology subshape. Geometry adapters retain ownership of the values in this structure. They are intentionally opaque to the runtime: a build123d/OCC adapter may use ``TopoDS_Shape`` values while another adapter can use its native handles. The registry only asks whether a handle is exactly the same topology item; it never uses this contract to score nearby geometry. """ event: str kind: str source_value: Any result_values: tuple[Any, ...] = () output_role: str | None = None derivation: str | None = None source_role: str | None = None result_role: str | None = None source_slot: str | None = None result_slot: str | None = None coverage: str = "complete" status: str = "proven" # ``kind`` remains the compatibility shorthand for same-type history. # Builders may also generate edge -> face or vertex -> edge relations. source_kind: str | None = None result_kind: str | None = None def __post_init__(self) -> None: if self.event not in {"preserved", "modified", "generated", "deleted"}: raise ValueError(f"unsupported topology delta event {self.event!r}") if self.kind not in {"face", "edge", "vertex"}: raise ValueError(f"unsupported topology delta kind {self.kind!r}") for name, value in (("source_kind", self.source_kind), ("result_kind", self.result_kind)): if value is not None and value not in {"face", "edge", "vertex"}: raise ValueError(f"unsupported topology delta {name} {value!r}") if self.event == "deleted" and (self.result_values or self.output_role is not None): raise ValueError("deleted topology delta relations cannot have result values or an output role") if self.output_role is not None and (not isinstance(self.output_role, str) or not self.output_role): raise ValueError("topology delta output_role must be a non-empty string when provided") derivation = self.derivation if derivation is None: derivation = { "preserved": "continuation", "modified": "fragment" if len(self.result_values) > 1 else "continuation", "generated": "boundary", "deleted": "replacement", }[self.event] object.__setattr__(self, "derivation", derivation) if derivation not in {"continuation", "fragment", "merge", "intersection", "boundary", "replacement"}: raise ValueError(f"unsupported topology derivation {derivation!r}") if self.coverage not in {"complete", "partial", "none"}: raise ValueError(f"unsupported topology coverage {self.coverage!r}") if self.status not in {"proven", "unknown", "rejected"}: raise ValueError(f"unsupported topology relation status {self.status!r}") @dataclass(frozen=True) class TopologySectionRelation: """One source-qualified boolean section edge. ``SectionEdges()`` alone proves that a boolean produced an edge, but not which input faces produced it. OCC can provide that additional fact when the exact section edge is returned by ``Generated(face)`` for each of two input faces. Keep this as a separate relation because the ordinary delta relation contract is intentionally one-source-to-many-results. """ source_values: tuple[Any, Any] result_value: Any coverage: str = "complete" status: str = "proven" def __post_init__(self) -> None: if self.source_values[0] is self.source_values[1]: raise ValueError("section relation requires two distinct source faces") if self.coverage not in {"complete", "partial", "none"}: raise ValueError(f"unsupported section relation coverage {self.coverage!r}") if self.status not in {"proven", "unknown", "rejected"}: raise ValueError(f"unsupported section relation status {self.status!r}") @dataclass(frozen=True) class TopologyBlendRelation: """Exact dress-up transition from an input edge/face pair to result edges. A fillet or chamfer builder creates a patch face from the selected input edge. A ``BLEND_EDGE`` query additionally names the source face that the patch blends into, so the patch face alone is insufficient evidence. This relation retains only an exact shared boundary between that generated patch and a final ``Modified(source_face)`` result; it never ranks result edges by their geometry. """ source_edge_value: Any source_face_value: Any patch_face_value: Any | None = None blend_into_result_value: Any | None = None result_values: tuple[Any, ...] = () coverage: str = "complete" status: str = "proven" def __post_init__(self) -> None: if self.coverage not in {"complete", "partial", "none"}: raise ValueError(f"unsupported blend relation coverage {self.coverage!r}") if self.status not in {"proven", "unknown", "rejected"}: raise ValueError(f"unsupported blend relation status {self.status!r}") @dataclass(frozen=True) class TopologyDelta: """Kernel-backed topology history for one adapter operation. ``operation`` is evidence only. The runtime transfers durable provenance solely from a unique exact relationship, never from an operation name or a geometric resemblance. """ operation: str relations: tuple[TopologyDeltaRelation, ...] = () # Boolean section edges are distinct from source continuations. INTERSECT # selectors may only rely on this exact builder evidence. section_values: tuple[Any, ...] = () # A section value is executable provenance only when this relation proves # both source faces and the exact final edge. Unqualified values remain # diagnostic facts in ``section_values``. section_relations: tuple[TopologySectionRelation, ...] = () # Dress-up transition evidence is kept separate from ordinary one-source # lineage. A future BLEND_EDGE resolver must consume both source handles. blend_relations: tuple[TopologyBlendRelation, ...] = () history_status: str = "proven" history_reason: str | None = None def __post_init__(self) -> None: if self.history_status not in {"proven", "partial", "unknown", "rejected"}: raise ValueError(f"unsupported topology delta history status {self.history_status!r}") @dataclass(frozen=True) class TopologyLineage: """An N:M semantic edge backed by an adapter history relation.""" source_record_ids: tuple[str, ...] result_record_ids: tuple[str, ...] derivation: str evidence: str coverage: str status: str operation: str output_role: str | None = None feature_id: str | None = None source_kind: str | None = None result_kind: str | None = None def as_dict(self) -> dict[str, Any]: result = { "source_record_ids": list(self.source_record_ids), "result_record_ids": list(self.result_record_ids), "derivation": self.derivation, "evidence": self.evidence, "coverage": self.coverage, "status": self.status, "operation": self.operation, } if self.output_role is not None: result["output_role"] = self.output_role if self.feature_id is not None: result["feature_id"] = self.feature_id if self.source_kind is not None: result["source_kind"] = self.source_kind if self.result_kind is not None: result["result_kind"] = self.result_kind return result @dataclass(frozen=True) class SelectorResolution: selector: dict[str, Any] status: str record: TopologyRecord | None = None # ``records`` carries policy-authorized 1:N selector results. ``record`` # remains the compatibility field for a unique selection and context use. records: tuple[TopologyRecord, ...] = () candidates: tuple[dict[str, Any], ...] = () diagnostic: RuntimeDiagnostic | None = None # This is assigned by the resolution branch, never reconstructed from # serialized selector fields. That keeps audit evidence honest when a # legacy selector and a provenance selector share the same shape. resolution_mode: str = "unresolved" evidence: dict[str, Any] = field(default_factory=dict) def as_dict(self) -> dict[str, Any]: output = { "selector": self.selector, "status": self.status, "resolution_mode": self.resolution_mode, "candidates": list(self.candidates), } if self.record is not None: output["record"] = self.record.public_dict() output["selected"] = self.record.public_dict() score = next( (candidate.get("score") for candidate in self.candidates if candidate.get("record_id") == self.record.record_id), None, ) if score is not None: output["score"] = score if self.records: output["records"] = [record.public_dict() for record in self.records] if self.diagnostic is not None: output["diagnostic"] = self.diagnostic.as_dict() if self.evidence: output["evidence"] = self.evidence return output class TopologyRegistry: """Feature-scoped context/topology registry with explainable matching.""" def __init__(self) -> None: self._records: list[TopologyRecord] = [] self._by_feature: dict[str, list[TopologyRecord]] = {} self._active_body_id: str | None = None self._topology_deltas: list[dict[str, Any]] = [] self._lineage: list[TopologyLineage] = [] # #8 selector 持久性:old_record_id -> [new_record_id]。fillet/chamfer # 会把一条直线边拆分为若干段(中间直段 + 两端圆弧),旧边不再与任何 # 新边几何等价;这里记录"位置轨迹延续"的直段后继,使后续 selector 的 # stable_id 引用可以解析到 active body 内的新形态。 self._successors: dict[str, list[str]] = {} def register(self, record: TopologyRecord) -> None: self._records.append(record) self._by_feature.setdefault(record.feature_id, []).append(record) def records_for_feature(self, feature_id: str) -> tuple[TopologyRecord, ...]: return tuple(self._by_feature.get(feature_id, ())) def records(self) -> tuple[TopologyRecord, ...]: return tuple(self._records) def topology_deltas(self) -> tuple[dict[str, Any], ...]: """Return serializable evidence derived from exact adapter history.""" return tuple(self._topology_deltas) def lineage(self) -> tuple[TopologyLineage, ...]: """Return kernel-backed N:M lineage without heuristic successors.""" return tuple(self._lineage) @classmethod def from_public_snapshot( cls, records: Iterable[dict[str, Any]], topology_deltas: Iterable[dict[str, Any]] = (), ) -> "TopologyRegistry": """Rehydrate resolver facts from a prior session report. Prefix binding uses this only as a diagnostic compatibility adapter. It receives exported record IDs and already-recorded kernel relations, never geometry guesses promoted to lineage. The live execution path still owns the opaque OCC values and records the original facts. """ registry = cls() for item in records: if not isinstance(item, dict): continue role_sources = [] for source in item.get("output_role_sources") or (): if not isinstance(source, dict): continue role, owner, source_role = ( source.get("output_role"), source.get("owner_feature_id"), source.get("source_output_role"), ) if all(isinstance(value, str) for value in (role, owner, source_role)): role_sources.append((role, owner, source_role)) record_id = str(item.get("record_id") or "") kind = str(item.get("kind") or "") # Old prefix reports did not export context feature IDs. Retain # a synthetic report-local producer for legacy geometric binding; # provenance selectors still require their own source relation. feature_id = str(item.get("feature_id") or "report_snapshot") if not record_id or not kind: continue source_entity = item.get("source_entity") source_pair = ( (str(source_entity["sketch_id"]), str(source_entity["entity_id"])) if isinstance(source_entity, dict) and isinstance(source_entity.get("sketch_id"), str) and isinstance(source_entity.get("entity_id"), str) else None ) source_pairs = tuple( (str(source["sketch_id"]), str(source["entity_id"])) for source in item.get("source_entities") or () if isinstance(source, dict) and isinstance(source.get("sketch_id"), str) and isinstance(source.get("entity_id"), str) ) registry.register(TopologyRecord( record_id=record_id, kind=kind, feature_id=feature_id, body_id=str(item["body_id"]) if item.get("body_id") is not None else None, geometry=dict(item.get("geometry") or {}), # Record IDs are opaque-but-exact within one exported report. value=record_id, owner_feature_ids=tuple(str(value) for value in item.get("owner_feature_ids") or ()), output_roles=tuple(str(value) for value in item.get("output_roles") or ()), output_role_sources=tuple(role_sources), source_entity=source_pair, source_entities=source_pairs, transient=bool(item.get("transient")), )) for delta in topology_deltas: if not isinstance(delta, dict): continue feature_id = str(delta.get("feature_id") or "") operation = str(delta.get("operation") or "") for item in delta.get("lineage") or (): if not isinstance(item, dict): continue registry._lineage.append(TopologyLineage( source_record_ids=tuple(str(value) for value in item.get("source_record_ids") or ()), result_record_ids=tuple(str(value) for value in item.get("result_record_ids") or ()), derivation=str(item.get("derivation") or ""), evidence=str(item.get("evidence") or "kernel_history"), coverage=str(item.get("coverage") or "partial"), status=str(item.get("status") or "unknown"), operation=str(item.get("operation") or operation), output_role=str(item["output_role"]) if item.get("output_role") is not None else None, feature_id=str(item.get("feature_id") or feature_id) or None, source_kind=str(item["source_kind"]) if item.get("source_kind") is not None else None, result_kind=str(item["result_kind"]) if item.get("result_kind") is not None else None, )) return registry def register_context(self, feature_id: str, context: PlaneSpec | AxisSpec) -> TopologyRecord: kind = "plane" if isinstance(context, PlaneSpec) else "axis" record = TopologyRecord( record_id=f"{feature_id}:{kind}", kind=kind, feature_id=feature_id, geometry=context.as_dict(), value=context, ) self.register(record) return record def register_transient_snapshot( self, feature_id: str, snapshot_id: str, records: Iterable[TopologyRecord], *, topology_delta: TopologyDelta, anchors: Iterable[TopologyRecord], ) -> tuple[TopologyRecord, ...]: """Register a non-selectable direct-prism input for one following boolean. A primary ``REMOVE`` builds its tool only long enough to feed a cut; unlike a ``new_body`` tool, that B-rep never becomes active model topology. Its exact faces and direct profile anchors are nevertheless needed to prove a source-qualified section edge. This records that limited historical snapshot without advancing ``_active_body_id`` or enabling geometry/stable-ID selection against the tool. """ if ( topology_delta.operation != "extrude" or topology_delta.history_status != "proven" or not topology_delta.relations ): raise ValueError("transient snapshot requires complete direct-prism history") raw_anchors = list(anchors) raw_records = list(records) if not snapshot_id or not raw_anchors or not raw_records: raise ValueError("transient snapshot requires source anchors and result topology") transient_anchors = [ TopologyRecord( record_id=record.record_id, kind=record.kind, feature_id=feature_id, body_id=None, geometry=dict(record.geometry), value=record.value, owner_feature_ids=(feature_id,), output_roles=record.output_roles, output_role_sources=record.output_role_sources, source_entity=record.source_entity, source_entities=record.source_entities, transient=True, ) for record in raw_anchors ] for anchor in transient_anchors: self.register(anchor) current = [ TopologyRecord( record_id=record.record_id, kind=record.kind, feature_id=feature_id, body_id=snapshot_id, geometry=dict(record.geometry), value=record.value, owner_feature_ids=(feature_id,), output_roles=record.output_roles, output_role_sources=record.output_role_sources, source_entity=record.source_entity, source_entities=record.source_entities, transient=True, ) for record in raw_records ] ( _exact_predecessors, _exact_successors, _kernel_covered_predecessors, exact_output_roles, exact_output_role_sources, delta_evidence, ) = self._exact_delta_links(topology_delta, transient_anchors, current) registered = [ TopologyRecord( record_id=record.record_id, kind=record.kind, feature_id=feature_id, body_id=snapshot_id, geometry=dict(record.geometry), value=record.value, owner_feature_ids=(feature_id,), output_roles=tuple(sorted({*record.output_roles, *exact_output_roles.get(record.record_id, ())})), output_role_sources=tuple(sorted({*record.output_role_sources, *exact_output_role_sources.get(record.record_id, ())})), source_entity=record.source_entity, source_entities=record.source_entities, transient=True, ) for record in current ] for record in registered: self.register(record) self._append_delta_evidence( feature_id, topology_delta, transient_anchors, current, delta_evidence, ) return tuple(registered) def _append_delta_evidence( self, feature_id: str, topology_delta: TopologyDelta, previous: Iterable[TopologyRecord], current: Iterable[TopologyRecord], delta_evidence: list[dict[str, Any]] | None, ) -> None: """Persist exact adapter evidence after any registered snapshot.""" if delta_evidence is None: return previous_records = list(previous) current_records = list(current) operation_lineage = [ TopologyLineage( source_record_ids=tuple(item["source_record_ids"]), result_record_ids=tuple(item["result_record_ids"]), derivation=str(item["derivation"]), evidence="kernel_history", coverage=str(item["coverage"]), status=str(item["lineage_status"]), operation=topology_delta.operation, output_role=item.get("output_role"), feature_id=feature_id, source_kind=item.get("source_kind"), result_kind=item.get("result_kind"), ) for item in delta_evidence if not item.get("blend_transition") ] self._lineage.extend(operation_lineage) self._topology_deltas.append({ "feature_id": feature_id, "operation": topology_delta.operation, "input_snapshot_ids": sorted({str(record.body_id) for record in previous_records if record.body_id}), "output_snapshot_ids": sorted({str(record.body_id) for record in current_records if record.body_id}), "input_body_members": sorted({str(record.body_id) for record in previous_records if record.body_id}), "output_body_members": sorted({str(record.body_id) for record in current_records if record.body_id}), "history_status": topology_delta.history_status, **({"history_reason": topology_delta.history_reason} if topology_delta.history_reason else {}), "relations": delta_evidence, "lineage": [lineage.as_dict() for lineage in operation_lineage], }) def replace_body_topology( self, feature_id: str, body_id: str, records: Iterable[TopologyRecord], *, active_body_id: str | None = None, topology_delta: TopologyDelta | None = None, additional_predecessors: Iterable[TopologyRecord] = (), member_preservations: Iterable[tuple[str, str]] = (), ) -> None: self.replace_body_topologies( feature_id, [(body_id, records)], active_body_id=active_body_id, topology_delta=topology_delta, additional_predecessors=additional_predecessors, member_preservations=member_preservations, ) def replace_body_topologies( self, feature_id: str, bodies: Iterable[tuple[str, Iterable[TopologyRecord]]], *, active_body_id: str | None = None, topology_delta: TopologyDelta | None = None, additional_predecessors: Iterable[TopologyRecord] = (), member_preservations: Iterable[tuple[str, str]] = (), ) -> None: """Record a fresh B-rep snapshot after a feature mutates the body. OCC topology object identity is invalidated by most body mutations. We therefore keep old objects out of active selector resolution but carry their semantic owners forward when, and only when, one current object has one geometrically equivalent predecessor. A changed or split object intentionally becomes owned by this feature instead of being guessed as belonging to an older one. ``active_body_id`` names the whole-body group when ``body_id`` is a member of a multi-solid body (issue #7): the group id keeps the next mutation's predecessor lookup scoped to every solid of the previous body, while each member keeps its own ``body:{feature}:{index}`` id. """ active_previous = [ record for record in self._records if self._active_body_id is not None and record.body_id is not None and ( record.body_id == self._active_body_id or record.body_id.startswith(f"{self._active_body_id}:") ) ] # A pattern COPY can carry a chain of exact transform/boolean builder # histories before its final aggregate snapshot is registered. These # temporary records are valid predecessors only for that documented # kernel-history bridge. They are deliberately excluded from geometric # fallback matching: an equal-looking final face never proves that it # belongs to one particular copy instance. transient_previous = list(additional_predecessors) previous = [*active_previous, *transient_previous] # 同一 source feature 的 pattern copy 可以产生完全相同的几何面。它们 # 必须保留为多个实例,不能在跨 body 的全局 predecessor 匹配中互相消费。 # pattern 的 Compound 成员顺序是稳定的:已有实例以同一 member index # 延续,新增实例只会出现在末尾。按该 index 限定后继匹配。 current = [(body_id, list(records)) for body_id, records in bodies] current_records = [record for _body_id, records in current for record in records] ( exact_predecessors, exact_successors, kernel_covered_predecessors, exact_output_roles, exact_output_role_sources, delta_evidence, ) = self._exact_delta_links( topology_delta, previous, current_records, ) previous_member_ids = { suffix for record in active_previous for suffix in [str(record.body_id).rsplit(":", 1)[-1]] if suffix.isdigit() } use_member_indexes = len(current) > 1 and previous_member_ids consumed_predecessors: set[str] = set() registered: list[TopologyRecord] = [] for body_id, records in current: member_id = str(body_id).rsplit(":", 1)[-1] local_predecessors = [ record for record in active_previous if not use_member_indexes or str(record.body_id).rsplit(":", 1)[-1] == member_id ] for record in records: exact_predecessor_id = exact_predecessors.get(record.record_id) # Member order is a useful isolation boundary for geometric # fallback matching, especially for coincident pattern # copies. It is not a provenance boundary when a kernel # builder explicitly relates one source subshape to one # result subshape: boolean/delete lifecycle can remove an # earlier member and shift a surviving source to another # member index. A unique OCC continuation remains exact # evidence across that index change. predecessor = next( (prior for prior in previous if prior.record_id == exact_predecessor_id), None, ) if predecessor is None and record.record_id not in exact_predecessors: predecessor = self._unique_equivalent_predecessor(record, local_predecessors, consumed_predecessors) owners = predecessor.owners if predecessor is not None else (feature_id,) output_roles = set(record.output_roles) output_roles.update(exact_output_roles.get(record.record_id, ())) output_role_sources = set(record.output_role_sources) output_role_sources.update(exact_output_role_sources.get(record.record_id, ())) # A feature-output role can survive a later operation only # through the same unique kernel continuation used for owner # provenance. Geometry equivalence alone never carries it. if predecessor is not None and record.record_id in exact_predecessors: output_roles.update(predecessor.output_roles) if predecessor is not None and record.record_id not in exact_predecessors: consumed_predecessors.add(predecessor.record_id) registered.append(TopologyRecord( record_id=record.record_id, kind=record.kind, feature_id=feature_id, body_id=body_id, geometry=dict(record.geometry), value=record.value, owner_feature_ids=owners, output_roles=tuple(sorted(output_roles)), output_role_sources=tuple(sorted(output_role_sources)), source_entity=record.source_entity, source_entities=record.source_entities, )) for record in registered: self.register(record) for predecessor_id, successor_ids in exact_successors.items(): known = self._successors.setdefault(predecessor_id, []) for successor_id in successor_ids: if successor_id not in known: known.append(successor_id) # A compound assembly may retain an independent member verbatim while # another member is created or changed. Member identity is a body # lifecycle fact, not a geometric continuation: record it only when # the session supplied an exact old/new member pair and every exported # subshape has one reciprocal IsSame partner. for source_body_id, result_body_id in member_preservations: # ``kind == body`` records describe aggregate snapshots. They are # not B-rep members and a Compound member has no corresponding # aggregate record, so including them would reject an otherwise # exact face/edge/vertex preservation relation. source_records = [ record for record in active_previous if record.body_id == source_body_id and record.kind != "body" ] result_records = [ record for record in registered if record.body_id == result_body_id and record.kind != "body" ] pairs: list[tuple[TopologyRecord, TopologyRecord]] = [] for source in source_records: matches = [ result for result in result_records if result.kind == source.kind and self._same_topology_value(source.value, result.value) ] if len(matches) != 1: pairs = [] break pairs.append((source, matches[0])) if ( not pairs or len(source_records) != len(result_records) or len({result.record_id for _source, result in pairs}) != len(result_records) ): continue for source, result in pairs: self._lineage.append(TopologyLineage( source_record_ids=(source.record_id,), result_record_ids=(result.record_id,), derivation="continuation", evidence="kernel_history", coverage="complete", status="proven", operation="body_member_preserve", feature_id=feature_id, source_kind=source.kind, result_kind=result.kind, )) if topology_delta is not None: self._append_delta_evidence( feature_id, topology_delta, previous, current_records, delta_evidence, ) # #8 selector 持久性:被消费(拆分成段)的旧边记录演化后继,供后续 # selector 的 stable_id 引用解析到 active body 内的新形态。多条演化 # 候选时只登记"漂移显著最小"的那条(例如底面边圆角后既有缩短的直段 # 也有圆角过渡带的新边,前者的端点与原边重合、漂移更小);漂移并列 # (如竖直边被完整消费成两条等距直段)属于本质歧义,保守不登记。 for prior in previous: if ( prior.record_id in consumed_predecessors or prior.record_id in exact_successors or prior.record_id in kernel_covered_predecessors ): continue candidates = sorted( ( (self._evolved_drift(prior, record), record.record_id) for record in registered if self._evolved_equivalent(prior, record) ), key=lambda item: item[0], ) if not candidates: continue best, second = candidates[0], (candidates[1] if len(candidates) > 1 else None) if second is None or (second[0] - best[0]) > max(0.5, 0.2 * best[0]): self._successors[prior.record_id] = [best[1]] self._active_body_id = active_body_id or body_id @staticmethod def _same_topology_value(left: Any, right: Any) -> bool: """Compare adapter handles only through their exact topology identity.""" left_value = getattr(left, "wrapped", left) right_value = getattr(right, "wrapped", right) if left_value is right_value: return True for candidate, other in ((left_value, right_value), (right_value, left_value)): for method_name in ("IsSame", "is_same"): method = getattr(candidate, method_name, None) if callable(method): try: return bool(method(other)) except (AttributeError, TypeError, ValueError): continue return False @classmethod def _exact_delta_links( cls, topology_delta: TopologyDelta | None, previous: list[TopologyRecord], current: list[TopologyRecord], ) -> tuple[ dict[str, str], dict[str, list[str]], set[str], dict[str, tuple[str, ...]], dict[str, tuple[tuple[str, str, str], ...]], list[dict[str, Any]] | None, ]: """Bind opaque kernel history to snapshots without geometric guessing. Ownership transfer is intentionally limited to a single source item and a single output item. Split/merge history remains useful evidence, but has no unique owner continuation until a later operation-specific contract can express it. """ if topology_delta is None: return {}, {}, set(), {}, {}, None candidate_sources: dict[str, set[str]] = {} kernel_covered_predecessors: set[str] = set() output_roles: dict[str, set[str]] = {} output_role_sources: dict[str, set[tuple[str, str, str]]] = {} relation_links: list[tuple[str, str] | None] = [] evidence: list[dict[str, Any]] = [] for relation in topology_delta.relations: source_kind = relation.source_kind or relation.kind result_kind = relation.result_kind or relation.kind sources = [ record for record in previous if record.kind == source_kind and cls._same_topology_value(record.value, relation.source_value) ] outputs = [ record for record in current if record.kind == result_kind and any(cls._same_topology_value(record.value, value) for value in relation.result_values) ] item = { "event": relation.event, # Keep the legacy shorthand for same-kind consumers while # exposing both sides of a cross-topology builder relation. "kind": relation.kind, "source_kind": source_kind, "result_kind": result_kind, "source_record_ids": [record.record_id for record in sources], "result_record_ids": [record.record_id for record in outputs], "proof": "kernel_history", "derivation": relation.derivation, "coverage": relation.coverage if len(outputs) == len(relation.result_values) else "partial", "lineage_status": ( relation.status if topology_delta.history_status == "proven" else "unknown" ), } for field_name in ("source_role", "result_role", "source_slot", "result_slot"): value = getattr(relation, field_name) if value is not None: item[field_name] = value if relation.output_role is not None: item["output_role"] = relation.output_role role_is_unique = len(relation.result_values) == 1 and len(outputs) == 1 item["output_role_status"] = ( "unique_result_snapshot" if role_is_unique else "non_unique_or_missing_result_snapshot" ) if role_is_unique: output_roles.setdefault(outputs[0].record_id, set()).add(relation.output_role) for source in sources: for source_role in source.output_roles: for source_owner in source.owners: output_role_sources.setdefault(outputs[0].record_id, set()).add( (relation.output_role, source_owner, source_role) ) if len(sources) == 1: kernel_covered_predecessors.add(sources[0].record_id) can_transfer = ( relation.event in {"preserved", "modified"} and len(relation.result_values) == 1 and len(sources) == 1 and len(outputs) == 1 ) if can_transfer: source_id, result_id = sources[0].record_id, outputs[0].record_id candidate_sources.setdefault(result_id, set()).add(source_id) relation_links.append((source_id, result_id)) else: relation_links.append(None) evidence.append(item) for relation in topology_delta.section_relations: sources = [ [ record for record in previous if record.kind == "face" and cls._same_topology_value(record.value, source_value) ] for source_value in relation.source_values ] outputs = [ record for record in current if record.kind == "edge" and cls._same_topology_value(record.value, relation.result_value) ] source_records = [records[0] for records in sources if len(records) == 1] complete = ( len(source_records) == 2 and len({record.record_id for record in source_records}) == 2 and not cls._same_topology_value(source_records[0].value, source_records[1].value) and len(outputs) == 1 and topology_delta.history_status == "proven" and relation.coverage == "complete" and relation.status == "proven" ) evidence.append({ "event": "generated", "kind": "edge", "source_kind": "face", "result_kind": "edge", "source_record_ids": [record.record_id for record in source_records], "result_record_ids": [record.record_id for record in outputs], "proof": "kernel_history", "derivation": "intersection", "coverage": "complete" if complete else "partial", "lineage_status": "proven" if complete else "unknown", "section_edge": True, "source_qualified": True, "status": "source_qualified_section_edge" if complete else "incomplete_source_qualified_section_edge", }) if topology_delta.section_values: section_outputs = [ record for record in current if record.kind == "edge" and any(cls._same_topology_value(record.value, value) for value in topology_delta.section_values) ] evidence.append({ "event": "generated", "kind": "edge", "source_kind": "face", "result_kind": "edge", "source_record_ids": [], "result_record_ids": [record.record_id for record in section_outputs], "proof": "kernel_history", "derivation": "intersection", "coverage": "complete" if len(section_outputs) == len(topology_delta.section_values) else "partial", "lineage_status": ( "proven" if topology_delta.history_status == "proven" and len(section_outputs) == len(topology_delta.section_values) else "unknown" ), "section_edge": True, "status": "recorded_section_edge", }) predecessors = { result_id: next(iter(source_ids)) for result_id, source_ids in candidate_sources.items() if len(source_ids) == 1 } successors: dict[str, list[str]] = {} for result_id, source_id in predecessors.items(): successors.setdefault(source_id, []).append(result_id) for item, relation, link in zip(evidence, topology_delta.relations, relation_links): if link is not None: _source_id, result_id = link item["status"] = ( "unique_exact_continuation" if len(candidate_sources[result_id]) == 1 else "ambiguous_exact_continuation" ) elif relation.event in {"preserved", "modified"}: item["status"] = "non_unique_or_incomplete" else: item["status"] = "recorded_without_owner_transfer" if item["coverage"] != "complete" or relation.status != "proven": item["lineage_status"] = "unknown" for relation in topology_delta.blend_relations: edge_sources = [ record for record in previous if record.kind == "edge" and cls._same_topology_value(record.value, relation.source_edge_value) ] face_sources = [ record for record in previous if record.kind == "face" and cls._same_topology_value(record.value, relation.source_face_value) ] patch_faces = [ record for record in current if record.kind == "face" and relation.patch_face_value is not None and cls._same_topology_value(record.value, relation.patch_face_value) ] blend_into_results = [ record for record in current if record.kind == "face" and relation.blend_into_result_value is not None and cls._same_topology_value(record.value, relation.blend_into_result_value) ] outputs = [ record for record in current if record.kind == "edge" and any(cls._same_topology_value(record.value, value) for value in relation.result_values) ] complete = ( len(edge_sources) == 1 and len(face_sources) == 1 and len(patch_faces) == 1 and len(blend_into_results) == 1 and len(relation.result_values) == 1 and len(outputs) == 1 and topology_delta.history_status == "proven" and relation.coverage == "complete" and relation.status == "proven" ) evidence.append({ "event": "generated", "kind": "edge", "source_kind": "edge", "result_kind": "edge", "source_record_ids": [record.record_id for record in edge_sources], "blend_into_source_record_ids": [record.record_id for record in face_sources], "blend_into_result_record_ids": [record.record_id for record in blend_into_results], "patch_face_record_ids": [record.record_id for record in patch_faces], "result_record_ids": [record.record_id for record in outputs], "proof": "kernel_history", "derivation": "boundary", "coverage": "complete" if complete else "partial", "lineage_status": "proven" if complete else "unknown", "blend_transition": True, "status": "exact_blend_boundary" if complete else "incomplete_blend_boundary", }) return ( predecessors, successors, kernel_covered_predecessors, {record_id: tuple(sorted(roles)) for record_id, roles in output_roles.items()}, {record_id: tuple(sorted(sources)) for record_id, sources in output_role_sources.items()}, evidence, ) @staticmethod def _record_is_active(record: TopologyRecord, active_body_id: str | None) -> bool: return not record.transient and (active_body_id is None or ( record.body_id == active_body_id or (record.body_id is not None and record.body_id.startswith(f"{active_body_id}:")) )) def _operation_component( self, seed: TopologyLineage, *, output_role: str | None = None, unqualified_output_role: bool = False, result_connected_only: bool = False, ) -> tuple[TopologyLineage, ...]: """Return the connected N:M relation component for one operation. A builder can report A -> R and B -> R as two API callbacks. Treating either callback in isolation would mislabel the operation as 1:1 and let a selector bypass merge policy. Components are connected through exact final-snapshot records, never through geometric similarity. A single builder may also expose unrelated cross-topology facts, or intermediate handles that do not survive in the registered output. Those remain diagnostics in ``TopologyDelta`` but cannot turn one face -> face continuation into an apparent face fragment. Cardinality is therefore measured per complete/proven source/result topology pair. """ same_operation = [ edge for edge in self._lineage if edge.feature_id == seed.feature_id and edge.operation == seed.operation and edge.evidence == "kernel_history" and edge.coverage == "complete" and edge.status == "proven" and edge.source_kind == seed.source_kind and edge.result_kind == seed.result_kind and (output_role is None or edge.output_role == output_role) and (not unqualified_output_role or edge.output_role is None) ] component: list[TopologyLineage] = [] record_ids = set(seed.result_record_ids) if result_connected_only else ( set(seed.source_record_ids) | set(seed.result_record_ids) ) pending = True while pending: pending = False for edge in same_operation: edge_ids = set(edge.result_record_ids) if result_connected_only else ( set(edge.source_record_ids) | set(edge.result_record_ids) ) if edge not in component and record_ids.intersection(edge_ids): component.append(edge) before = len(record_ids) record_ids.update(edge_ids) pending = pending or len(record_ids) != before return tuple(component) def _operation_cardinality_error( self, edge: TopologyLineage, *, allowed: set[str], source_record_id: str, output_role: str | None = None, unqualified_output_role: bool = False, result_connected_only: bool = False, ) -> RuntimeDiagnostic | None: """Reject relation components whose N:M semantics the selector disallows. Builder APIs commonly surface a merge as separate callbacks. This applies the same component-level policy whether a selector starts at a stable source or directly names an output role. """ component = self._operation_component( edge, output_role=output_role, unqualified_output_role=unqualified_output_role, result_connected_only=result_connected_only, ) component_sources = set().union(*(set(item.source_record_ids) for item in component)) component_results = set().union(*(set(item.result_record_ids) for item in component)) required = set() if len(component_sources) > 1: required.add("merge") if len(component_results) > 1: required.add("fragment") if required.issubset(allowed): return None return RuntimeDiagnostic( "selector_relation_non_unique", "The topology operation's complete cardinality is not allowed by this selector policy", detail={ "source_record_id": source_record_id, "operation": edge.operation, "derivation": edge.derivation, "required": sorted(required), "allowed": sorted(allowed), "source_count": len(component_sources), "result_count": len(component_results), }, ) def _intent_lineage_successors( self, source_record_id: str, *, allowed: set[str], active_body_id: str | None, initial_output_role: str | None = None, target_kind: str | None = None, ) -> tuple[list[TopologyRecord], list[TopologyLineage], RuntimeDiagnostic | None]: """Traverse complete kernel lineage and enforce operation cardinality. ``all_fragments`` is deliberately stricter than collecting whatever active descendants happen to be registered: every branch reached from the source must have complete, proven evidence. """ pending = [source_record_id] visited = {source_record_id} result_ids: set[str] = set() used: list[TopologyLineage] = [] while pending: current = pending.pop(0) outgoing = [ edge for edge in self._lineage if current in edge.source_record_ids and (target_kind is None or edge.result_kind == target_kind) ] role_scope = initial_output_role if current == source_record_id else None unqualified_role_scope = current == source_record_id and role_scope is None if role_scope is not None: outgoing = [edge for edge in outgoing if edge.output_role == role_scope] elif unqualified_role_scope: # A direct prism exposes distinct cap-edge mappings through # explicit output roles. They are not fragments of the # unqualified edge -> side-face SWEPT relation. outgoing = [edge for edge in outgoing if edge.output_role is None] # An adapter can report intermediate builder handles which are not # members of the final snapshot. They carry ``partial`` evidence # with no record id and are retained in the delta for diagnosis, # but cannot be a reachable lineage branch. In contrast, a # partial relation that does bind a final record, or a complete # deletion, remains visible below and blocks resolution. outgoing = [ edge for edge in outgoing if edge.result_record_ids or edge.coverage == "complete" ] if not outgoing: if role_scope is not None: return [], used, RuntimeDiagnostic( "selector_kernel_history_missing", "No complete builder relation proves the requested source-profile output role", detail={ "source_record_id": current, "output_role": role_scope, "allowed": sorted(allowed), }, ) continue for edge in outgoing: if edge.evidence != "kernel_history" or edge.coverage != "complete" or edge.status != "proven": return [], used, RuntimeDiagnostic( "selector_kernel_history_missing", "Topology lineage is incomplete or unavailable for a reachable source branch", detail={"source_record_id": current, "operation": edge.operation, "coverage": edge.coverage, "status": edge.status}, ) cardinality_error = self._operation_cardinality_error( edge, allowed=allowed, source_record_id=current, output_role=role_scope, unqualified_output_role=unqualified_role_scope, ) if edge.derivation not in allowed or cardinality_error is not None: detail = cardinality_error.detail if cardinality_error is not None else { "source_record_id": current, "operation": edge.operation, "derivation": edge.derivation, "required": [], "allowed": sorted(allowed), } return [], used, RuntimeDiagnostic( "selector_relation_non_unique", "The topology operation's complete cardinality is not allowed by this selector policy", detail=detail, ) if edge not in used: used.append(edge) for record_id in edge.result_record_ids: result_ids.add(record_id) if record_id not in visited: visited.add(record_id) pending.append(record_id) if not used: return [], used, RuntimeDiagnostic( "selector_kernel_history_missing", "No complete proven lineage starts at the selector source", detail={"source_record_id": source_record_id, "allowed": sorted(allowed)}, ) active = [ record for record in self._records if record.record_id in result_ids and self._record_is_active(record, active_body_id) ] if not active: return [], used, RuntimeDiagnostic( "selector_body_member_inactive", "The proven selector lineage does not reach an active body member", detail={"source_record_id": source_record_id, "active_body_id": active_body_id}, ) return active, used, None @staticmethod def _numbers_equal(left: Any, right: Any, *, tolerance: float = 1e-6) -> bool: try: return abs(float(left) - float(right)) <= tolerance except (TypeError, ValueError): return False @classmethod def _vectors_equal(cls, left: Any, right: Any, *, tolerance: float = 1e-6) -> bool: try: first = _vector3(left, field_name="prior topology geometry") second = _vector3(right, field_name="current topology geometry") except ValueError: return False return all(abs(a - b) <= tolerance for a, b in zip(first, second)) @classmethod def _geometry_equivalent(cls, prior: TopologyRecord, current: TopologyRecord) -> bool: """Check a complete, orientation-aware snapshot signature. This is intentionally much stricter than selector scoring. Selector scoring may compare partial source evidence; provenance transfer must never manufacture ownership from a merely similar candidate. """ if prior.kind != current.kind: return False left, right = prior.geometry, current.geometry for key in ("surface_type", "curve_type"): if left.get(key) != right.get(key): return False for key in ("bbox_mm", "center_mm", "normal", "plane_normal"): if key in left or key in right: if key not in left or key not in right: return False left_value, right_value = left[key], right[key] if key == "bbox_mm": if not isinstance(left_value, (list, tuple)) or not isinstance(right_value, (list, tuple)): return False if len(left_value) != 6 or len(right_value) != 6: return False if not all(cls._numbers_equal(a, b) for a, b in zip(left_value, right_value)): return False elif not cls._vectors_equal(left_value, right_value): return False for key in ("area_mm2", "length_mm", "plane_offset_mm"): if key in left or key in right: if key not in left or key not in right or not cls._numbers_equal(left[key], right[key]): return False for key in ("adjacency_signature", "adjacent_face_count", "incident_edge_count"): if key in left or key in right: if key not in left or key not in right or left[key] != right[key]: return False left_start, left_end = left.get("start_mm"), left.get("end_mm") right_start, right_end = right.get("start_mm"), right.get("end_mm") if any(value is not None for value in (left_start, left_end, right_start, right_end)): if None in (left_start, left_end, right_start, right_end): return False same_direction = cls._vectors_equal(left_start, right_start) and cls._vectors_equal(left_end, right_end) reverse_direction = cls._vectors_equal(left_start, right_end) and cls._vectors_equal(left_end, right_start) if not same_direction and not reverse_direction: return False return True @classmethod def _unique_equivalent_predecessor( cls, current: TopologyRecord, predecessors: Iterable[TopologyRecord], consumed_predecessors: set[str], ) -> TopologyRecord | None: matches = [ record for record in predecessors if record.record_id not in consumed_predecessors and cls._geometry_equivalent(record, current) ] return matches[0] if len(matches) == 1 else None @staticmethod def _evolved_drift(prior: TopologyRecord, current: TopologyRecord) -> float | None: """Endpoint drift between direction-aligned straight edges. Returns the minimum total endpoint drift (mm) when the two edges are collinear straight lines (either orientation), otherwise ``None``. """ if prior.kind != current.kind: return None left, right = prior.geometry, current.geometry if left.get("curve_type") != "line" or right.get("curve_type") != "line": return None if None in (left.get("start_mm"), left.get("end_mm"), right.get("start_mm"), right.get("end_mm")): return None def _delta(a: tuple[float, float, float], b: tuple[float, float, float]) -> tuple[float, float, float]: return (b[0] - a[0], b[1] - a[1], b[2] - a[2]) def _dist(a: tuple[float, float, float], b: tuple[float, float, float]) -> float: return sqrt(sum((a[i] - b[i]) ** 2 for i in range(3))) left_dir = _delta(left["start_mm"], left["end_mm"]) right_dir = _delta(right["start_mm"], right["end_mm"]) if _length(left_dir) <= 1e-9 or _length(right_dir) <= 1e-9: return None cross = ( left_dir[1] * right_dir[2] - left_dir[2] * right_dir[1], left_dir[2] * right_dir[0] - left_dir[0] * right_dir[2], left_dir[0] * right_dir[1] - left_dir[1] * right_dir[0], ) if _length(cross) / (_length(left_dir) * _length(right_dir)) > 1e-3: return None same_order = _dist(left["start_mm"], right["start_mm"]) + _dist(left["end_mm"], right["end_mm"]) reversed_order = _dist(left["start_mm"], right["end_mm"]) + _dist(left["end_mm"], right["start_mm"]) return min(same_order, reversed_order) @classmethod def _evolved_equivalent(cls, prior: TopologyRecord, current: TopologyRecord, *, drift_mm: float = 5.0) -> bool: """Loose "position trajectory" equivalence used for evolved successors. Unlike ``_geometry_equivalent`` (strict, anti-false-positive provenance), this deliberately tolerates small endpoint drift: fillet/chamfer split a straight edge into segments (a middle straight run plus end arcs). The straight run keeps the same direction and stays within ``drift_mm`` of the original edge, so it can serve as the edge's evolved successor. Uniqueness is enforced by the caller (only a single best candidate is recorded). """ drift = cls._evolved_drift(prior, current) return drift is not None and drift <= drift_mm @staticmethod def _vector_score(expected: Any, actual: Any, tolerance: float = 1e-4) -> float | None: try: left = _vector3(expected, field_name="selector geometry") right = _vector3(actual, field_name="record geometry") except ValueError: return None error = _length(tuple(a - b for a, b in zip(left, right))) return max(0.0, 1.0 - error / tolerance) @classmethod def _undirected_vector_score(cls, expected: Any, actual: Any, tolerance: float = 1e-4) -> tuple[float | None, bool]: """Score a geometric axis/plane normal independent of orientation.""" try: left = _vector3(expected, field_name="selector geometry") right = _vector3(actual, field_name="record geometry") except ValueError: return None, False direct = _length(tuple(a - b for a, b in zip(left, right))) reversed_error = _length(tuple(a + b for a, b in zip(left, right))) reversed_direction = reversed_error < direct return max(0.0, 1.0 - min(direct, reversed_error) / tolerance), reversed_direction @classmethod def _geometry_score(cls, selector_geometry: dict[str, Any], record_geometry: dict[str, Any]) -> float | None: if not selector_geometry: return 0.0 scores: list[float] = [] reversed_plane_normal = False for key in ("center_mm", "circle_center_mm", "normal", "origin_mm", "direction", "plane_normal", "start_mm", "end_mm", "axis_origin_mm", "axis_direction"): if key in selector_geometry: # A cap source identifies a geometric plane, not OCC's face # orientation. When plane offset is present, prefer the # adapter's canonical plane normal for a consistent equation. actual = record_geometry.get("plane_normal") if key == "normal" and "plane_offset_mm" in selector_geometry else record_geometry.get(key) if key in {"normal", "plane_normal", "axis_direction", "direction"}: score, reversed_direction = cls._undirected_vector_score(selector_geometry[key], actual) if key == "normal" and "plane_offset_mm" in selector_geometry: reversed_plane_normal = reversed_direction else: score = cls._vector_score(selector_geometry[key], actual) if score is None: return None scores.append(score) for key in ("surface_type", "curve_type"): if key in selector_geometry: if record_geometry.get(key) != selector_geometry[key]: # #8 selector 持久性:fillet/chamfer 会把直线边演化为圆弧、 # 平面演化为柱面,但被选中拓扑的位置锚定(bbox/center/端点) # 不变。曲线/曲面类型变化不再一票否决,而是记低分:位置完全 # 重合的候选(同一条边的形态演化)仍可胜出;位置不重合的 # 相邻边会被 0 分项拉低,仍被 minimum_score 挡住。 scores.append(0.5) else: scores.append(1.0) if "bbox_mm" in selector_geometry: expected = selector_geometry["bbox_mm"] actual = record_geometry.get("bbox_mm") if not isinstance(expected, list) or not isinstance(actual, list) or len(expected) != len(actual): return None delta = max(abs(float(a) - float(b)) for a, b in zip(expected, actual)) scores.append(max(0.0, 1.0 - delta / 1e-4)) if "plane_offset_mm" in selector_geometry: try: actual_offset = float(record_geometry.get("plane_offset_mm")) if reversed_plane_normal: actual_offset = -actual_offset delta = abs(float(selector_geometry["plane_offset_mm"]) - actual_offset) except (TypeError, ValueError): return None scores.append(max(0.0, 1.0 - delta / 1e-4)) if "radius_mm" in selector_geometry: try: delta = abs(float(selector_geometry["radius_mm"]) - float(record_geometry.get("radius_mm"))) except (TypeError, ValueError): return None scores.append(max(0.0, 1.0 - delta / 1e-4)) if "area_mm2" in selector_geometry: try: expected_area = float(selector_geometry["area_mm2"]) actual_area = float(record_geometry.get("area_mm2")) except (TypeError, ValueError): return None relative_delta = abs(expected_area - actual_area) / max(abs(expected_area), 1e-9) scores.append(max(0.0, 1.0 - relative_delta / 1e-4)) if "minimum_area_mm2" in selector_geometry: try: if float(record_geometry.get("area_mm2")) + 1e-6 < float(selector_geometry["minimum_area_mm2"]): return None except (TypeError, ValueError): return None return sum(scores) / len(scores) if scores else 0.0 @staticmethod def _dot(left: list[float], right: list[float]) -> float: return sum(float(a) * float(b) for a, b in zip(left, right)) @classmethod def _source_circle_candidates( cls, geometry: dict[str, Any], candidates: Iterable[TopologyRecord], ) -> list[TopologyRecord]: """Resolve CADFS' explicit source-circle geometry without a heuristic. This is a declared geometric selector for ordinary CDSL, not a FeatureScript provenance fallback. The same implementation is used by live runtime and prefix-binding report rehydration. """ center = geometry.get("source_circle_center_mm") normal = geometry.get("source_plane_normal") try: radius = float(geometry.get("source_circle_radius_mm") or 0) except (TypeError, ValueError): return [] if not isinstance(center, list) or not isinstance(normal, list) or radius <= 0: return [] plane_offset = cls._dot(center, normal) matches = [] for record in candidates: record_geometry = record.geometry if record.kind != "edge" or record_geometry.get("curve_type") != "circle": continue points = [record_geometry.get("start_mm"), record_geometry.get("end_mm")] if not all(isinstance(point, list) and len(point) == 3 for point in points): continue if any(abs(cls._dot(point, normal) - plane_offset) > 0.05 for point in points): continue radial = [] for point in points: delta = [float(point[index]) - float(center[index]) for index in range(3)] axial = cls._dot(delta, normal) radial.append(sqrt(max(0.0, sum(value * value for value in delta) - axial * axial))) if all(abs(value - radius) <= max(0.05, radius * 1e-4) for value in radial): matches.append(record) return matches def _intersection_source_face( self, source: dict[str, Any], ) -> tuple[TopologyRecord | None, list[TopologyLineage], RuntimeDiagnostic | None]: """Resolve one historical face anchor for a qualified section query. This deliberately resolves the *input snapshot* of a boolean, rather than whichever descendant happens to be active now. CAP faces are identified by their exact builder role; direct-prism side faces start at one source-profile edge anchor. Both paths require the runtime facts registered when the input body was built. """ family = source.get("query_family") owner = source.get("owner_feature_id") if not isinstance(owner, str) or not owner: return None, [], RuntimeDiagnostic( "selector_source_unavailable", "An INTERSECT source face has no owning feature", ) if family == "CAP_FACE": role = source.get("output_role") if role not in {"extrude.start", "extrude.end"}: return None, [], RuntimeDiagnostic( "selector_source_unavailable", "A CAP_FACE INTERSECT source requires one direct prism cap role", detail={"owner_feature_id": owner, "output_role": role}, ) # Output roles propagate over exact continuations so ordinary # role resolution can reach an active descendant. An INTERSECT # source instead identifies the historical boolean input. Read # only the owner's exact builder-role fact; cached roles on later # descendants are not additional source snapshots. role_facts = [ relation for delta in self._topology_deltas if delta.get("feature_id") == owner and delta.get("operation") == "extrude" for relation in delta.get("relations") or () if relation.get("output_role") == role and relation.get("coverage") == "complete" and relation.get("lineage_status") == "proven" and relation.get("result_kind") == "face" ] if len(role_facts) != 1: return None, [], RuntimeDiagnostic( "selector_kernel_history_missing" if not role_facts else "selector_relation_non_unique", "The CAP_FACE input has no unique complete builder-role fact", detail={"owner_feature_id": owner, "output_role": role, "fact_count": len(role_facts)}, ) result_ids = role_facts[0].get("result_record_ids") if not isinstance(result_ids, list) or len(result_ids) != 1: return None, [], RuntimeDiagnostic( "selector_relation_non_unique", "The CAP_FACE builder role does not identify one face snapshot", detail={"owner_feature_id": owner, "output_role": role, "result_count": len(result_ids or ())}, ) candidates = [ record for record in self._records if record.record_id == result_ids[0] and record.kind == "face" and record.feature_id == owner ] if len(candidates) != 1: return None, [], RuntimeDiagnostic( "selector_kernel_history_missing" if not candidates else "selector_ambiguous", "The CAP_FACE builder-role snapshot is unavailable or non-unique", detail={"owner_feature_id": owner, "output_role": role, "candidate_count": len(candidates)}, ) return candidates[0], [], None if family == "SWEPT_FACE": source_entity = source.get("source_entity") if not isinstance(source_entity, dict): return None, [], RuntimeDiagnostic( "selector_source_unavailable", "A SWEPT_FACE INTERSECT source requires one source-profile edge", detail={"owner_feature_id": owner}, ) sketch_id, entity_id = source_entity.get("sketch_id"), source_entity.get("entity_id") if not isinstance(sketch_id, str) or not sketch_id or not isinstance(entity_id, str) or not entity_id: return None, [], RuntimeDiagnostic( "selector_source_unavailable", "The SWEPT_FACE source-profile edge is incomplete", detail={"owner_feature_id": owner}, ) anchors = [ record for record in self._records if record.kind == "edge" and record.source_entity == (sketch_id, entity_id) ] if len(anchors) != 1: return None, [], RuntimeDiagnostic( "selector_source_unavailable" if not anchors else "selector_ambiguous", "The SWEPT_FACE source-profile edge is unavailable or non-unique", detail={"owner_feature_id": owner, "candidate_count": len(anchors)}, ) relations = [ edge for edge in self._lineage if edge.feature_id == owner and edge.operation == "extrude" and edge.derivation == "boundary" and edge.coverage == "complete" and edge.status == "proven" and edge.source_kind == "edge" and edge.result_kind == "face" and edge.source_record_ids == (anchors[0].record_id,) and len(edge.result_record_ids) == 1 ] result_ids = {record_id for edge in relations for record_id in edge.result_record_ids} candidates = [ record for record in self._records if record.record_id in result_ids and record.kind == "face" and (owner in record.owners or record.feature_id == owner) ] if len(relations) != 1 or len(candidates) != 1: return None, relations, RuntimeDiagnostic( "selector_kernel_history_missing" if not candidates else "selector_relation_non_unique", "The SWEPT_FACE input has no unique complete direct-prism relation", detail={"owner_feature_id": owner, "relation_count": len(relations), "candidate_count": len(candidates)}, ) return candidates[0], relations, None return None, [], RuntimeDiagnostic( "selector_query_unsupported", "INTERSECT currently supports only CAP_FACE and direct-prism SWEPT_FACE inputs", detail={"query_family": family}, ) def _resolve_intersection_intent( self, selector: dict[str, Any], intent: dict[str, Any], *, allowed: set[str], multiplicity: str, active_body_id: str | None, ) -> SelectorResolution: """Resolve one two-face source-qualified boolean section edge.""" if selector.get("kind") != "edge" or allowed != {"intersection"} or multiplicity != "one": return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_query_unsupported", "INTERSECT requires exactly the intersection/one derivation policy", detail={"kind": selector.get("kind"), "allowed": sorted(allowed), "multiplicity": multiplicity}, ), ) sources = intent.get("intersection_sources") if not isinstance(sources, list) or len(sources) != 2 or not all(isinstance(source, dict) for source in sources): return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_source_unavailable", "INTERSECT requires exactly two explicit source face anchors", ), ) source_records: list[TopologyRecord] = [] source_relations: list[TopologyLineage] = [] for source in sources: record, relations, error = self._intersection_source_face(source) if error is not None or record is None: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=error or RuntimeDiagnostic("selector_source_unavailable", "INTERSECT source face is unavailable"), ) source_records.append(record) source_relations.extend(relations) if self._same_topology_value(source_records[0].value, source_records[1].value): return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_source_unavailable", "INTERSECT source anchors must resolve to two distinct faces", detail={"source_record_ids": [record.record_id for record in source_records]}, ), ) relations = [] for edge in self._lineage: if ( edge.derivation != "intersection" or edge.coverage != "complete" or edge.status != "proven" or edge.source_kind != "face" or edge.result_kind != "edge" or len(edge.source_record_ids) != 2 or len(edge.result_record_ids) != 1 ): continue relation_sources = [ record for record in self._records if record.record_id in edge.source_record_ids and record.kind == "face" ] if len(relation_sources) != 2: continue # Boolean aggregation may give an unchanged input member a new # snapshot record ID. Exact IsSame, not role copying or geometry, # connects the historical source anchor to that input record. matches = [ [ relation_source for relation_source in relation_sources if self._same_topology_value(anchor.value, relation_source.value) ] for anchor in source_records ] if ( len(matches[0]) == 1 and len(matches[1]) == 1 and matches[0][0].record_id != matches[1][0].record_id ): relations.append(edge) if len(relations) != 1: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_kernel_history_missing" if not relations else "selector_relation_non_unique", "No unique complete source-qualified section edge matches the two source faces", detail={"source_record_ids": [record.record_id for record in source_records], "relation_count": len(relations)}, ), ) result = next(( record for record in self._records if record.record_id == relations[0].result_record_ids[0] and record.kind == "edge" ), None) if result is None: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic("selector_kernel_history_missing", "The section edge is absent from the result snapshot"), ) if not self._record_is_active(result, active_body_id): return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_body_member_inactive", "The source-qualified section edge is not in the active body member", detail={"record_id": result.record_id, "active_body_id": active_body_id}, ), ) return SelectorResolution( selector=selector, status="resolved", record=result, candidates=({"score": 1.0, **result.public_dict()},), resolution_mode="kernel_intersection", evidence={ "source_query": intent.get("source_query"), "semantic_anchor": {"type": "source_qualified_section", "sources": sources}, "source_records": [record.record_id for record in source_records], "result_records": [result.record_id], "relations": [edge.as_dict() for edge in [*source_relations, relations[0]]], "body_member": result.body_id, "policy": intent.get("derivation_policy"), }, ) def _resolve_blend_edge_intent( self, selector: dict[str, Any], intent: dict[str, Any], *, active_body_id: str | None, ) -> SelectorResolution: """Resolve one complete exact dress-up patch-boundary transition.""" sources = intent.get("blend_sources") owner = selector.get("owner_feature_id") def unresolved(code: str, message: str, detail: dict[str, Any]) -> SelectorResolution: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic(code, message, detail=detail), ) if not isinstance(sources, dict) or not isinstance(owner, str): return unresolved("selector_source_unavailable", "BLEND_EDGE has no explicit dress-up source contract", {}) edge_source = sources.get("edge") face_source = sources.get("face") if not isinstance(edge_source, dict) or not isinstance(face_source, dict): return unresolved("selector_source_unavailable", "BLEND_EDGE source pair is incomplete", {}) source_owner = edge_source.get("owner_feature_id") source_entity = edge_source.get("source_entity") edge_role = edge_source.get("lineage_role") face_family = face_source.get("query_family") face_role = face_source.get("output_role") if ( edge_source.get("query_family") != "CAP_EDGE" or face_family not in {"CAP_FACE", "SWEPT_FACE"} or source_owner != face_source.get("owner_feature_id") or not isinstance(source_owner, str) or not isinstance(source_entity, dict) or edge_role not in {"extrude.start", "extrude.end"} ): return unresolved("selector_source_unavailable", "BLEND_EDGE source pair violates the direct-prism contract", {}) if face_family == "SWEPT_FACE" and face_source.get("source_entity") != source_entity: return unresolved( "selector_source_unavailable", "BLEND_EDGE swept-face source must match its cap-edge anchor", {}, ) sketch_id, entity_id = source_entity.get("sketch_id"), source_entity.get("entity_id") if not isinstance(sketch_id, str) or not isinstance(entity_id, str): return unresolved("selector_source_unavailable", "BLEND_EDGE edge source lacks a sketch entity", {}) anchors = [ record for record in self._records if record.kind == "edge" and record.source_entity == (sketch_id, entity_id) ] if len(anchors) != 1: return unresolved( "selector_source_unavailable" if not anchors else "selector_ambiguous", "BLEND_EDGE source edge anchor is unavailable or ambiguous", {"anchor_count": len(anchors), "source_entity": source_entity}, ) cap_edges = [ edge for edge in self._lineage if edge.feature_id == source_owner and edge.operation == "extrude" and edge.source_kind == "edge" and edge.result_kind == "edge" and edge.source_record_ids == (anchors[0].record_id,) and edge.output_role == edge_role and edge.coverage == "complete" and edge.status == "proven" and len(edge.result_record_ids) == 1 ] cap_edge_ids = {record_id for edge in cap_edges for record_id in edge.result_record_ids} if face_family == "CAP_FACE": source_faces = [ record for record in self._records if record.kind == "face" and record.feature_id == source_owner and face_role in record.output_roles ] if face_role in {"extrude.start", "extrude.end"} else [] else: face_entity = face_source.get("source_entity") face_sketch_id = face_entity.get("sketch_id") if isinstance(face_entity, dict) else None face_entity_id = face_entity.get("entity_id") if isinstance(face_entity, dict) else None face_anchors = [ record for record in self._records if record.kind == "edge" and record.source_entity == (face_sketch_id, face_entity_id) ] face_relations = [ relation for relation in self._lineage if relation.feature_id == source_owner and relation.operation == "extrude" and relation.source_kind == "edge" and relation.result_kind == "face" and len(face_anchors) == 1 and relation.source_record_ids == (face_anchors[0].record_id,) and relation.coverage == "complete" and relation.status == "proven" and len(relation.result_record_ids) == 1 ] source_face_ids = {record_id for relation in face_relations for record_id in relation.result_record_ids} source_faces = [ record for record in self._records if record.record_id in source_face_ids and record.kind == "face" ] if len(cap_edge_ids) != 1 or len(source_faces) != 1: return unresolved( "selector_source_unavailable" if not cap_edge_ids or not source_faces else "selector_ambiguous", "BLEND_EDGE direct source edge or cap face is unavailable or ambiguous", {"cap_edge_count": len(cap_edge_ids), "cap_face_count": len(source_faces)}, ) source_edge_id = next(iter(cap_edge_ids)) source_face_id = source_faces[0].record_id matching = [ relation for delta in self._topology_deltas if delta.get("feature_id") == owner for relation in delta.get("relations") or () if relation.get("blend_transition") is True and relation.get("status") == "exact_blend_boundary" and relation.get("coverage") == "complete" and relation.get("lineage_status") == "proven" and relation.get("source_record_ids") == [source_edge_id] and relation.get("blend_into_source_record_ids") == [source_face_id] and len(relation.get("blend_into_result_record_ids") or ()) == 1 and len(relation.get("patch_face_record_ids") or ()) == 1 and len(relation.get("result_record_ids") or ()) == 1 ] result_ids = {relation["result_record_ids"][0] for relation in matching} active_results = [ record for record in self._records if record.record_id in result_ids and record.kind == "edge" and self._record_is_active(record, active_body_id) ] if len(matching) != 1 or len(active_results) != 1: return unresolved( "selector_relation_non_unique" if matching or result_ids else "selector_kernel_history_missing", "BLEND_EDGE has no unique complete exact dress-up boundary in the active body", {"relation_count": len(matching), "active_result_count": len(active_results)}, ) record = active_results[0] return SelectorResolution( selector=selector, status="resolved", record=record, candidates=({"score": 1.0, **record.public_dict()},), resolution_mode="blend_boundary", evidence={ "source_query": intent.get("source_query"), "source_records": [source_edge_id, source_face_id], "result_records": [record.record_id], "blend_relation": matching[0], "policy": intent.get("derivation_policy"), }, ) def _resolve_blend_face_intent( self, selector: dict[str, Any], intent: dict[str, Any], *, active_body_id: str | None, ) -> SelectorResolution: """Resolve one exact active dress-up patch face from its input edge.""" owner = selector.get("owner_feature_id") source = intent.get("blend_face_source") def unresolved(code: str, message: str, detail: dict[str, Any]) -> SelectorResolution: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic(code, message, detail=detail), ) if not isinstance(owner, str) or not isinstance(source, dict): return unresolved("selector_source_unavailable", "BLEND_FACE has no explicit direct-prism source contract", {}) source_owner = source.get("owner_feature_id") source_entity = source.get("source_entity") role = source.get("lineage_role") if ( source.get("query_family") != "CAP_EDGE" or not isinstance(source_owner, str) or not isinstance(source_entity, dict) or role not in {"extrude.start", "extrude.end"} ): return unresolved("selector_source_unavailable", "BLEND_FACE source violates the direct-prism contract", {}) sketch_id, entity_id = source_entity.get("sketch_id"), source_entity.get("entity_id") if not isinstance(sketch_id, str) or not isinstance(entity_id, str): return unresolved("selector_source_unavailable", "BLEND_FACE source lacks one sketch edge", {}) anchors = [ record for record in self._records if record.kind == "edge" and record.source_entity == (sketch_id, entity_id) ] if len(anchors) != 1: return unresolved( "selector_source_unavailable" if not anchors else "selector_ambiguous", "BLEND_FACE source edge anchor is unavailable or ambiguous", {"anchor_count": len(anchors), "source_entity": source_entity}, ) cap_relations = [ relation for relation in self._lineage if relation.feature_id == source_owner and relation.operation == "extrude" and relation.source_kind == "edge" and relation.result_kind == "edge" and relation.source_record_ids == (anchors[0].record_id,) and relation.output_role == role and relation.coverage == "complete" and relation.status == "proven" and len(relation.result_record_ids) == 1 ] cap_ids = {record_id for relation in cap_relations for record_id in relation.result_record_ids} if len(cap_ids) != 1: return unresolved( "selector_source_unavailable" if not cap_ids else "selector_ambiguous", "BLEND_FACE role-qualified direct source edge is unavailable or ambiguous", {"cap_edge_count": len(cap_ids)}, ) cap_id = next(iter(cap_ids)) patch_relations = [ relation for relation in self._lineage if relation.feature_id == owner and relation.operation in {"fillet", "chamfer"} and relation.derivation == "boundary" and relation.source_kind == "edge" and relation.result_kind == "face" and relation.source_record_ids == (cap_id,) and relation.coverage == "complete" and relation.status == "proven" and len(relation.result_record_ids) == 1 ] patch_ids = {record_id for relation in patch_relations for record_id in relation.result_record_ids} active_patches = [ record for record in self._records if record.record_id in patch_ids and record.kind == "face" and self._record_is_active(record, active_body_id) ] if len(patch_relations) != 1 or len(active_patches) != 1: return unresolved( "selector_relation_non_unique" if patch_relations or patch_ids else "selector_kernel_history_missing", "BLEND_FACE has no unique complete generated patch face in the active body", {"relation_count": len(patch_relations), "active_result_count": len(active_patches)}, ) record = active_patches[0] return SelectorResolution( selector=selector, status="resolved", record=record, candidates=({"score": 1.0, **record.public_dict()},), resolution_mode="blend_patch_face", evidence={ "source_query": intent.get("source_query"), "source_records": [anchors[0].record_id, cap_id], "result_records": [record.record_id], "relations": [relation.as_dict() for relation in [cap_relations[0], patch_relations[0]]], "body_member": record.body_id, "policy": intent.get("derivation_policy"), }, ) def _resolve_proven_operand_set( self, selector: dict[str, Any], *, active_body_id: str | None, ) -> SelectorResolution: """Evaluate one direct query set from exact active child record IDs.""" operands = selector.get("query_operands") kind = selector.get("kind") intent = selector["selector_intent"] contract = intent.get("query_set_contract") operator = { "proven_operand_union": "union", "proven_operand_intersection": "intersection", "proven_operand_subtraction": "subtraction", }.get(contract) source_name = { "union": "qUnion", "intersection": "qIntersection", "subtraction": "qSubtraction", }.get(operator, "query set") if not isinstance(operands, list): # Defensive: the public gate checked this. return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_query_set_invalid", "The FeatureScript query-set selector has no executable operands", ), ) records: list[TopologyRecord] = [] seen_record_ids: set[str] = set() operand_record_sets: list[tuple[TopologyRecord, ...]] = [] operand_evidence: list[dict[str, Any]] = [] for index, operand in enumerate(operands): resolution = self.resolve(operand, active_body_id=active_body_id) resolved_records = resolution.records or ((resolution.record,) if resolution.record is not None else ()) if resolution.status != "resolved" or not resolved_records: cause = resolution.diagnostic return SelectorResolution( selector=selector, status="not_found", candidates=tuple({"score": 1.0, **record.public_dict()} for record in records), diagnostic=RuntimeDiagnostic( cause.code if cause is not None else "selector_query_operand_unresolved", f"{source_name} operand {index} did not resolve to a proven active result", detail={ "operand_index": index, **({"cause": cause.as_dict()} if cause is not None else {}), }, ), ) for record in resolved_records: if record.kind != kind: return SelectorResolution( selector=selector, status="not_found", candidates=tuple({"score": 1.0, **candidate.public_dict()} for candidate in records), diagnostic=RuntimeDiagnostic( "selector_query_set_kind_mismatch", f"A {source_name} operand resolved to a topology kind outside the declared set", detail={"operand_index": index, "expected_kind": kind, "record_kind": record.kind}, ), ) if not self._record_is_active(record, active_body_id): return SelectorResolution( selector=selector, status="not_found", candidates=tuple({"score": 1.0, **candidate.public_dict()} for candidate in records), diagnostic=RuntimeDiagnostic( "selector_body_member_inactive", f"A {source_name} operand does not resolve inside the active body member", detail={"operand_index": index, "record_id": record.record_id, "active_body_id": active_body_id}, ), ) if record.record_id not in seen_record_ids: seen_record_ids.add(record.record_id) records.append(record) operand_record_sets.append(tuple(resolved_records)) operand_evidence.append({ "operand_index": index, "resolution_mode": resolution.resolution_mode, "result_records": [record.record_id for record in resolved_records], "relations": list(resolution.evidence.get("relations") or ()), }) if operator == "intersection": shared_ids = { record.record_id for record in operand_record_sets[0] } for operand_records in operand_record_sets[1:]: shared_ids.intersection_update(record.record_id for record in operand_records) records = [] seen_record_ids.clear() for record in operand_record_sets[0]: if record.record_id in shared_ids and record.record_id not in seen_record_ids: seen_record_ids.add(record.record_id) records.append(record) elif operator == "subtraction": excluded_ids = {record.record_id for record in operand_record_sets[1]} records = [] seen_record_ids.clear() for record in operand_record_sets[0]: if record.record_id not in excluded_ids and record.record_id not in seen_record_ids: seen_record_ids.add(record.record_id) records.append(record) if not records: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_query_empty", f"The {source_name} selector resolved no active topology records", detail={"operator": operator}, ), ) public_records = tuple({"score": 1.0, **record.public_dict()} for record in records) return SelectorResolution( selector=selector, status="resolved", records=tuple(records), candidates=public_records, resolution_mode=f"query_set_{operator}", evidence={ "source_query": intent.get("source_query"), "query_expr": intent.get("query_expr"), "set_contract": intent.get("query_set_contract"), "set_operator": operator, "operand_resolutions": operand_evidence, "source_records": [], "result_records": [record.record_id for record in records], "body_members": [record.body_id for record in records], "policy": intent.get("derivation_policy"), }, ) def _resolve_owner_body_selector( self, selector: dict[str, Any], *, active_body_id: str | None, ) -> SelectorResolution: """Project one exact active topology result to its owning body record.""" query_input = selector.get("query_input") if not isinstance(query_input, dict): # Defensive: the public gate checked this. return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_owner_body_invalid", "The qOwnerBody selector has no executable topology input", ), ) input_resolution = self.resolve(query_input, active_body_id=active_body_id) input_records = input_resolution.records or ( (input_resolution.record,) if input_resolution.record is not None else () ) if input_resolution.status != "resolved" or not input_records: cause = input_resolution.diagnostic return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_owner_body_input_unresolved", "The qOwnerBody input did not resolve to proven active topology", detail={ **({"cause": cause.as_dict()} if cause is not None else {}), }, ), ) if len(input_records) != 1: return SelectorResolution( selector=selector, status="not_found", candidates=tuple({"score": 1.0, **record.public_dict()} for record in input_records), diagnostic=RuntimeDiagnostic( "selector_owner_body_ambiguous", "The qOwnerBody input resolved to more than one topology member", detail={"input_record_ids": [record.record_id for record in input_records]}, ), ) input_record = input_records[0] if ( input_record.body_id is None or active_body_id is None or input_record.body_id != active_body_id or not self._record_is_active(input_record, active_body_id) ): return SelectorResolution( selector=selector, status="not_found", candidates=({"score": 1.0, **input_record.public_dict()},), diagnostic=RuntimeDiagnostic( "selector_owner_body_inactive", "The qOwnerBody input is not an exact member of the active body", detail={ "input_record_id": input_record.record_id, "input_body_id": input_record.body_id, "active_body_id": active_body_id, }, ), ) body_records = [ record for record in self._records if record.kind == "body" and not record.transient and record.body_id == input_record.body_id ] public_candidates = tuple({"score": 1.0, **record.public_dict()} for record in body_records) if len(body_records) != 1: return SelectorResolution( selector=selector, status="not_found", candidates=public_candidates, diagnostic=RuntimeDiagnostic( "selector_owner_body_not_found" if not body_records else "selector_owner_body_ambiguous", "The qOwnerBody input has no unique exact active body record", detail={ "input_record_id": input_record.record_id, "body_id": input_record.body_id, "candidate_count": len(body_records), }, ), ) body_record = body_records[0] return SelectorResolution( selector=selector, status="resolved", record=body_record, candidates=public_candidates, resolution_mode="owner_body", evidence={ "source_query": selector["selector_intent"].get("source_query"), "query_expr": selector["selector_intent"].get("query_expr"), "input_resolution_mode": input_resolution.resolution_mode, "input_record_ids": [input_record.record_id], "result_records": [body_record.record_id], "body_member": input_record.body_id, "relations": list(input_resolution.evidence.get("relations") or ()), "policy": selector["selector_intent"].get("derivation_policy"), }, ) def _resolve_primary_cut_copy_cap_edge_selector( self, selector: dict[str, Any], *, active_body_id: str | None, ) -> SelectorResolution: """Project one exact transient CAP_EDGE through its owning primary cut. COPY itself does not permit a topology lookup. The sole executable bridge is the two-link history emitted for a primary blind cut: a direct-prism cap boundary into the transient tool, immediately followed by that same feature's exact subtract continuation. """ owner = selector.get("owner_feature_id") query_input = selector.get("query_input") if not isinstance(owner, str) or not isinstance(query_input, dict): return SelectorResolution( selector=selector, status="not_found", diagnostic=RuntimeDiagnostic( "selector_copy_invalid", "The COPY selector has no executable primary-cut CAP_EDGE input", ), ) input_resolution = self.resolve(query_input, active_body_id=active_body_id) input_records = input_resolution.records or ( (input_resolution.record,) if input_resolution.record is not None else () ) if input_resolution.status != "resolved" or len(input_records) != 1: cause = input_resolution.diagnostic return SelectorResolution( selector=selector, status="not_found", candidates=input_resolution.candidates, diagnostic=RuntimeDiagnostic( "selector_copy_input_unresolved", "The COPY CAP_EDGE input did not resolve to one proven active edge", detail={ **({"cause": cause.as_dict()} if cause is not None else {}), "input_record_ids": [record.record_id for record in input_records], }, ), ) record = input_records[0] relations = list(input_resolution.evidence.get("relations") or ()) if ( record.kind != "edge" or record.transient or not self._record_is_active(record, active_body_id) or len(relations) != 2 ): return SelectorResolution( selector=selector, status="not_found", candidates=({"score": 1.0, **record.public_dict()},), diagnostic=RuntimeDiagnostic( "selector_copy_kernel_history_missing", "COPY requires one active edge reached by exactly two primary-cut lineage relations", detail={ "record_id": record.record_id, "relation_count": len(relations), "active_body_id": active_body_id, }, ), ) boundary, continuation = relations valid_boundary = ( boundary.get("feature_id") == owner and boundary.get("operation") == "extrude" and boundary.get("derivation") == "boundary" and boundary.get("evidence") == "kernel_history" and boundary.get("coverage") == "complete" and boundary.get("status") == "proven" and boundary.get("source_kind") == "edge" and boundary.get("result_kind") == "edge" and str(boundary.get("output_role") or "").startswith("extrude.") and len(boundary.get("result_record_ids") or ()) == 1 ) valid_continuation = ( continuation.get("feature_id") == owner and continuation.get("operation") == "subtract" and continuation.get("derivation") == "continuation" and continuation.get("evidence") == "kernel_history" and continuation.get("coverage") == "complete" and continuation.get("status") == "proven" and continuation.get("source_kind") == "edge" and continuation.get("result_kind") == "edge" and continuation.get("source_record_ids") == boundary.get("result_record_ids") and continuation.get("result_record_ids") == [record.record_id] ) if not valid_boundary or not valid_continuation: return SelectorResolution( selector=selector, status="not_found", candidates=({"score": 1.0, **record.public_dict()},), diagnostic=RuntimeDiagnostic( "selector_copy_kernel_history_missing", "COPY has no exact transient-prism boundary and same-owner cut continuation", detail={ "owner_feature_id": owner, "relations": relations, }, ), ) return SelectorResolution( selector=selector, status="resolved", record=record, candidates=({"score": 1.0, **record.public_dict()},), resolution_mode="copy_lineage", evidence={ "source_query": selector["selector_intent"].get("source_query"), "query_expr": selector["selector_intent"].get("query_expr"), "input_query_expr": query_input["selector_intent"].get("query_expr"), "input_resolution_mode": input_resolution.resolution_mode, "source_records": list(boundary.get("source_record_ids") or ()), "transient_records": list(boundary.get("result_record_ids") or ()), "result_records": [record.record_id], "relations": relations, "body_member": record.body_id, "policy": selector["selector_intent"].get("derivation_policy"), }, ) def _resolve_primary_cut_copy_cap_face_selector( self, selector: dict[str, Any], *, active_body_id: str | None, ) -> SelectorResolution: """Resolve one immediate COPY(CAP_FACE) attachment by exact history.""" owner = selector.get("owner_feature_id") query_input = selector.get("query_input") input_intent = query_input.get("selector_intent") if isinstance(query_input, dict) else None role = input_intent.get("lineage_role") if isinstance(input_intent, dict) else None if not isinstance(owner, str) or role not in {"extrude.start", "extrude.end"}: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic("selector_copy_invalid", "COPY(CAP_FACE) attachment has no primary-cut cap role"), ) boundary = [ relation for relation in self._lineage if relation.feature_id == owner and relation.operation == "extrude" and relation.output_role == role and relation.derivation == "boundary" and relation.evidence == "kernel_history" and relation.coverage == "complete" and relation.status == "proven" and relation.source_kind == "face" and relation.result_kind == "face" and len(relation.result_record_ids) == 1 ] if len(boundary) != 1: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_copy_kernel_history_missing", "COPY(CAP_FACE) has no unique complete transient-prism cap relation", detail={"owner_feature_id": owner, "output_role": role, "relation_count": len(boundary)}, ), ) transient_id = boundary[0].result_record_ids[0] continuation = [ relation for relation in self._lineage if relation.feature_id == owner and relation.operation == "subtract" and relation.derivation == "continuation" and relation.evidence == "kernel_history" and relation.coverage == "complete" and relation.status == "proven" and relation.source_kind == "face" and relation.result_kind == "face" and relation.source_record_ids == (transient_id,) and len(relation.result_record_ids) == 1 ] if len(continuation) != 1: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_copy_kernel_history_missing", "COPY(CAP_FACE) has no unique same-owner subtract continuation", detail={"owner_feature_id": owner, "transient_record_id": transient_id, "relation_count": len(continuation)}, ), ) result_id = continuation[0].result_record_ids[0] records = [ record for record in self._records if record.record_id == result_id and record.kind == "face" and not record.transient and self._record_is_active(record, active_body_id) ] if len(records) != 1: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_copy_inactive", "COPY(CAP_FACE) successor is not one active face", detail={"result_record_id": result_id, "candidate_count": len(records), "active_body_id": active_body_id}, ), ) record = records[0] return SelectorResolution( selector=selector, status="resolved", record=record, candidates=({"score": 1.0, **record.public_dict()},), resolution_mode="copy_lineage", evidence={ "source_query": selector["selector_intent"].get("source_query"), "query_expr": selector["selector_intent"].get("query_expr"), "transient_records": [transient_id], "result_records": [record.record_id], "relations": [boundary[0].as_dict(), continuation[0].as_dict()], "body_member": record.body_id, "policy": selector["selector_intent"].get("derivation_policy"), }, ) def _resolve_primary_cut_copy_swept_face_selector( self, selector: dict[str, Any], *, active_body_id: str | None, ) -> SelectorResolution: """Resolve COPY(SWEPT_FACE) via one exact prism-face cut continuation.""" owner = selector.get("owner_feature_id") query_input = selector.get("query_input") input_intent = query_input.get("selector_intent") if isinstance(query_input, dict) else None source_entity = input_intent.get("source_entity") if isinstance(input_intent, dict) else None if not isinstance(owner, str) or not isinstance(source_entity, dict): return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic("selector_copy_invalid", "COPY(SWEPT_FACE) attachment has no source-profile edge"), ) source_pair = (source_entity.get("sketch_id"), source_entity.get("entity_id")) anchors = [ record for record in self._records if record.feature_id == owner and record.kind == "edge" and record.transient and record.source_entity == source_pair ] if len(anchors) != 1: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_copy_kernel_history_missing", "COPY(SWEPT_FACE) has no unique direct tool source-edge anchor", detail={"owner_feature_id": owner, "source_entity": source_entity, "anchor_count": len(anchors)}, ), ) boundary = [ relation for relation in self._lineage if relation.feature_id == owner and relation.operation == "extrude" and relation.derivation == "boundary" and relation.evidence == "kernel_history" and relation.coverage == "complete" and relation.status == "proven" and relation.source_kind == "edge" and relation.result_kind == "face" and relation.source_record_ids == (anchors[0].record_id,) and len(relation.result_record_ids) == 1 ] if len(boundary) != 1: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_copy_kernel_history_missing", "COPY(SWEPT_FACE) has no unique complete transient-prism swept-face relation", detail={"owner_feature_id": owner, "anchor_record_id": anchors[0].record_id, "relation_count": len(boundary)}, ), ) transient_id = boundary[0].result_record_ids[0] continuation = [ relation for relation in self._lineage if relation.feature_id == owner and relation.operation == "subtract" and relation.derivation == "continuation" and relation.evidence == "kernel_history" and relation.coverage == "complete" and relation.status == "proven" and relation.source_kind == "face" and relation.result_kind == "face" and relation.source_record_ids == (transient_id,) and len(relation.result_record_ids) == 1 ] if len(continuation) != 1: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_copy_kernel_history_missing", "COPY(SWEPT_FACE) has no unique same-owner subtract continuation", detail={"owner_feature_id": owner, "transient_record_id": transient_id, "relation_count": len(continuation)}, ), ) result_id = continuation[0].result_record_ids[0] records = [ record for record in self._records if record.record_id == result_id and record.kind == "face" and not record.transient and self._record_is_active(record, active_body_id) ] if len(records) != 1: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( "selector_copy_inactive", "COPY(SWEPT_FACE) successor is not one active face", detail={"result_record_id": result_id, "candidate_count": len(records), "active_body_id": active_body_id}, ), ) record = records[0] return SelectorResolution( selector=selector, status="resolved", record=record, candidates=({"score": 1.0, **record.public_dict()},), resolution_mode="copy_lineage", evidence={ "source_query": selector["selector_intent"].get("source_query"), "query_expr": selector["selector_intent"].get("query_expr"), "transient_records": [transient_id], "result_records": [record.record_id], "relations": [boundary[0].as_dict(), continuation[0].as_dict()], "body_member": record.body_id, "policy": selector["selector_intent"].get("derivation_policy"), }, ) def resolve( self, selector: dict[str, Any], *, minimum_score: float = 0.8, active_body_id: str | None = None, ) -> SelectorResolution: kind = selector.get("kind") owner = selector.get("owner_feature_id") intent = selector.get("selector_intent") provenance_intent = selector_has_provenance_intent(selector) def unresolved(code: str, message: str, *, detail: dict[str, Any] | None = None, candidates: tuple[dict[str, Any], ...] = ()) -> SelectorResolution: return SelectorResolution( selector=selector, status="not_found", candidates=candidates, diagnostic=RuntimeDiagnostic(code, message, detail=detail or {}), ) if provenance_intent: validation_error = validate_selector_provenance_intent(selector) if validation_error is not None: return unresolved( validation_error.code, validation_error.message, detail=validation_error.detail, ) allowed, multiplicity = selector_intent_policy(intent) else: allowed, multiplicity = set(), "one" if provenance_intent and intent.get("query_set_contract") in { "proven_operand_union", "proven_operand_intersection", "proven_operand_subtraction", }: return self._resolve_proven_operand_set(selector, active_body_id=active_body_id) if provenance_intent and intent.get("query_family") == "OWNER_BODY": return self._resolve_owner_body_selector(selector, active_body_id=active_body_id) if provenance_intent and intent.get("query_family") == "COPY": if intent.get("copy_contract") == "primary_cut_cap_face_workplane": return self._resolve_primary_cut_copy_cap_face_selector(selector, active_body_id=active_body_id) if intent.get("copy_contract") == "primary_cut_swept_face_workplane": return self._resolve_primary_cut_copy_swept_face_selector(selector, active_body_id=active_body_id) return self._resolve_primary_cut_copy_cap_edge_selector(selector, active_body_id=active_body_id) if provenance_intent and intent.get("query_family") == "INTERSECT": return self._resolve_intersection_intent( selector, intent, allowed=allowed, multiplicity=multiplicity, active_body_id=active_body_id, ) if provenance_intent and intent.get("query_family") == "BLEND_EDGE": return self._resolve_blend_edge_intent(selector, intent, active_body_id=active_body_id) if provenance_intent and intent.get("query_family") == "BLEND_FACE": return self._resolve_blend_face_intent(selector, intent, active_body_id=active_body_id) if provenance_intent and intent.get("query_family") == "SWEPT_BODY": mixed_evidence = any( selector.get(key) is not None for key in ("stable_id", "snapshot_id", "geometry", "binding_feature_id", "output_role") ) if ( kind != "body" or not owner or selector.get("source") != "runtime_snapshot" or intent.get("evidence") != "active_body_member" or intent.get("body_member_contract") != "direct_new_body" or allowed != {"boundary"} or multiplicity != "one" or mixed_evidence ): return unresolved( "selector_source_unavailable", "The SWEPT_BODY selector does not satisfy the direct active-member contract", detail={"query_family": "SWEPT_BODY", "owner_feature_id": owner}, ) owner_records = [ record for record in self._records if record.kind == "body" and not record.transient and record.feature_id == owner and owner in record.owners ] active_records = [ record for record in owner_records if active_body_id is not None and record.body_id == active_body_id ] public_candidates = tuple( {"score": 1.0, **record.public_dict()} for record in active_records ) if len(active_records) == 1: record = active_records[0] return SelectorResolution( selector=selector, status="resolved", record=record, candidates=public_candidates, resolution_mode="body_member", evidence={ "source_query": intent.get("source_query"), "semantic_anchor": {"type": "direct_body_member", "owner_feature_id": owner}, "source_records": [record.record_id], "result_records": [record.record_id], "relations": [], "body_member": record.body_id, "policy": intent.get("derivation_policy"), }, ) if len(active_records) > 1: return unresolved( "selector_ambiguous", "More than one active body member satisfies the SWEPT_BODY contract", detail={"owner_feature_id": owner, "candidate_count": len(active_records)}, candidates=public_candidates, ) return unresolved( "selector_body_member_inactive" if owner_records else "selector_source_unavailable", "The direct SWEPT_BODY member is not the active body" if owner_records else "The SWEPT_BODY producer has no body member record", detail={"owner_feature_id": owner, "active_body_id": active_body_id}, ) candidates = [record for record in self._records if record.kind == kind and not record.transient] if active_body_id and kind in {"face", "edge", "vertex", "body"}: # #7 multi-body:记录 body_id 可能是 body:{feature}:{index}(多体 # 成员),用前缀匹配把整个主体的记录纳入候选,同时保证旧 body 的 # 记录(不同 feature 前缀)不会泄漏进来。 candidates = [ record for record in candidates if record.body_id == active_body_id or (record.body_id is not None and record.body_id.startswith(f"{active_body_id}:")) ] output_role = str(selector.get("output_role") or "").strip() if owner and not output_role: candidates = [record for record in candidates if owner in record.owners] if kind == "plane" and selector.get("frame") is not None: # #6 pattern 引用重解析:pattern 重放 source(pattern_mirror)时, # mirror_plane 的 plane 引用由运行时随实例变换后内联为显式 frame # (_translated_node / _mirrored_node),这里直接构造 PlaneSpec, # 不再走 stable_id / 几何匹配,避免解析到未随实例变换的原始面。 try: plane = PlaneSpec.from_mapping(selector.get("frame") or {}) except (TypeError, ValueError): plane = None if plane is not None: record = TopologyRecord( record_id=f"inline:{id(plane)}", kind="plane", feature_id=str(owner or "inline"), geometry=plane.as_dict(), value=plane, ) return SelectorResolution( selector=selector, status="resolved", record=record, candidates=({"score": 1.0, **record.public_dict()},), resolution_mode="explicit_datum", evidence={"source_records": [], "result_records": [record.record_id], "facts": ["inline_plane_frame"]}, ) return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( code="selector_frame_incomplete", message="An inline plane frame requires origin_mm, x_dir and normal", detail={"frame": selector.get("frame")}, ), ) geometry = normalize_selector_geometry(selector.get("geometry")) source_circle_candidates = self._source_circle_candidates(geometry, candidates) if source_circle_candidates: candidates = source_circle_candidates # The source-circle fields themselves were just checked exactly # enough for this explicit geometric contract. They are not part # of the generic score schema. geometry = {} if selector.get("snapshot_id") and not owner: return unresolved( "selector_owner_required", "A snapshot selector requires owner_feature_id", detail={"minimum_score": minimum_score}, ) if output_role: if not owner: return unresolved( "selector_output_role_owner_required", "A feature output role selector requires owner_feature_id", detail={"output_role": output_role}, ) if active_body_id is None: return unresolved( "selector_output_role_active_body_required", "A feature output role selector requires an active body snapshot", detail={"output_role": output_role}, ) if any(selector.get(key) is not None for key in ("stable_id", "snapshot_id", "geometry", "binding_feature_id")): return unresolved( "selector_output_role_mixed_evidence", "A feature output role selector cannot mix stable or geometry evidence", detail={"output_role": output_role}, ) # Direct output roles are written to the relation evidence before # the result snapshot is registered. The record's retained role # is a convenient cache, not the sole proof source. role_record_ids = { record_id for edge in self._lineage if edge.output_role == output_role for record_id in edge.result_record_ids } role_candidates = [ record for record in candidates if output_role in record.output_roles or record.record_id in role_record_ids ] role_source = selector.get("output_role_source") if role_source is not None: source_owner = role_source.get("owner_feature_id") if isinstance(role_source, dict) else None source_role = role_source.get("output_role") if isinstance(role_source, dict) else None if not isinstance(source_owner, str) or not isinstance(source_role, str): return unresolved( "selector_output_role_source_invalid", "An output role selector source requires owner_feature_id and output_role", ) if output_role != "shell.offset_face" or source_role not in {"extrude.start", "extrude.end"}: return unresolved( "selector_output_role_source_unsupported", "Output role sources are currently supported only for shell.offset_face from an extrusion cap", ) role_candidates = [ record for record in role_candidates if (output_role, source_owner, source_role) in record.output_role_sources ] public_candidates = tuple( {"score": 1.0, **record.public_dict()} for record in role_candidates ) if provenance_intent: if multiplicity == "source_qualified" and role_source is None and not intent.get("disambiguation"): return unresolved( "selector_source_unavailable", "A source-qualified output role selector has no source qualification", detail={"output_role": output_role}, candidates=public_candidates, ) role_edges = [edge for edge in self._lineage if edge.output_role == output_role] proven_candidates: list[tuple[TopologyRecord, list[TopologyLineage]]] = [] cardinality_errors: list[RuntimeDiagnostic] = [] for candidate in role_candidates: for edge in role_edges: if edge.coverage != "complete" or edge.status != "proven": continue if edge.derivation not in allowed: continue cardinality_error = self._operation_cardinality_error( edge, allowed=allowed, source_record_id=candidate.record_id, result_connected_only=True, ) if cardinality_error is not None: cardinality_errors.append(cardinality_error) continue if candidate.record_id in edge.result_record_ids: proven_candidates.append((candidate, [edge])) break for source_record_id in edge.result_record_ids: descendants, relations, error = self._intent_lineage_successors( source_record_id, allowed=allowed, active_body_id=active_body_id, target_kind=candidate.kind, ) if error is None and any(item.record_id == candidate.record_id for item in descendants): proven_candidates.append((candidate, [edge, *relations])) break else: continue break if multiplicity == "all_fragments" and len(proven_candidates) != len(role_candidates): return unresolved( "selector_kernel_history_missing", "One or more output-role fragments lack complete kernel evidence", detail={ "output_role": output_role, "candidate_count": len(role_candidates), "proven_candidate_count": len(proven_candidates), }, candidates=public_candidates, ) if not proven_candidates: if cardinality_errors: error = cardinality_errors[0] return unresolved( error.code, error.message, detail=error.detail, candidates=public_candidates, ) return unresolved( "selector_kernel_history_missing", "No complete builder relation proves the requested active output role", detail={"output_role": output_role, "allowed": sorted(allowed)}, candidates=public_candidates, ) role_candidates = [candidate for candidate, _relations in proven_candidates] public_candidates = tuple({"score": 1.0, **record.public_dict()} for record in role_candidates) if multiplicity == "all_fragments": relations = [ relation for _candidate, candidate_relations in proven_candidates for relation in candidate_relations ] return SelectorResolution( selector=selector, status="resolved", records=tuple(role_candidates), candidates=public_candidates, resolution_mode="operation_role", evidence={ "source_query": intent.get("source_query"), "semantic_anchor": { "type": "output_role", "owner_feature_id": owner, "output_role": output_role, }, "source_records": [], "result_records": [record.record_id for record in role_candidates], "relations": [edge.as_dict() for edge in relations], "body_members": [record.body_id for record in role_candidates], "policy": intent.get("derivation_policy"), }, ) if len(role_candidates) == 1: relations = proven_candidates[0][1] if provenance_intent else [] return SelectorResolution( selector=selector, status="resolved", record=role_candidates[0], candidates=public_candidates, resolution_mode="operation_role", evidence={ "source_query": intent.get("source_query") if provenance_intent else None, "semantic_anchor": {"type": "output_role", "owner_feature_id": owner, "output_role": output_role}, "source_records": [], "result_records": [role_candidates[0].record_id], "relations": [edge.as_dict() for edge in relations], "body_member": role_candidates[0].body_id, "policy": intent.get("derivation_policy") if provenance_intent else None, }, ) if len(role_candidates) > 1: return SelectorResolution( selector=selector, status="ambiguous", candidates=public_candidates, diagnostic=RuntimeDiagnostic( code="selector_output_role_ambiguous", message="More than one active topology record has the requested output role", detail={"output_role": output_role, "candidate_count": len(role_candidates)}, ), ) return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( code="selector_output_role_not_found", message="No active topology record has the requested output role", detail={"output_role": output_role, "candidate_count": 0}, ), ) if provenance_intent and intent.get("query_family") in {"CAP_EDGE", "CAP_VERTEX", "SWEPT_FACE", "SWEPT_EDGE", "OFFSET_EDGE"}: if not isinstance(owner, str) or not owner: return unresolved( "selector_source_unavailable", "The provenance selector has no explicit producer owner for its source anchor", detail={"query_family": intent.get("query_family")}, ) source_entity = intent.get("source_entity") source_entities = intent.get("source_entities") source_kind: str | None = None semantic_anchor: dict[str, Any] | None = None anchor_records: list[TopologyRecord] = [] if isinstance(source_entity, dict): sketch_id, entity_id = source_entity.get("sketch_id"), source_entity.get("entity_id") if isinstance(sketch_id, str) and sketch_id and isinstance(entity_id, str) and entity_id: source_kind = "edge" semantic_anchor = {"type": "source_entity", "sketch_id": sketch_id, "entity_id": entity_id} anchor_records = [ record for record in self._records if record.kind == source_kind and record.feature_id == owner and record.source_entity == (sketch_id, entity_id) ] elif isinstance(source_entities, list): pairs = tuple(sorted( (str(source.get("sketch_id")), str(source.get("entity_id"))) for source in source_entities if isinstance(source, dict) and isinstance(source.get("sketch_id"), str) and isinstance(source.get("entity_id"), str) )) if len(pairs) == len(source_entities) and len(pairs) >= 2 and len(set(pairs)) == len(pairs): source_kind = "vertex" semantic_anchor = { "type": "source_vertex", "source_entities": [ {"sketch_id": sketch_id, "entity_id": entity_id} for sketch_id, entity_id in pairs ], } anchor_records = [ record for record in self._records if record.kind == source_kind and record.feature_id == owner and record.source_entities == pairs ] if source_kind is None: return unresolved( "selector_source_unavailable", "The provenance selector has no valid source-profile semantic anchor", detail={"query_family": intent.get("query_family")}, ) lineage_role = intent.get("lineage_role") if intent.get("query_family") == "CAP_EDGE" or ( intent.get("query_family") == "OFFSET_EDGE" and source_kind == "edge" ): cap_roles = ( {"extrude.start", "extrude.end", "sweep.start", "sweep.end"} if intent.get("query_family") == "CAP_EDGE" else {"extrude.start", "extrude.end"} ) if kind != "edge" or lineage_role not in cap_roles: return unresolved( "selector_source_unavailable", "The cap-edge provenance selector lacks a valid direct cap role", detail={"query_family": intent.get("query_family"), "lineage_role": lineage_role}, ) elif intent.get("query_family") in {"CAP_VERTEX", "OFFSET_EDGE"}: if kind != "vertex" or source_kind != "vertex" or lineage_role not in { "extrude.start", "extrude.end", } if intent.get("query_family") == "CAP_VERTEX" else kind != "edge" or source_kind != "vertex" or lineage_role is not None: return unresolved( "selector_source_unavailable", "The source-vertex provenance selector has an invalid direct-prism role contract", detail={"query_family": intent.get("query_family"), "lineage_role": lineage_role}, ) else: lineage_role = None public_anchors = tuple({"score": 1.0, **record.public_dict()} for record in anchor_records) if not anchor_records or (multiplicity != "all_fragments" and len(anchor_records) != 1): return unresolved( "selector_source_unavailable" if not anchor_records else "selector_ambiguous", "The source-profile semantic anchor is unavailable or has unsupported cardinality in this replay", detail={ "query_family": intent.get("query_family"), "semantic_anchor": semantic_anchor, "candidate_count": len(anchor_records), }, candidates=public_anchors, ) successors: list[TopologyRecord] = [] relations: list[TopologyLineage] = [] seen_successors: set[str] = set() for anchor in anchor_records: branch_successors, branch_relations, error = self._intent_lineage_successors( anchor.record_id, allowed=allowed, active_body_id=active_body_id, initial_output_role=lineage_role, target_kind=kind, ) branch_successors = [record for record in branch_successors if record.kind == kind] if error is not None: return unresolved( error.code, error.message, detail={**error.detail, "semantic_anchor": semantic_anchor}, candidates=tuple({"score": 1.0, **record.public_dict()} for record in branch_successors), ) # A set-valued source query must be proven for every selected # physical source fragment. Returning only surviving branches # would make a missing fragment look like a valid selector. if multiplicity == "all_fragments" and not branch_successors: return unresolved( "selector_kernel_history_missing", "A source-profile fragment has no complete active lineage result", detail={"semantic_anchor": semantic_anchor, "source_record_id": anchor.record_id}, ) relations.extend(branch_relations) for record in branch_successors: if record.record_id not in seen_successors: seen_successors.add(record.record_id) successors.append(record) public_successors = tuple({"score": 1.0, **record.public_dict()} for record in successors) if multiplicity == "all_fragments": return SelectorResolution( selector=selector, status="resolved", records=tuple(successors), candidates=public_successors, resolution_mode="kernel_lineage", evidence={ "source_query": intent.get("source_query"), "semantic_anchor": semantic_anchor, "source_records": [record.record_id for record in anchor_records], "result_records": [record.record_id for record in successors], "relations": [edge.as_dict() for edge in relations], "body_members": [record.body_id for record in successors], "policy": intent.get("derivation_policy"), }, ) if len(successors) != 1: return SelectorResolution( selector=selector, status="ambiguous" if successors else "not_found", candidates=public_successors, diagnostic=RuntimeDiagnostic( "selector_relation_non_unique" if successors else "selector_body_member_inactive", "The complete proven source-profile lineage does not yield one active result", detail={ "semantic_anchor": semantic_anchor, "candidate_count": len(successors), "multiplicity": multiplicity, }, ), ) return SelectorResolution( selector=selector, status="resolved", record=successors[0], candidates=public_successors, resolution_mode="kernel_lineage", evidence={ "source_query": intent.get("source_query"), "semantic_anchor": semantic_anchor, "source_records": [anchor_records[0].record_id], "result_records": [successors[0].record_id], "relations": [edge.as_dict() for edge in relations], "body_member": successors[0].body_id, "policy": intent.get("derivation_policy"), }, ) stable_id = str(selector.get("stable_id") or "").strip() if stable_id: # #8 selector 持久性:stable_id 是跨 body 演化的持久标识符,精确 # 匹配在 active body 过滤之前对整个记录集(kind + owner 过滤)执行。 # 命中已过期(旧 body)的记录时,经演化后继映射解析到 active body # 内的新形态(fillet/chamfer 拆段后的直段后继);无后继则回落到 # 几何打分流程。 stable_records = [ record for record in self._records if record.kind == kind and not record.transient and (not owner or owner in record.owners) ] exact = [record for record in stable_records if record.record_id == stable_id] if len(exact) == 1: record = exact[0] if provenance_intent: successors, relations, error = self._intent_lineage_successors( record.record_id, allowed=allowed, active_body_id=active_body_id, target_kind=kind, ) public_successors = tuple({"score": 1.0, **candidate.public_dict()} for candidate in successors) if error is not None: return unresolved( error.code, error.message, detail={**error.detail, "stable_id": stable_id}, candidates=public_successors, ) if multiplicity == "all_fragments": return SelectorResolution( selector=selector, status="resolved", records=tuple(successors), candidates=public_successors, resolution_mode="kernel_lineage", evidence={ "source_query": intent.get("source_query"), "source_records": [record.record_id], "result_records": [candidate.record_id for candidate in successors], "relations": [edge.as_dict() for edge in relations], "body_members": [candidate.body_id for candidate in successors], "policy": intent.get("derivation_policy"), }, ) if len(successors) != 1: return SelectorResolution( selector=selector, status="ambiguous", candidates=public_successors, diagnostic=RuntimeDiagnostic( code="selector_relation_non_unique", message="The complete proven topology lineage does not yield one active result", detail={"stable_id": stable_id, "candidate_count": len(successors), "multiplicity": multiplicity}, ), ) return SelectorResolution( selector=selector, status="resolved", record=successors[0], candidates=public_successors, resolution_mode="kernel_lineage", evidence={ "source_query": intent.get("source_query"), "source_records": [record.record_id], "result_records": [successors[0].record_id], "relations": [edge.as_dict() for edge in relations], "body_member": successors[0].body_id, "policy": intent.get("derivation_policy"), }, ) is_active = active_body_id is None or ( record.body_id == active_body_id or (record.body_id is not None and record.body_id.startswith(f"{active_body_id}:")) ) if not is_active and provenance_intent: policy = intent.get("derivation_policy") or {} allowed = { value for value in policy.get("allowed") or () if value in {"continuation", "fragment", "merge", "intersection", "boundary", "replacement"} } successors = self._intent_lineage_successors( record.record_id, allowed=allowed, active_body_id=active_body_id, target_kind=kind, ) if len(successors) == 1: record = successors[0] is_active = True elif len(successors) > 1: if policy.get("multiplicity") == "all_fragments" and "fragment" in allowed: return SelectorResolution( selector=selector, status="resolved", records=tuple(successors), candidates=tuple({"score": 1.0, **candidate.public_dict()} for candidate in successors), ) return SelectorResolution( selector=selector, status="ambiguous", candidates=tuple({"score": 1.0, **candidate.public_dict()} for candidate in successors), diagnostic=RuntimeDiagnostic( code="selector_relation_non_unique", message="The proven topology lineage has more than one active result", detail={"stable_id": stable_id, "candidate_count": len(successors), "multiplicity": policy.get("multiplicity")}, ), ) else: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( code="selector_kernel_history_missing", message="No complete proven lineage reaches an active topology record", detail={"stable_id": stable_id, "allowed": sorted(allowed)}, ), ) if not is_active: successors = [ candidate for candidate in stable_records if candidate.record_id in self._successors.get(record.record_id, ()) and ( candidate.body_id == active_body_id or (candidate.body_id is not None and active_body_id and candidate.body_id.startswith(f"{active_body_id}:")) ) ] if len(successors) == 1: record = successors[0] is_active = True elif len(successors) > 1: return SelectorResolution( selector=selector, status="ambiguous", candidates=tuple({"score": 1.0, **candidate.public_dict()} for candidate in successors), diagnostic=RuntimeDiagnostic( code="selector_ambiguous", message="More than one evolved successor record satisfies the stable_id", detail={"stable_id": stable_id, "candidate_count": len(successors)}, ), ) if is_active: # A stable ID is only a lookup accelerator for snapshot-aware # selectors. It cannot revive a B-rep entity whose geometric # signature changed after an upstream rebuild. if selector.get("snapshot_id"): score = self._geometry_score(geometry, record.geometry) if geometry else None if score is None or score < minimum_score: return SelectorResolution( selector=selector, status="not_found", candidates=({"score": round(float(score or 0), 6), **record.public_dict()},), diagnostic=RuntimeDiagnostic( code="selector_geometry_mismatch", message="The stable selector record no longer matches its geometry signature", detail={"stable_id": stable_id, "score": score, "minimum_score": minimum_score}, ), ) return SelectorResolution( selector=selector, status="resolved", record=record, candidates=({"score": round(float(score), 6) if selector.get("snapshot_id") else 1.0, **record.public_dict()},), resolution_mode="runtime_snapshot", evidence={"source_records": [], "result_records": [record.record_id], "facts": ["runtime_stable_id"]}, ) if not geometry: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( code="selector_stable_id_inactive", message="The stable selector record is not active and has no geometry signature for rebinding", detail={"stable_id": stable_id}, ), ) if len(exact) > 1: return SelectorResolution( selector=selector, status="ambiguous", candidates=tuple({"score": 1.0, **record.public_dict()} for record in exact), diagnostic=RuntimeDiagnostic( code="selector_ambiguous", message="More than one runtime topology record has the requested stable_id", detail={"stable_id": stable_id, "candidate_count": len(exact)}, ), ) if selector.get("snapshot_id") and not geometry: return SelectorResolution( selector=selector, status="not_found", candidates=(), diagnostic=RuntimeDiagnostic( code="selector_geometry_mismatch", message="A snapshot selector requires a geometry signature", detail={"minimum_score": minimum_score}, ), ) if provenance_intent: return unresolved( "selector_source_unavailable", "The provenance selector has no runtime-resolvable semantic source anchor", detail={"query_family": intent.get("query_family"), "anchors": [key for key in ("output_role", "source_entity") if intent.get(key)]}, ) scored: list[tuple[float, TopologyRecord]] = [] for candidate in candidates: # An owner-qualified context selector is deterministic when it has # a single runtime candidate even if its source stable_id cannot # survive the SolidWorks -> OCC boundary. score = 1.0 if not geometry else self._geometry_score(geometry, candidate.geometry) if score is not None: scored.append((score, candidate)) scored.sort(key=lambda item: (-item[0], item[1].record_id)) public_candidates = tuple({"score": round(score, 6), **record.public_dict()} for score, record in scored) if not scored or scored[0][0] < minimum_score: return SelectorResolution( selector=selector, status="not_found", candidates=public_candidates, diagnostic=RuntimeDiagnostic( code="selector_not_found", message="No runtime topology record satisfies the selector", detail={"candidate_count": len(scored), "minimum_score": minimum_score}, ), ) if selector.get("match_mode") == "all": matches = tuple(record for score, record in scored if score >= minimum_score) return SelectorResolution( selector=selector, status="resolved", records=matches, candidates=public_candidates, resolution_mode="geometry", evidence={"source_records": [], "result_records": [record.record_id for record in matches], "facts": ["explicit_geometry"]}, ) best_score, best_record = scored[0] if len(scored) > 1 and abs(scored[1][0] - best_score) <= 1e-9: return SelectorResolution( selector=selector, status="ambiguous", candidates=public_candidates, diagnostic=RuntimeDiagnostic( code="selector_ambiguous", message="More than one runtime topology record has the best selector score", detail={"best_score": best_score, "candidate_count": len(scored)}, ), ) return SelectorResolution( selector=selector, status="resolved", record=best_record, candidates=public_candidates, resolution_mode="geometry", evidence={"source_records": [], "result_records": [best_record.record_id], "facts": ["explicit_geometry"]}, )