更新功能

This commit is contained in:
2026-08-26 14:13:11 +08:00
parent beaa5dd9fe
commit a508e6f07e
47 changed files with 5616 additions and 520 deletions
+2 -1
View File
@@ -249,7 +249,8 @@
"stable_id": {"type": "string", "minLength": 1},
"owner_feature_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
"geometry": {"type": "object"},
"source": {"enum": ["solidworks", "inferred_from_step"]},
"source": {"enum": ["solidworks", "inferred_from_step", "runtime_snapshot", "viewer_selection"]},
"snapshot_id": {"type": "string", "minLength": 1},
"confidence": {"type": "number", "minimum": 0, "maximum": 1}
},
"required": ["kind", "stable_id", "source", "confidence"],
+10 -2
View File
@@ -89,6 +89,7 @@ class ExecutionSession:
results: dict[str, FeatureResult] = field(default_factory=dict)
replay_definitions: dict[str, FeaturePlanNode] = field(default_factory=dict)
selector_resolutions: list[dict[str, Any]] = field(default_factory=list)
active_feature_id: str = ""
def register_body(self, feature_id: str, body: Any, *, replay_node: FeaturePlanNode | None = None) -> None:
self.body = body
@@ -103,7 +104,9 @@ class ExecutionSession:
def resolve(self, selector: dict[str, Any]) -> SelectorResolution:
resolution = self.topology.resolve(selector, active_body_id=self.body_id)
self.selector_resolutions.append(resolution.as_dict())
evidence = resolution.as_dict()
evidence["feature_id"] = self.active_feature_id
self.selector_resolutions.append(evidence)
return resolution
def result(self, node: FeaturePlanNode, *, context: PlaneSpec | AxisSpec | None = None, diagnostics: list[RuntimeDiagnostic] | None = None) -> FeatureResult:
@@ -677,7 +680,12 @@ def _execute_node(node: FeaturePlanNode, session: ExecutionSession, sketch_overr
executor = EXECUTORS.get(node.atomic_id)
if executor is None:
raise ValueError(f"No executor registered for {node.atomic_id!r}")
return executor(node, session, sketch_override)
previous_feature_id = session.active_feature_id
session.active_feature_id = node.feature_id
try:
return executor(node, session, sketch_override)
finally:
session.active_feature_id = previous_feature_id
ExecutorFunction = Callable[[FeaturePlanNode, ExecutionSession, dict[str, Any] | None], FeatureResult]
@@ -381,6 +381,13 @@ class SelectorResolution:
}
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.diagnostic is not None:
output["diagnostic"] = self.diagnostic.as_dict()
return output
@@ -589,6 +596,66 @@ class TopologyRegistry:
if owner:
candidates = [record for record in candidates if owner in record.owners]
geometry = normalize_selector_geometry(selector.get("geometry"))
if selector.get("snapshot_id") and not owner:
return SelectorResolution(
selector=selector,
status="not_found",
candidates=(),
diagnostic=RuntimeDiagnostic(
code="selector_owner_required",
message="A snapshot selector requires owner_feature_id",
detail={"minimum_score": minimum_score},
),
)
stable_id = str(selector.get("stable_id") or "").strip()
if stable_id:
exact = [record for record in candidates if record.record_id == stable_id]
if len(exact) == 1:
record = exact[0]
# 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()},),
)
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},
),
)
scored: list[tuple[float, TopologyRecord]] = []
for candidate in candidates:
# An owner-qualified context selector is deterministic when it has