from collections.abc import Callable, Sequence
from threading import RLock
from types import TracebackType
from typing import (
TYPE_CHECKING,
Any,
Optional,
)
# Melder Imports
from melder.aether.aetheric_frame.dev_ops.spell_system_states.spell_state_change_reason import (
SpellStateChangeReason,
)
from melder.aether.conduit.conduit_ward.permissions.permissions import Permissions
from melder.aether.conduit.meld.creation_context.creation_context_factory import (
CreationContextFactory,
)
from melder.aether.spellbook.bind.spell_index import SpellIndex
from melder.aether.spellbook.existence.existence import Existence
from melder.aether.spellbook.spell_compiler.spell_compiler_artifact import (
SpellCompilerArtifact,
)
from melder.aether.spellbook.spell_compiler.symbolic_graph.spell_symbolic_graph import (
SpellSymbolicGraph,
)
from melder.aether.spellbook.spell_types.spell_types import SpellType
from melder.utilities.general_base.cleanable import Cleanable
from melder.utilities.helpers.general_helpers import SpellInputUtils
from melder.utilities.helpers.ulid_factory import new_ulid
from melder.utilities.synchronization.counter_switch import CounterSwitch
if TYPE_CHECKING:
from melder.aether.aetheric_frame.dev_ops.spell_system_states.spell_system_state import (
SpellSystemState,
)
from melder.aether.aetheric_frame.dev_ops.spell_system_states.spell_system_states import (
SpellSystemStates,
)
from melder.aether.conduit.meld.creation_context.creation_context import (
CreationContext,
)
from melder.aether.spellbook.spell_compiler.spell_requirements_finder.spell_requirements import (
SpellRequirements,
)
from melder.aether.spellbook.spell_compiler.system.spell_system_validation_state import (
SpellSystemValidationState,
)
from melder.aether.spellbook.spell_compiler.validation.spell_validation_result import (
SpellValidationResult,
)
from melder.aether.spellbook.spellbook import Spellbook
from melder.utilities.synchronization.creation_gate_controller import (
CreationGateController,
)
#region Spell
[docs]
class Spell(Cleanable):
"""
Internal
Represents one registered spell inside the Melder runtime.
A `Spell` is the canonical bind-time runtime record for one class,
function, lambda, or existing object registration. It keeps the spell's
structural identity, lifecycle policy, access policy, reflective profile,
build-time artifacts, spell-local runtime helpers, and ownership metadata
together so the rest of Melder can reason about one stable object instead
of a loose bundle of values.
Contract:
- Wraps exactly one registered spell target plus its `SpellIndex`
lineage/version record.
- Owns spell-local mutable runtime state such as hooks, dependency/build
artifacts, the spell-owned `CreationContextFactory`, the spell-owned
`CreationContext`, execution-plan dispatch-route metadata, and mutation
overlays.
- Does not validate bind-time inputs by itself; upstream bind/examiner
stages are expected to hand it already-validated configuration.
- Uses an internal `RLock` to guard multi-field configuration and cleanup
transitions.
- Becomes unusable after `cleanup()` completes; later live-object methods
are expected to fail through `check_cleaned()`.
- Disposal metadata is established once at bind. The ordered list, presence
flag, and bind-time SHA describe that same policy. Post-creation mutation
is unsupported; this class adds no copying or mutation guards.
Core Responsibilities:
- Holds an immutable reference to the object (function/class/instance) it represents.
- Tracks configuration data: type, binding profile (if attached), spellframe,
ownership, and hooks.
- Defines dependency DAGs for invocation and construction (via external DAG /
resolution pipelines).
- Manages permission control via the `Permissions` enum.
- Enables hook-based lifecycle support (pre, activation, post).
- Acts as a source of truth for spell identity and access.
- Stores bind-time resolved disposal metadata (matched method names + boolean flag).
- Tracks whether runtime resolution is still required before the first context build.
- Caches the Phase 11 execution-plan dispatch-route hint used by current
runtime path selection.
Permissions (`Permissions` enum):
- `read`: Allows other conduits to use the spell as-is, but not modify
or recreate it.
- `create`: Allows other conduits to instantiate or construct new
versions.
- `block`: Prevents external access. Internal owner-conduit access is
still allowed.
Key Concepts:
- Each spell has a unique SHA256 `spell_id`, generated from its bind-time fingerprint.
- `spellframe` distinguishes the context it was declared in (e.g., Protocol, class,
or string frame).
- Spells may be cleaned (`cleanup()`), after which modification is disallowed.
- Dependency graphs and resolution profiles are produced by the Resolution / Meld
pipeline, not by this class directly.
- Permissions are enforced during conduit contract evaluation.
Parameters:
spell (Any):
The actual object to register (function, class, lambda, or existing instance).
spell_index (SpellIndex):
Versioned identity for this spell (current + historical fingerprints).
spellframe (Optional[Any]):
Frame context (usually a Protocol, class, or string) to scope the spell's identity.
binding_name (Optional[str]):
The logical name this spell is bound to (e.g., "database", "engine").
Normalized as part of the internal key via SpellInputUtils.
Maybe None for unnamed/default bindings.
spell_name (str):
The actual internal name of the object or callable (for display/debugging).
existence (Existence):
The spell's lifecycle policy (unique, shared, etc.).
spell_type (SpellType):
Indicates if the spell is a class, method, lambda, or existing creation, and
whether it participates in spellframes and/or binding names.
spell_id (str):
Unique identifier derived from object fingerprinting (SHA256).
permissions (Permissions):
Defines access control level for borrowing, invoking, or recreating this spell.
aetheric_frame (str):
Logical Aether frame / namespace this spell was registered under.
profile (Optional[Any]):
Optional reflective profile associated with this spell.
Currently:
- The Bind pipeline finishes by attaching a combined general or
detailed spell profile here.
- Those combined profiles still expose the underlying binding and
resolution artifacts for downstream consumers.
- Legacy usage expecting raw profile types should treat this field
as an opaque introspection artifact and normalize it first.
existing_object (Optional[object]):
Optional pre-instantiated object to attach to the spell (EXISTING_CREATION* types).
For factory-like spells (class/method/lambda), this is usually None.
spellbook (Spellbook):
Back-reference to the owning Spellbook. This is a required live-owner
contract used for internal coordination, graph wiring, diagnostics,
and spell-system-state attachment.
args and kwargs:
Arbitrary tags and metadata for internal use or future extensions.
Threading / Concurrency:
- '_Lock' guards internal multi-field mutation.
- Spell-owned runtime context publication uses `_creation_context_switch`
so only one builder wins publication at a time.
- Higher-level conduit/spellbook orchestration still owns system-level
concurrency decisions; this class only protects its own local state.
Lifecycle / Cleanup:
- `Spell` owns its spell compiler artifact foundation, spell-owned
`CreationContextFactory`, spell-owned `CreationContext`, hook lists,
dependency/build artifacts and cached execution-plan dispatch-route
metadata.
- Conduit ownership can be restamped later, which invalidates the
spell-owned `CreationContext` and rebuilds the spell-owned factory.
- `cleanup()` is deterministic, best-effort for owned child cleanup, and
clears references to prevent reuse-after-clean.
Registration:
MELDER KERNEL - guarded (internal manifest). Melder constructs
every `Spell` during `Bind`; a user never asks Melder to inject one, so
`bind(Spell)` is the category error the guard refuses. `access=public` is
deliberate and orthogonal to the guard: agents RECEIVE spells from `bind()`
and from viewer/examination surfaces and read their identity, existence, and
permissions - they simply do not construct or re-bind them.
Subsystem Context:
The unit of currency of the spellbook subsystem. `Bind` produces one `Spell`
(paired with its `SpellIndex` lineage/version record) from a user target;
`Spellbook` registers it into its owned/contracted maps and spell-id caches and
registers the lineage into `SpellSystemStates`. Downstream, the SpellCompiler
keys every phase artifact on this spell (by `spell_index.selected_spell_id`) and
`Meld` reads it to choose reuse-vs-construct. It hands structural identity to
`SpellIndex`, build/plan artifacts to the SpellCompiler, and its `Permissions`
to `ConduitWard` contract evaluation.
System Context:
Lives in the Spellbook layer of the DGR boot order
(Aether|AetherUtilitySystem -> Crystallizer -> MutationResearch -> Nexus ->
AethericFrame -> Spellbook -> Conduit|Ward). Created at bind time - after a
frame exists, before conjure - and is the object every later layer operates on
per-spell: SpellCompiler phases 1-11, `SpellSystemStates` validity, ChangeControl
dirty-root gating, and `Meld` resolution into live instances via `Creations`. It
sits between the `Spellbook` that owns it and the Conduit/Meld layer that
resolves it.
Notes:
- This class is never used directly by users. It is created during `bind()` and
registered into the Spellbook and Aether.
- Internal mutation after cleaning is disallowed.
- Dependency graphs, resolution frames, and resolution profiles are produced
by the Resolution / Meld layer; `Spell` itself does not execute resolution.
AGENT_ACCESS: public
AGENT_PURPOSE:
access: public. One registered binding: identity, existence, permissions, spellframe,
hooks. You receive spells from bind and from viewer surfaces; you do not construct them.
"""
__slots__ = Cleanable.__slots__ + [
"_active",
"_activation_hooks",
"_creation_context",
"_creation_context_factory",
"_creation_context_switch",
"_caching_enabled",
"_compiler_artifact",
"_door_epoch",
"_dynamic_environment",
"_hooks_enabled",
"_id",
"_is_class_spell",
"_is_existing_creation",
"_is_lambda_spell",
"_is_method_spell",
"_key",
"_lock",
"_mutation_override",
"_owner_conduit_id",
"_owner_conduit_name",
"_owner_creations",
"_post_hooks",
"_pre_hooks",
"_spellbook_cleanup",
"_spell_system_states",
"_spellbook",
"aetheric_frame",
"binding_name",
"dependencies",
"dependency_graph",
"disposal_method_names",
"existence",
"has_disposal_methods",
"metadata",
"permissions",
"profile",
"resolution_required",
"resolution_complete",
"requires_spellspace_request",
"retries",
"spell",
"spell_id",
"spell_index",
"spell_name",
"spell_type",
"spellframe",
"tags",
"timeout",
"user_created_object",
]
def __init__(
self,
spell: Any,
spell_index: SpellIndex,
spellframe: Any | None,
binding_name: str | None,
spell_name: str,
existence: Existence,
spell_type: SpellType,
spell_id: str,
permissions: Permissions,
aetheric_frame: str,
spellbook: "Spellbook",
profile: Any | None = None,
existing_object: object | None = None,
disposal_method_names: Optional[list[str]] = None,
*args: Any,
**kwargs: Any,
) -> None:
"""
Internal constructor for a bound Spell record.
Args:
spell (Any): The underlying callable/class/instance being registered.
spell_index (SpellIndex): Lineage/version tracker for this spell.
spellframe (Optional[Any]): Logical frame/contract used to scope the spell.
binding_name (Optional[str]): Optional binding key is used to disambiguate spells under the same frame.
spell_name (str): Resolved name for the spell (qualname or type name).
existence (Existence): Lifecycle policy for instantiation/sharing semantics.
spell_type (SpellType): Classification of the spell (class/method/lambda/existing-creation variants).
spell_id (str): SHA256 fingerprint for this spell's structural identity.
permissions (Permissions): Access policy for other conduits.
aetheric_frame (str): Aether frame identifier this spell belongs to.
spellbook (Spellbook): Back-reference to the owning spellbook for coordination.
profile (Optional[Any]): Binding/introspection profile attached by the examiner.
existing_object (Optional[object]): Pre-created instance for EXISTING_CREATION* spell types.
Stored on the spell under the DIFFERENT name `user_created_object`.
disposal_method_names (Optional[list[str]]): Bind-resolved disposal methods in
execution order. Retained directly; omitted metadata gets a fresh empty list.
Bind owns matching and deduplication, which are not repeated here.
*args: Optional positional metadata tags, collected into `tags`.
**kwargs: Optional keyword metadata map, collected into `metadata`.
Contract:
- INTERNAL CONSTRUCTOR. Spells are produced by `Spellbook.bind(...)`
through the Bind pipeline; user code receives them, it does not
build them. Input validation (existence, permissions, profile
shape) is enforced upstream and is NOT repeated here.
- UNRECOGNIZED KEYWORDS ARE ACCEPTED SILENTLY. `**kwargs` is a
metadata bag, so a misspelled parameter name lands in `metadata`
rather than raising `TypeError`.
- TWO DISTINCT IDENTITIES are established. `spell_id` is the
caller-supplied structural fingerprint; `_id` is a fresh ULID
minted here that identifies THIS record. They are not
interchangeable - the fingerprint describes what the spell IS,
the ULID describes which object you are holding.
- The four `is_*` family flags are computed ONCE from `spell_type`
and cover all 14 `SpellType` members with no overlap, so exactly
one is True for the object's whole life.
- The canonical `(frame_key, bind_key)` lookup key is normalized
immediately via `SpellInputUtils.make_spell_key_from_parts(...)`
and fixed at construction; key identity never drifts afterwards.
- Disposal order and its presence flag are established once. The
resolved list is not copied, sorted, or frozen during construction.
Owned State:
Owns `_lock`, `_compiler_artifact` (created eagerly against
`spell_id`), `_creation_context_switch`, the hook lists, and the
metadata bags. BORROWS `_spellbook` and `_spell_system_states` -
the latter is read straight off the owning spellbook, so spell
cleanup must never tear it down.
Threading:
Creates the reentrant `_lock` that guards later configuration and
cleanup; construction itself is unsynchronized because the object
is not yet reachable by other threads. `_door_epoch` starts at 0 and
its bump sites assume per-spell serialization.
Lifecycle / Cleanup:
Born unowned and uncompiled. Ownership fields, the creation-context
factory, and every phase artifact stay `None`/False until conjure
stamps ownership and the compiler runs.
NOTE `_dynamic_environment` defaults to False, so
`apply_mutation_override` / `clear_mutation_override` raise
`RuntimeError` until a dynamic conduit stamps this spell.
`_caching_enabled` defaults True and may be overridden by the
owning Spellbook/Aether posture at that same stamping point.
Returns:
None.
"""
super().__init__()
self._lock = RLock()
self._active = True # active in its index; flipped by notch/disable
self._id: str = new_ulid() # Unique internal ID for tracking
# Spell Data
self.spell_index: SpellIndex = spell_index
self.spell: Any = spell # Object reference
self.spell_id: str = spell_id # SHA256 unique identifier
self.spellframe: Any | None = spellframe
self.spell_type: SpellType = spell_type
self._is_existing_creation: bool = spell_type in (
SpellType.EXISTING_CREATION,
SpellType.EXISTING_CREATION_WITH_SPELLFRAME,
SpellType.EXISTING_CREATION_WITH_BINDING_NAME_WITH_SPELLFRAME,
)
self._is_class_spell: bool = spell_type in (
SpellType.SPELL,
SpellType.SPELL_WITH_SPELLFRAME,
SpellType.SPELL_WITH_BINDING_NAME,
SpellType.SPELL_WITH_BINDING_NAME_WITH_SPELLFRAME,
)
self._is_method_spell: bool = spell_type in (
SpellType.METHOD,
SpellType.METHOD_WITH_BINDING_NAME,
SpellType.METHOD_WITH_SPELLFRAME,
SpellType.METHOD_WITH_BINDING_NAME_WITH_SPELLFRAME,
)
self._is_lambda_spell: bool = spell_type in (
SpellType.LAMBDA_METHOD_WITH_BINDING_NAME,
SpellType.LAMBDA_METHOD_WITH_SPELLFRAME,
SpellType.LAMBDA_METHOD_WITH_BINDING_NAME_WITH_SPELLFRAME,
)
self.user_created_object: object | None = existing_object
self.binding_name: str | None = binding_name
self.spell_name: str = spell_name
self.existence: Existence = existence
# Reflective spell profile.
# Treated as opaque here; downstream consumers normalize general or
# detailed profiles as needed.
self.profile: Any | None = profile
self.aetheric_frame: str = aetheric_frame
self.timeout: int | None = None # Optional timeout for spell execution
self.retries: int = 0 # Number of retries allowed for spell execution
# Permissions
self.permissions: Permissions = permissions
# Spellbook
self._spellbook: "Spellbook" = spellbook
self._spellbook_cleanup: bool = False
# Spell Metadata
self.tags = list(args) if args else []
self.metadata = kwargs if kwargs else {}
self._mutation_override: dict[str, Any] | None = None
self.disposal_method_names: list[str] = (
disposal_method_names if disposal_method_names is not None else []
)
self.has_disposal_methods: bool = bool(self.disposal_method_names)
# Hooks (private storage; Spellbook controls mutation)
self._hooks_enabled: bool = False
# Fast-meld-door invalidation epoch: bumped by every spell-level
# invalidation chokepoint (hook attach, resolution invalidation,
# creation-context cleanup/reset). Fast-door entries capture this
# value at build time and compare it per hit, replacing per-hit
# reads of the individual guard flags (shared-object traffic).
# Bump sites assume chokepoints are serialized per spell (they run
# under spell/spellbook locks or single-threaded build phases).
self._door_epoch: int = 0
self._pre_hooks: list[Callable[..., Any]] = []
self._activation_hooks: list[Callable[..., Any]] = []
self._post_hooks: list[Callable[..., Any]] = []
# Final build-time artifacts
self.dependency_graph: Any = None
self.dependencies: list[str] = [] # SHA256 spell IDs required for this spell to function
# Foundation artifact home for compiler/build state and validation
# artifacts owned directly by the Spell.
self._compiler_artifact: SpellCompilerArtifact = (
SpellCompilerArtifact(self.spell_id)
)
# Spell-owned meld execution context (created lazily by CreationContextFactory).
self._creation_context: "CreationContext | None" = None
# Spell-owned context factory configured at conduit ownership stamp time.
self._creation_context_factory: CreationContextFactory | None = None
# Spell-owned selector latch for one-leader CreationContext publication.
self._creation_context_switch: CounterSwitch = CounterSwitch(state=0)
# Runtime cache policy mirror. Defaults to enabled and may be overridden
# later by the owning Spellbook/Aether posture during conduit stamping.
self._caching_enabled: bool = True
# Runtime mode carried from owning conduit for context factory wiring.
self._dynamic_environment: bool = False
# Runtime resolution gate flag (False for full AOT by default).
self.resolution_required: bool = False
# Runtime deferred-resolution completion flag.
# Starts False and flips True only when Phase12 compile wiring completes.
self.resolution_complete: bool = False
# Compiler-derived runtime request flag.
# True means the rooted request graph contains a spellspace-scoped
# dependency, and the spell therefore requires an active spellspace
# request context at runtime.
self.requires_spellspace_request: bool = False
# Created after Conduit made (ownership / scope integration)
self._owner_conduit_id: str | None = None
self._owner_conduit_name: str | None = None
self._owner_creations: Any = None # Scope level creations for singletons
# Spell System State
self._spell_system_states: SpellSystemStates = self._spellbook._spell_system_states
# Key for the spell in the Spellbook (normalized)
frame_key, bind_key = SpellInputUtils.make_spell_key_from_parts(
spellframe=self.spellframe,
spell_name=self.spell_name,
binding_name=self.binding_name,
)
self._key = (frame_key, bind_key)
#region Disposal
[docs]
def cleanup(self) -> None:
"""
Release the spell-owned runtime state and permanently retire this Spell.
Purpose:
Deterministically tear down the spell-local runtime surface so later
code cannot keep using stale build artifacts, runtime contexts, or
owner references after the spell leaves service.
Contract:
- Idempotent: repeated calls become no-ops after `_cleaned` flips.
- Thread-safe: acquires `_lock`, re-checks `_cleaned`, and then
performs teardown under the guarded section.
- Best-effort child cleanup: owned child cleanup failures are
swallowed so teardown still reaches the final cleared state.
- Clears hooks, metadata, dependency/build artifacts, execution-plan
metrics, spell-owned factory/context state, conduit ownership
state, spellbook references, and reflective profile state.
- Sets `_cleaned` before the guarded section exits, then drops the
`_lock` reference itself after the teardown completes.
- Deletes the disposal-name reference without clearing its list;
creation entries may still retain that established metadata.
Runtime resolution and instance lifecycle remain owned by the Resolution
/ Meld layer, not by this class.
Returns:
None.
"""
if self._cleaned:
return
with self._lock:
if self._cleaned:
return
if not self._spellbook_cleanup:
self._spellbook.cleanup_and_remove_spell(self)
return
if self.dependency_graph is not None:
try:
self.dependency_graph.cleanup()
except Exception:
# Never let cleanup explosions propagate.
pass
if self.profile is not None and isinstance(self.profile, Cleanable):
try:
self.profile.cleanup()
except Exception:
pass
try:
self._compiler_artifact.cleanup()
except Exception:
pass
# Drop references to help GC and enforce immutability after cleanup.
self._cleanup_creation_context()
self._cleanup_creation_context_factory()
try:
self._creation_context_switch.cleanup()
except Exception:
pass
if self._pre_hooks:
self._pre_hooks.clear()
if self._activation_hooks:
self._activation_hooks.clear()
if self._post_hooks:
self._post_hooks.clear()
if self.tags:
self.tags.clear()
if self.metadata:
self.metadata.clear()
if self._mutation_override:
self._mutation_override.clear()
if self.dependencies:
self.dependencies.clear()
self._cleaned = True
self._hooks_enabled = False
del self._owner_creations
del self.user_created_object
del self._spellbook_cleanup
del self._spell_system_states
del self._spellbook
del self._pre_hooks
del self._activation_hooks
del self._post_hooks
del self.tags
del self.metadata
del self._mutation_override
del self.dependencies
del self.disposal_method_names
del self.dependency_graph
del self.profile
del self.spell
del self._creation_context
del self._creation_context_factory
del self._creation_context_switch
del self._caching_enabled
del self._compiler_artifact
del self.spell_index
#endregion Disposal
def _set_hooks(
self,
*,
pre_hooks: Sequence[Callable[..., Any]] | None = None,
activation_hooks: Sequence[Callable[..., Any]] | None = None,
post_hooks: Sequence[Callable[..., Any]] | None = None,
) -> None:
"""
Internal
Attach lifecycle hook lists and update the spell hook gate.
Contract:
- Replaces only the hook lists provided (None means "leave as-is").
- Updates `_hooks_enabled` based on current hook list contents.
- Hook callability is validated by Spellbook before this is called.
- Requires a live Spell instance.
Args:
pre_hooks:
Optional list/tuple of pre-cast hooks. Each hook must accept no
arguments and be invoked before resolution.
activation_hooks:
Optional list/tuple of activation hooks. Each hook receives the
newly created instance as its first argument.
post_hooks:
Optional list/tuple of post-cast hooks. Each hook must accept no
arguments and be invoked after resolution.
Returns:
None.
Raises:
RuntimeError: If the Spell has already been cleaned.
"""
with self._lock:
if pre_hooks is not None:
self._pre_hooks = list(pre_hooks)
if activation_hooks is not None:
self._activation_hooks = list(activation_hooks)
if post_hooks is not None:
self._post_hooks = list(post_hooks)
self._hooks_enabled = bool(
self._pre_hooks or self._activation_hooks or self._post_hooks
)
# Invalidate fast-meld-door entries: the hook gate changed.
self._door_epoch += 1
def _cleanup_creation_context(self) -> None:
"""
Internal
Dispose and clear the spell-owned CreationContext, if present.
Contract:
- Idempotent and safe to call repeatedly.
- Best-effort cleanup; exceptions are
swallowed so callers can continue ownership/dirty transitions.
- Leaves `_creation_context` as `None`.
- Resets `_creation_context_switch` to idle state (`0`) so
future `get_or_build` calls can elect a new leader.
"""
# Invalidate fast-meld-door entries before the teardown: covers the
# context clear AND the switch reset (a reset without replacement
# would otherwise pass the door's context-identity pin).
self._door_epoch += 1
if self._creation_context is not None:
try:
self._creation_context.cleanup()
except Exception:
pass
self._creation_context = None
if self._creation_context_switch.state > 0:
self._creation_context_switch.advance(
-self._creation_context_switch.state
)
def _cleanup_creation_context_factory(self) -> None:
"""
Internal
Dispose and clear the spell-owned CreationContextFactory, if present.
Contract:
- Idempotent and safe to call repeatedly.
- Best-effort cleanup; exceptions are swallowed so ownership
transitions can continue.
- Leaves `_creation_context_factory` as `None`.
"""
if self._creation_context_factory is not None:
try:
self._creation_context_factory.cleanup()
except Exception:
pass
self._creation_context_factory = None
def _configure_creation_context_factory(
self,
*,
dynamic_environment: bool,
creation_gate_controller: "CreationGateController",
) -> None:
"""
Internal
Rebuild the spell-owned CreationContextFactory for current conduit ownership.
Purpose:
Ensure CreationContextFactory dependencies track the latest owner
conduit runtime mode and frame gate-governance surface.
Contract:
- Replaces any existing factory instance.
- Stores dynamic mode on the spell for runtime metadata.
- Requires a non-null CreationGateController.
Args:
dynamic_environment:
True when the owning conduit runs in dynamic mode.
creation_gate_controller:
Frame-owned CreationGateController used by the factory for
spell-lineage gate operations.
Returns:
None.
Raises:
ValueError:
If `creation_gate_controller` is None.
"""
if creation_gate_controller is None:
raise ValueError("creation_gate_controller cannot be None.")
self._cleanup_creation_context_factory()
self._dynamic_environment = bool(dynamic_environment)
self._creation_context_factory = CreationContextFactory(
dynamic_environment=self._dynamic_environment,
creation_gate_controller=creation_gate_controller,
)
def _get_or_build_creation_context(self) -> Any:
"""
Internal
Resolve or build the spell-owned CreationContext through its factory.
Purpose:
Provide one spell-local runtime entrypoint for creation-context
retrieval so callers do not directly own factory references.
Contract:
- Requires the spell to have an initialized factory.
- Returns a ready state-2 context without locking.
- Serializes only the cold/rebuild path under the spell RLock and
rechecks readiness after acquiring it. A conduit-local phase run
owns the same lock while it clears and republishes phase-11
state, so a competing conduit cannot build from the transient
artifact gap.
- Delegates build/get policy to CreationContextFactory.
- Returns a live CreationContext instance bound to this spell.
Threading:
The normal ready-context path remains one lock-free state read and
one context read. Only state 0/1 retrieval takes `_lock`; the lock
is re-entrant because phase and ownership callers may already hold
it while requesting a rebuild.
Returns:
Any:
Spell-owned CreationContext instance.
Raises:
RuntimeError:
If the spell has no configured CreationContextFactory.
"""
creation_context_switch = self._creation_context_switch
if creation_context_switch.state >= 2:
return self._creation_context
creation_context_factory = self._creation_context_factory
if creation_context_factory is None:
raise RuntimeError("Spell has no configured CreationContextFactory.")
return creation_context_factory.get_or_build_for_spell(self)
[docs]
def emit_cache(self) -> bool:
"""
Public API
Emit this spell's current cache payload through its owning Spellbook.
Purpose:
Provide one spell-facing cache export entrypoint without moving
cache ownership or file mutation into `CreationContext`.
Contract:
- Returns early when spell-level cache policy is disabled.
- Delegates the real cache update to the owning Spellbook.
- Requires the spell to still have a live owning Spellbook.
Returns:
bool:
True when the spell emitted a cache payload, otherwise False.
Raises:
RuntimeError:
If the spell no longer has an owning Spellbook.
"""
self.check_cleaned()
if not self._caching_enabled:
return False
spellbook = self._spellbook
if spellbook is None:
raise RuntimeError("Spell has no owning Spellbook surface.")
if self._creation_context is None:
return False
return spellbook._emit_spell_cache(self)
[docs]
def emit_cache_file(self) -> bool:
"""
Public API
Emit the Spellbook-owned cache file for this spell's current cache state.
Purpose:
Provide one spell-facing entrypoint for forcing the current
Spellbook-owned in-memory cache state to disk.
Contract:
- Returns early when spell-level cache policy is disabled.
- Delegates the real file emit to the owning Spellbook.
- Requires the spell to still have a live owning Spellbook.
Returns:
bool:
True when the Spellbook cache file was emitted, otherwise False.
Raises:
RuntimeError:
If the spell no longer has an owning Spellbook.
"""
self.check_cleaned()
if not self._caching_enabled:
return False
spellbook = self._spellbook
if spellbook is None:
raise RuntimeError("Spell has no owning Spellbook surface.")
return spellbook._emit_cache_file(self)
#region Context Manager
def __enter__(self) -> Spell:
"""
Acquire the spell's internal lock and return `self`.
Purpose:
Allow internal configuration code to group multiple field updates
under the same lock without exposing `_lock` directly.
Contract:
- Intended for internal use only.
- Does not perform a cleaned-state guard on its own; callers must
ensure they are operating on a live spell.
- Must be paired with `__exit__` to avoid leaking the lock.
Returns:
Spell:
This spell instance while the internal lock is held.
"""
self._lock.acquire()
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
"""
Release the spell's internal lock after a context-manager's block.
Contract:
- Releases unconditionally, including when the block raised. The
exception arguments are accepted and IGNORED - returning `None`
is falsy, so no exception is ever suppressed here.
- Exactly one release per `__enter__`. The lock is an `RLock`, so
nested `with spell:` blocks are legal and each level must exit.
- Performs no cleaned-state guard and no state mutation; it is
purely the unlock half of the pair.
Raises:
RuntimeError: If called without a matching `__enter__` on this
thread, propagated from the underlying lock release.
Returns:
None.
"""
self._lock.release()
#endregion Context Manager
def __repr__(self) -> str:
"""
Return a concise diagnostic representation for logging and debugging.
Contract:
- Includes spell name, binding key, frame label, and the SHA256
spell identifier.
- Falls back to `type(self.spell).__name__` when no explicit
`spellframe` exists.
Returns:
str:
Stable human-readable representation of this spell's identity.
"""
if self.spellframe:
frame = getattr(self.spellframe, "__name__", str(self.spellframe))
else:
frame = type(self.spell).__name__
return (
f"Spell(name={self.spell_name}, binding={self.binding_name or '__default__'}, "
f"frame={frame}, SHA256={self.spell_id})"
)
#region Introspection Helpers
@property
def key(self) -> tuple[str, str]:
"""
Internal
Return the canonical `(frame_key, binding_key)` lookup tuple.
Contract:
- Always reflects the bind-time normalized key produced by
`SpellInputUtils.make_spell_key_from_parts(...)`.
- Read-only at the Spell layer; callers must not mutate key
semantics after bind time.
Returns:
tuple[str, str]:
Canonical Spellbook dictionary key for this spell.
"""
return self._key
@property
def is_existing_creation(self) -> bool:
"""
Whether this spell represents an existing, pre-created object.
Contract:
- Derived ONCE from `spell_type` during `__init__` and never
recomputed; it is a bind-time fact, not runtime state.
- Part of a strict four-way partition with `is_class_spell`,
`is_method_spell` and `is_lambda_spell`: the four families cover
all 14 `SpellType` members with no overlap, so for any spell
EXACTLY ONE of them is True. Branching on all four without a
fallback is safe and total.
- Answers "did the user hand us the object?", which is distinct
from `has_existing_object` - that one asks whether the object is
still attached right now.
Threading:
Unsynchronized read of an immutable bool; safe from any thread.
Returns:
bool:
True only for `EXISTING_CREATION*` spell types.
"""
return self._is_existing_creation
@property
def is_class_spell(self) -> bool:
"""
Whether this spell represents a class-backed factory registration.
Contract:
- Derived ONCE from `spell_type` during `__init__`; bind-time fact.
- Covers the four `SPELL*` members. This is the FACTORY family:
melding constructs an instance per the spell's `Existence`
policy, as opposed to `is_existing_creation` where the instance
already exists.
- Exactly one of the four family flags is True for any spell (see
`is_existing_creation` for the full partition).
Threading:
Unsynchronized read of an immutable bool; safe from any thread.
Returns:
bool:
True only for `SPELL*` spell types.
"""
return self._is_class_spell
@property
def is_method_spell(self) -> bool:
"""
Whether this spell represents a non-lambda method or function registration.
Contract:
- Derived ONCE from `spell_type` during `__init__`; bind-time fact.
- Covers the four named `METHOD*` members and EXCLUDES the three
`LAMBDA_METHOD*` members, which report through `is_lambda_spell`.
The split exists because a lambda has no stable qualname to bind
against, so lambda variants always carry a binding name or
spellframe.
- Exactly one of the four family flags is True for any spell (see
`is_existing_creation` for the full partition).
Threading:
Unsynchronized read of an immutable bool; safe from any thread.
Returns:
bool:
True only for non-lambda `METHOD*` spell types.
"""
return self._is_method_spell
@property
def is_lambda_spell(self) -> bool:
"""
Whether this spell represents one of the lambda-backed method spell variants.
Contract:
- Derived ONCE from `spell_type` during `__init__`; bind-time fact.
- Covers the three `LAMBDA_METHOD*` members. Every one of them
carries a binding name, a spellframe, or both - there is no bare
`LAMBDA_METHOD` member, because an anonymous function supplies no
usable identity of its own to bind against.
- Exactly one of the four family flags is True for any spell (see
`is_existing_creation` for the full partition).
Threading:
Unsynchronized read of an immutable bool; safe from any thread.
Returns:
bool:
True only for lambda `METHOD*` spell types.
"""
return self._is_lambda_spell
@property
def has_existing_object(self) -> bool:
"""
Whether this spell currently holds a concrete user-provided object.
Contract:
- Meaningful only for `EXISTING_CREATION*` spell types.
- Returns False for factory-style spell types even if they later
create runtime instances through conduits.
Returns:
bool:
True when `user_created_object` is currently attached.
"""
return self.user_created_object is not None
@property
def owner_conduit_info(self) -> tuple[str | None, str | None]:
"""
Return the current conduit ownership tuple for this spell.
Contract:
- Both halves start `None` and are populated together when the
owning conduit stamps ownership, which happens after conjure -
never at bind time. `(None, None)` therefore means "not yet
owned", not "error".
- The name half is a convenience label and may be `None` even once
the id is set, for conduits created without an explicit name.
Threading:
Reads the two attributes separately WITHOUT holding `self._lock`,
so the pair is not snapshotted atomically. A read racing an
ownership stamp can observe a mixed tuple (new id, stale name).
Callers that need a coherent pair must hold the spell lock via the
`with spell:` context manager.
Returns:
tuple[Optional[str], Optional[str]]:
`(owner_conduit_id, owner_conduit_name)` when ownership has been
stamped, otherwise `(None, None)`.
"""
return self._owner_conduit_id, self._owner_conduit_name
@property
def requirements(self) -> SpellRequirements | None:
"""
Phase 1 artifact for this spell, if it has been computed.
This is populated by the compiler artifact during structural phase
execution.
Contract:
- PHASE-GATED PROBE. `None` means Phase 1 has not produced this
artifact yet - either the structural phases have not run, or the
artifact was invalidated and reset. It is never an error value,
so this property does not raise for an un-run phase.
- A read-through onto the spell-owned `SpellCompilerArtifact`. The
Spell is the public face of that artifact; the artifact itself is
internal.
- The returned object is live, not a copy. A subsequent
recompilation can replace or mutate it underneath the caller.
Threading:
Unsynchronized read-through; a snapshot of a reference that
compilation can swap concurrently.
Returns:
Optional[SpellRequirements]: Phase 1 requirements, or None before the
structural phases have run for this spell.
"""
return self._compiler_artifact._requirements
@property
def symbolic_graph(self) -> SpellSymbolicGraph | None:
"""
Phase 2 symbolic graph for this spell, if it has been computed.
This is populated by the compiler artifact during structural phase
execution.
Contract:
- PHASE-GATED PROBE; `None` means Phase 2 has not produced this
artifact yet, or it was reset by an invalidation. Not an error.
- Phase 2 consumes Phase 1, so a non-None graph implies
`requirements` was populated at the time the graph was built -
but not that it is still populated now, since invalidation can
clear artifacts independently.
- Read-through onto the spell-owned `SpellCompilerArtifact`;
returns the live object, not a copy.
Threading:
Unsynchronized read-through; snapshot only.
Returns:
Optional[SpellSymbolicGraph]: The Phase 2 symbolic graph, or None before
Phase 2 has run.
"""
return self._compiler_artifact._symbolic_graph
@property
def resolution_frame(self) -> Any:
"""
Phase 3 local resolution frame / DAG for this spell, if it has been computed.
This is populated by the compiler artifact during structural phase
execution.
Concrete type is intentionally opaque here; callers should treat it as
an internal resolution artifact.
Contract:
- PHASE-GATED PROBE; `None` means Phase 3 has not produced this
artifact yet, or it was reset. Not an error.
- The `Any` return type is DELIBERATE, not missing typing. The
concrete resolution/DAG type is internal and free to change;
treat the value as an opaque handle to pass back into melder,
and do not branch on its structure.
- Read-through onto the spell-owned `SpellCompilerArtifact`;
returns the live object, not a copy.
Threading:
Unsynchronized read-through; snapshot only.
Returns:
Any: The Phase 3 resolution frame, or None before Phase 3 has run.
"""
return self._compiler_artifact._resolution_frame
@property
def validation_result_phase4(self) -> SpellValidationResult | None:
"""
Phase 4 validation result for this spell, if it has been computed.
This is populated by the compiler artifact during structural phase
execution.
Contract:
- THIS IS THE DISCRIMINATOR for the `validated` / `is_broken`
booleans. Those two are `False` both before Phase 4 runs and
after a failing verdict; this property is `None` in the first
case and non-None in the second, so it is the only way to tell
"not checked" from "checked".
- STRUCTURAL verdict only - it judges the spell in isolation.
Conduit-scoped judgement lives in `validation_result_phase6`,
and a spell can pass Phase 4 and still fail Phase 6.
- Read-through onto the spell-owned `SpellCompilerArtifact`;
returns the live object, not a copy.
Threading:
Unsynchronized read-through; snapshot only.
Returns:
Optional[SpellValidationResult]: The structural (Phase 4) verdict, or None
if Phase 4 has not run for this spell.
"""
return self._compiler_artifact._validation_result_phase4
@property
def validation_result_phase6(self) -> SpellSystemValidationState | None:
"""
Phase 6 validation result for this spell, if it has been computed.
This is populated by the compiler artifact during conduit-scoped
validation.
Contract:
- CONDUIT-SCOPED verdict, unlike the structural Phase 4 result.
`None` here is expected for a bound-but-never-conjured spell,
because Phase 6 runs as part of conduit-scoped validation rather
than at bind time.
- Passing Phase 4 does not imply Phase 6 will pass: a spell can be
structurally sound and still be unsatisfiable in the conduit it
is asked to live in (missing dependency, permission refusal).
- `None` is not an error and this property does not raise.
- Read-through onto the spell-owned `SpellCompilerArtifact`;
returns the live object, not a copy.
Threading:
Unsynchronized read-through; snapshot only.
Returns:
Optional[SpellSystemValidationState]: The system (Phase 6) verdict, or None
if Phase 6 has not run for this conduit.
"""
return self._compiler_artifact._validation_result_phase6
@property
def validated(self) -> bool:
"""
Whether Phase 4 validation currently considers this spell valid.
Contract:
- `False` IS AMBIGUOUS. It means either "Phase 4 has not run" or
"Phase 4 ran and the spell did not pass". The flag alone cannot
distinguish them, because it is initialized `False` and is reset
to `False` whenever the artifact is invalidated.
- To tell the two apart, read `validation_result_phase4`: `None`
means the phase has not produced a verdict; a non-None result
means the verdict is real and `validated` reflects it.
- Never raises for an un-run phase; absence is reported as `False`,
not as an error.
Threading:
Unsynchronized read of the compiler artifact's flag. It is a
snapshot; a concurrent revalidation can flip it immediately after
this returns.
Returns:
bool:
False until Phase 4 validation has populated the compiler
artifact.
"""
return self._compiler_artifact._validated_phase4
@property
def is_broken(self) -> bool:
"""
Whether validation currently classifies this spell as broken or unsafe.
Contract:
- `False` IS AMBIGUOUS in the same way as `validated`: it is the
initial value AND the reset value, so an unvalidated spell and a
validated-healthy spell both report `False`.
- `is_broken` is NOT the negation of `validated`. A spell that has
never been validated reports `validated=False` AND
`is_broken=False` simultaneously. Do not treat
`not is_broken` as "safe to use".
- The honest health check is: `validation_result_phase4 is not None`
(a verdict exists) AND `validated` (the verdict passed).
Threading:
Unsynchronized read of the compiler artifact's flag; a snapshot
only.
Returns:
bool:
False until Phase 4 validation has populated the compiler
artifact.
"""
return self._compiler_artifact._is_broken
#endregion Introspection Helpers
#region Configuration
[docs]
def invalidate_spell(
self,
change_reason: SpellStateChangeReason | None = None,
) -> None:
"""
Invalidate this spell for a full next-meld rebuild.
Purpose:
Provide one spell-local helper for the common "this spell is no
longer trustworthy; rebuild it on the next meld" path. This method
is the spell-owned convenience wrapper over two different
invalidation layers:
1. spell-local runtime invalidation
- clear the cached `CreationContext`
- force deferred runtime resolution to run again
2. lineage/control-plane invalidation
- mark the lineage structurally gated in `SpellSystemStates`
Contract:
- Safe to call multiple times on a live spell.
- Requires the spell to be attached to a dynamic runtime
environment.
- Clears the spell-owned `CreationContext` so the cached runtime dispatch state cannot survive a structural invalidation.
- Sets `resolution_complete=False` and `resolution_required=True`
so the next meld re-enters the deferred runtime plan path after
structural validation succeeds.
- Uses `SpellSystemStates.mark_structural_change(...)` when the
control-plane registry is available.
- Defaults the reason to `SpellStateChangeReason.structure_changed`
when callers do not supply a more specific reason.
- Intentionally does not use transfer-only hard-disable semantics;
this helper models the recoverable post-change posture rather than
the unsafe mid-transfer posture.
Args:
change_reason:
Optional structural change reason to record in the lineage
state. When omitted, the helper uses
`SpellStateChangeReason.structure_changed`.
Returns:
None.
Raises:
RuntimeError:
If the spell has already been cleaned, or if the spell is not
attached to a dynamic runtime environment.
"""
self.check_cleaned()
if change_reason is None:
change_reason = SpellStateChangeReason.structure_changed
if not self._dynamic_environment:
raise RuntimeError(
"Dynamic environment is not enabled. Spell invalidation for revalidation requires dynamic mode."
)
with self._lock:
self._cleanup_creation_context()
self.resolution_complete = False
self.resolution_required = True
if self._spell_system_states is not None and self.spell_index is not None:
self._spell_system_states.mark_structural_change(
self.spell_index,
change_reason,
)
def _add_owned_conduit(
self,
conduit_id: str,
conduit_name: str | None = None,
creations: Any = None,
*,
dynamic_environment: bool,
creation_gate_controller: "CreationGateController",
caching_enabled: bool,
) -> None:
"""
Internal
Records ownership information about the Conduit that \"owns\" this spell.
This is used to:
- Attach the spell to a specific Conduit identity (for logging, diagnostics, and scoping).
- Provide a handle to the Conduit's creation scope (e.g., for singletons tied to that conduit).
- Reconfigure CreationContextFactory dependencies for the new owner.
- Invalidate the spell-owned CreationContext because ownership/scoping changed.
Args:
conduit_id (str):
The unique ID of the conduit that owns this spell.
conduit_name (Optional[str]):
Human-readable name of the owning conduit, if available.
creations (Any):
Conduit-level creations container used for managing shared instances.
dynamic_environment (bool):
True when the owning conduit runs in dynamic mode.
creation_gate_controller (CreationGateController):
Frame-owned CreationGateController used by CreationContextFactory.
caching_enabled (bool):
True when the owning Spellbook/Aether posture wants this spell
to emit runtime payloads into the cache.
Returns:
None.
"""
with self._lock:
# Ownership changes invalidate spell-bound runtime context shape.
self._cleanup_creation_context()
self._configure_creation_context_factory(
dynamic_environment=dynamic_environment,
creation_gate_controller=creation_gate_controller,
)
self._owner_conduit_id = conduit_id
self._owner_conduit_name = conduit_name
self._owner_creations = creations
self._caching_enabled = caching_enabled
def _add_build_details(
self,
dag: Any,
dependencies: list[str],
) -> None:
"""
Internal
Attach static build-time dependency graph details to this spell.
This is typically invoked by the structural DAG builder after it has
analyzed the spell's parameters and constructed a dependency DAG.
Contract:
- Replaces the current dependency graph/dependencies references.
- Invalidates any existing spell-owned CreationContext so the runtime shape is rebuilt against the updated spell structure.
Args:
dag:
A static DAG representation for this spell's dependency structure.
This object is considered immutable at runtime and may expose a
`dispose()` method for cleanup.
dependencies:
A list of spell_ids (SHA256 fingerprints) that this spell depends on.
Raises:
ValueError:
If `dag` is None or `dependencies` is None.
"""
if dag is None:
raise ValueError("Dependency graph cannot be None.")
if dependencies is None:
raise ValueError("Dependencies cannot be None.")
with self._lock:
self.dependency_graph = dag
self.dependencies = dependencies
self._cleanup_creation_context()
#endregion Configuration
#region Spell Mutations
@property
def system_state(self) -> SpellSystemState | None:
"""
Return the SpellSystemState instance associated with this spell's lineage.
This is a read-mostly view into the change-control and validation state
tracked by SpellSystemStates.
Contract:
- Mutation and contract operations can ask for the current lineage state.
- Higher-level dev-ops and validation pipelines can inspect this value
while orchestrating Phase 1-7 revalidation.
- Returns `None` when SpellSystemStates are unavailable or the lineage is
not currently tracked.
Returns:
Optional[SpellSystemState]:
The state object for this spell's lineage, if available.
"""
self.check_cleaned()
if self._spell_system_states is None:
return None
# SpellSystemStates tracks states by SpellIndex.id / spell_id
return self._spell_system_states.get_by_index_id(self.spell_index.id)
# ------------------------------------------------------------------
# Mutation override (graph overlay) API
# ------------------------------------------------------------------
@property
def mutation_override(self) -> dict:
"""
Current persistent default override payload for this spell.
This payload is pre-normalized into the same override-map shape that
meld-time runtime overrides use. It is conceptually separate from the
caller-supplied `spell_override` argument passed into `meld(...)`:
- meld `spell_override` -> one-call runtime override payload
- `Spell.mutation_override` -> persistent default override payload
stored on the spell itself
Contract:
- `None` is the ONLY inactive sentinel. An empty dict is never
stored: `apply_mutation_override({})` normalizes to `None`, so a
caller testing `== {}` will never match. Test `is None`, or use
`has_mutation_override`.
- Positional payloads are normalized into `{"__args__": [...]}`.
- EMPTY POSITIONAL IS NOT EMPTY. `apply_mutation_override([])`
stores `{"__args__": []}`, which is a non-empty dict and
therefore reports `has_mutation_override == True`. Empty dict and
empty list are deliberately NOT symmetric: `{}` clears, `[]`
installs a positional override of zero arguments.
- Keyword payloads are copied into a fresh dict so later meld calls
do not depend on caller-owned containers.
Note:
The declared return annotation is `dict`, but this property returns
`Optional[dict[str, Any]]` - `None` is the normal inactive value.
Trust the Returns block below over the signature.
Returns:
Optional[dict[str, Any]]:
The normalized persistent default override payload currently
attached to this spell, or `None` when no default payload is
active.
"""
return self._mutation_override
@property
def has_mutation_override(self) -> bool:
"""
Whether this spell currently has a non-empty mutation overlay.
This is a convenience for Dynamic or AI-native flows that want a quick
check before doing more expensive revalidation or graph rebuilds.
Contract:
- Truthiness test over the stored payload, so it is `False` exactly
when the payload is `None`.
- Reports `True` for a zero-argument POSITIONAL override, because
`[]` normalizes to `{"__args__": []}` which is a non-empty dict.
`apply_mutation_override([])` therefore leaves this `True` while
`apply_mutation_override({})` leaves it `False`.
- Unlike `apply_mutation_override` / `clear_mutation_override`,
this does NOT require a dynamic environment and does not raise;
it is safe to probe on any live spell.
Threading:
Unsynchronized single-attribute read.
Returns:
bool:
True when the current overlay payload is non-empty.
"""
return bool(self._mutation_override)
[docs]
def apply_mutation_override(
self,
override: dict | list | tuple | None,
) -> None:
"""
Apply or replace the persistent default override payload for this spell.
Contract:
- Requires the spell to be attached to a dynamic runtime
environment.
- Normalizes the payload into the same runtime override-map shape
that meld-time caller overrides use.
- Stores only the normalized runtime payload shape on the spell.
- Does not invalidate the spell, clear CreationContext, or mark
structural change state.
- Treats `None` and empty dict payloads as "no active default
override payload."
Args:
override:
New persistent default override payload. Supported shapes match
meld-time override payloads:
- `dict` for targeted keyword-style overrides
- `list` / `tuple` for root positional overrides
- `None` to clear the default payload
Returns:
None.
Raises:
RuntimeError:
If the spell is not attached to a dynamic runtime environment.
TypeError:
If `override` is not one of the supported override payload
shapes.
"""
self.check_cleaned()
if not self._dynamic_environment:
raise RuntimeError(
"Dynamic environment is not enabled. Mutation overrides require dynamic mode."
)
self._mutation_override = self._normalize_mutation_override_payload(override)
[docs]
def clear_mutation_override(self) -> None:
"""
Clear any active persistent default override payload for this spell.
Contract:
- Requires the spell to be attached to a dynamic runtime
environment.
- Resets the stored payload back to `None`.
- Does not invalidate the spell or rebuild runtime shape.
Returns:
None.
Raises:
RuntimeError:
If the spell is not attached to a dynamic runtime environment.
"""
self.check_cleaned()
if not self._dynamic_environment:
raise RuntimeError(
"Dynamic environment is not enabled. Mutation overrides require dynamic mode."
)
self._mutation_override = None
@staticmethod
def _normalize_mutation_override_payload(
override: dict | list | tuple | None,
) -> dict[str, Any] | None:
"""
Normalize one stored mutation override payload into runtime shape.
Contract:
- `None` and empty dict payloads normalize to `None`.
- Dict payloads are shallow-copied into a fresh mapping.
- Positional list/tuple payloads normalize to `{"__args__": [...]}`.
- Raises on unsupported payload shapes instead of coercing them.
"""
if override is None:
return None
if isinstance(override, dict):
if not override:
return None
return dict(override)
if isinstance(override, (list, tuple)):
return {"__args__": list(override)}
raise TypeError(
"mutation_override must be a dict, list, or tuple."
)
#endregion Spell Mutations
#endregion Spell