refactor(cdsl_engine): executor registry + per-family executor package
Phase 3 of the decoupling refactor (behavior-preserving): - registry.py: atomic_executor decorator, ALL_ATOMIC_IDS with fail-fast registration validation, execute_node dispatcher - executors/: one module per family (extrude, revolve, surfaces, loft_sweep, bodies, context, primitives, parametric, holes, dressup, patterns) + shared helpers in executors/common - executors/__init__: explicit aggregation + completeness check (registry must cover every declared atomic id at import time) - runtime.py: slimmed to entry points (analyze_cdsl/rebuild_cdsl) plus full historical re-exports incl. test-referenced privates Adding an atomic operation now touches only one executor module and its schema contract; the shared registry never changes. Verified against baseline: zero new failures.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""Atomic executor registry and dispatch for the session runtime.
|
||||
|
||||
Executors register themselves with :func:`atomic_executor` from their own
|
||||
modules; ``cdsl_engine.executors`` imports every executor module exactly once
|
||||
to build the registry. Adding a new atomic operation therefore means adding
|
||||
a decorated function in a family module -- the registry itself never changes.
|
||||
|
||||
``registry`` deliberately imports no executor module, so pattern executors
|
||||
can re-enter :func:`execute_node` for replay without an import cycle.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Protocol
|
||||
|
||||
from .session import ExecutionSession
|
||||
from .topology import CapabilityResult, FeaturePlanNode, FeatureResult
|
||||
|
||||
|
||||
class AtomicExecutor(Protocol):
|
||||
atomic_id: str
|
||||
|
||||
def preflight(self, node: FeaturePlanNode, session: ExecutionSession) -> CapabilityResult: ...
|
||||
def execute(self, node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult: ...
|
||||
|
||||
|
||||
#: The complete capability contract of this runtime. Every registered
|
||||
#: executor must name an id from this set, so a mistyped registration fails
|
||||
#: at import time instead of surfacing as an unknown-atomic blocker later.
|
||||
ALL_ATOMIC_IDS = frozenset({
|
||||
"extrude_add_blind", "extrude_add_blind_with_hole", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided", "extrude_surface",
|
||||
"extrude_cut_through", "extrude_from_face", "loft_add", "loft_add_with_cap_face", "sweep_add",
|
||||
"revolve_add", "revolve_cut", "revolve_surface", "hole_blind", "hole_countersink",
|
||||
"hole_counterbore", "sphere_add", "box_add", "cylinder_add",
|
||||
"reference_plane", "reference_axis",
|
||||
"hole_wizard", "fillet", "chamfer", "shell", "pattern_linear", "pattern_mirror",
|
||||
"pattern_circular", "boolean_bodies", "transform_bodies", "delete_bodies",
|
||||
"thread_add", "thread_cut",
|
||||
"bend_add",
|
||||
"gear_add", "rack_add",
|
||||
})
|
||||
|
||||
#: ``atomic_id -> executor``. Populated by ``cdsl_engine.executors``.
|
||||
EXECUTORS: dict[str, "ExecutorFunction"] = {}
|
||||
|
||||
ExecutorFunction = Callable[[FeaturePlanNode, ExecutionSession, dict[str, Any] | None], FeatureResult]
|
||||
|
||||
|
||||
def atomic_executor(*atomic_ids: str) -> Callable[["ExecutorFunction"], "ExecutorFunction"]:
|
||||
"""Register one executor function for one or more atomic ids.
|
||||
|
||||
Registration validates against ``ALL_ATOMIC_IDS`` and rejects duplicates,
|
||||
keeping the registry consistent with the declared capability contract.
|
||||
"""
|
||||
unknown = [atomic_id for atomic_id in atomic_ids if atomic_id not in ALL_ATOMIC_IDS]
|
||||
if unknown:
|
||||
raise ValueError(f"cannot register executors for unknown atomic ids: {unknown}")
|
||||
|
||||
def decorator(func: "ExecutorFunction") -> "ExecutorFunction":
|
||||
for atomic_id in atomic_ids:
|
||||
if atomic_id in EXECUTORS:
|
||||
raise ValueError(f"duplicate executor registration for {atomic_id!r}")
|
||||
EXECUTORS[atomic_id] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def execute_node(
|
||||
node: FeaturePlanNode,
|
||||
session: ExecutionSession,
|
||||
sketch_override: dict[str, Any] | None = None,
|
||||
) -> FeatureResult:
|
||||
"""Dispatch one plan node through its registered executor."""
|
||||
executor = EXECUTORS.get(node.atomic_id)
|
||||
if executor is None:
|
||||
raise ValueError(f"No executor registered for {node.atomic_id!r}")
|
||||
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
|
||||
Reference in New Issue
Block a user