5ffb106f36
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.
84 lines
3.5 KiB
Python
84 lines
3.5 KiB
Python
"""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
|