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.binding_manifest import BindingManifest
from melder.aether.spellbook.bind.spell_index import SpellIndex
from melder.aether.spellbook.existence.existence import Existence
from melder.aether.spellbook.existence.managed_spell_mode import ManagedSpellMode
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.aether.spellbook.spellframe_kind.spellframe_kind import SpellframeKind
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 import CreationGate
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`, the
index that organizes this spell with the other members and selects one.
- 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 binding hash describe that same policy. Post-creation mutation
is unsupported; this class adds no copying or mutation guards.
- Resolution capability is fixed per bound version and exposed by the
read-only resolvable property, independently of activity and compilation state.
- The managed options of an `Existence.managed_spell_quantity` binding (mode, limit,
queue timeout, reset method names) are fixed per bound version the same way, as
Bind normalized them, and exposed by read-only properties; they are part of the
binding manifest, never of the spell_id.
- `binding_manifest` records how this version is bound - its address, spellframe and
kind, existence, disposal order, resolvable and managed settings - with its own
SHA256, `binding_hash`. Built once at construction from the same arguments and kept
after cleanup, like `spell_id`: what a spell IS is its spell_id, how it is kept is
its binding (EPIC-2026-10-18-spell_identity_is_the_code).
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 SHA256 `spell_id`: the digest of the registered object's code
(`SpellCodeDigest`) and nothing from the bind call. How it is bound is its
`binding_manifest`, hashed apart as `binding_hash`.
- `spellframe` is the category half of the address: a string category or a
Protocol contract. `spellframe_kind` records which (or `none` for a bare
binding) and `implemented_protocols` the Protocol bind checked the spell
against, so Phase 3 never infers a meaning from the frame's name.
- 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):
The SpellIndex this spell is organized under; the index selects one member
version as active.
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):
The SHA256 of the registered object's code (`SpellCodeDigest`); nothing from
the bind call enters it.
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 the `SpellIndex` that organizes it) from a user target;
`Spellbook` registers it into its owned/contracted maps and spell-id caches and
registers its SpellIndex 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",
"_resolvable",
"_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",
# Appended, not alphabetized: new slots go last so the offsets of the
# hot fast-door slots (`_door_epoch`, `_creation_context`) stay put.
"_creation_context_failure",
"_creation_gate",
"spellframe_kind",
"implemented_protocols",
"_removal_lock",
"_managed_spell_mode",
"_managed_spell_limit",
"_managed_spell_timeout_seconds",
"_managed_spell_reset_methods",
"binding_manifest",
]
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,
resolvable: bool = True,
spellframe_kind: SpellframeKind = SpellframeKind.none,
implemented_protocols: tuple[type, ...] = (),
managed_spell_mode: Optional[ManagedSpellMode] = None,
managed_spell_limit: Optional[int] = None,
managed_spell_timeout_seconds: Optional[float] = None,
managed_spell_reset_methods: tuple[str, ...] = (),
**kwargs: Any,
) -> None:
"""
Internal constructor for a bound Spell record.
Args:
spell (Any): The underlying callable/class/instance being registered.
spell_index (SpellIndex): The SpellIndex this spell is organized under (it selects one member).
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): The SHA256 of the registered object's code (`SpellCodeDigest`).
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`.
resolvable (bool): Validated per-version resolution capability. Stored natively,
outside metadata, and independent of active/parked or compiler readiness state.
spellframe_kind (SpellframeKind): What `spellframe` is - `none` (bare), `category`
(a string) or `contract` (a Protocol) - as Bind classified it. Phase 3 reads it.
implemented_protocols (tuple[type, ...]): The Protocol(s) Bind structurally checked
this spell against: `(spellframe,)` for a contract frame, else empty.
managed_spell_mode (Optional[ManagedSpellMode]): The pool policy of a managed binding,
as Bind normalized it; None on any other existence.
managed_spell_limit (Optional[int]): Elastic idle cap, or the most objects alive for
limited_quantity and queue; None on any other existence.
managed_spell_timeout_seconds (Optional[float]): Queue wait in seconds; None otherwise.
managed_spell_reset_methods (tuple[str, ...]): Reset method names, run in order on every
release; empty when none were named or the spell is not managed.
**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 code digest of the target; `_id` is a fresh ULID
minted here that identifies THIS record. They are not
interchangeable - the digest describes what the spell IS,
the ULID describes which object you are holding.
- The binding manifest (`binding_manifest`, `binding_hash`) is built
last, from these same arguments: how this version is bound, hashed
apart from what it is. A value record, kept after cleanup.
- 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. `_removal_lock` starts
as None; the first direct `cleanup()` creates it under `_lock`.
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()
# Serializes direct cleanup() removals of this spell, which run outside `_lock`; created by the first direct
# cleanup (`_direct_removal_route`) and taken by nothing else (direct_spell_cleanup_lock_order, 0.2.8233).
self._removal_lock: Optional[RLock] = None
self._active = True # active in its index; flipped by notch/disable
self._resolvable: bool = resolvable
# Managed options (Existence.managed_spell_quantity): normalized by Bind, part of the binding manifest
# and never of the spell_id (since 0.2.8254); read-only per-version policy like `_resolvable`;
# None / None / None / () on any other existence.
# The spell's pool is composed from them when its owner store is stamped (S3).
self._managed_spell_mode: Optional[ManagedSpellMode] = managed_spell_mode
self._managed_spell_limit: Optional[int] = managed_spell_limit
self._managed_spell_timeout_seconds: Optional[float] = managed_spell_timeout_seconds
self._managed_spell_reset_methods: tuple[str, ...] = managed_spell_reset_methods
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 of the target's code (SpellCodeDigest)
self.spellframe: Any | None = spellframe
# Frame kind recorded by Bind (none / category / contract) and the
# Protocol this spell was structurally checked against. Identity data
# like `spellframe`: Phase 3 reads the kind instead of inferring a
# meaning from the frame's name, and both survive cleanup.
self.spellframe_kind: SpellframeKind = spellframe_kind
self.implemented_protocols: tuple[type, ...] = implemented_protocols
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.
# `dependency_graph` is a RETIRED tombstone (2026-09-26): phase 3 no
# longer builds a per-spell dependency graph object, so this is always
# None. The ordered local frame lives on the compiler artifact
# (`resolution_frame.ordered_node_ids`), the direct dependency ids in
# `dependencies`, and the per-socket targets in the SpellSystemStates
# local topology. The attribute is kept so readers of the old shape do
# not fail on lookup; it is deleted with the other slots at cleanup.
self.dependency_graph: Optional[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)
# Cause of the last failed context build or rebuild window. Melds that
# waited on that build report it; a new rebuild window clears it.
self._creation_context_failure: Optional[BaseException] = None
# Borrowed spell-index CreationGate, set only under dynamic ownership.
# A dynamic meld holds one ticket on it from before it reads the
# context until its executor returns; a rebuild window freezes and
# drains it before phases replace the plan. The frame's
# CreationGateController owns the gate's lifecycle.
self._creation_gate: Optional[CreationGate] = None
# 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)
# How this version is bound, as a value record with its own SHA256 (S2 of
# EPIC-2026-10-18-spell_identity_is_the_code): built last, from the arguments above, so a
# bound, staged, restored or grafted Spell carries one. Kept after cleanup, like `spell_id`.
self.binding_manifest: BindingManifest = BindingManifest.build(
key=self._key,
spellframe=self.spellframe,
spellframe_kind=self.spellframe_kind,
binding_name=self.binding_name,
existence=self.existence,
disposal_method_names=self.disposal_method_names,
resolvable=resolvable,
managed_spell_mode=managed_spell_mode,
managed_spell_limit=managed_spell_limit,
managed_spell_timeout_seconds=managed_spell_timeout_seconds,
managed_spell_reset_methods=managed_spell_reset_methods,
)
#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.
- A direct call on a live spell - one its Spellbook is not driving
(`_spellbook_cleanup` unset) - removes the spell through
`Spellbook.cleanup_and_remove_spell`, which takes it off every
registry and then calls back here for the local teardown. That
removal runs outside `_lock`, under this spell's `_removal_lock`
(created by the first direct call), so a second direct call waits
for it and then returns.
- 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. `_lock` is kept,
not deleted with the other fields, so a lock-guarded reader such as
`_spell_index_unless_cleaned` can still take it on a cleaned spell.
`_removal_lock` is deleted with the other fields; a direct call
waiting on it holds its own reference.
- Deletes the disposal-name reference without clearing its list;
creation entries may still retain that established metadata.
- Keeps the identity data readable: `spell_id`, the address (`key`),
the spellframe and its kind, and `binding_manifest` / `binding_hash`.
Threading:
The Book removal takes the Spellbook lock, frame locks and the Nexus
record, while Book teardown (`Spellbook._cleanup_spells`, also
reached from root and frame teardown) and conjure's ownership
stamping (`define_conduit_into_spells`) take this spell's `_lock`
under the Spellbook lock. Holding `_lock` across the removal hung both
threads when they met (before 0.2.8233); now `_lock` covers only the
decision (`_direct_removal_route`) and the local teardown. Nothing
else in Melder takes `_removal_lock`, so it closes no lock-order
cycle; a caller holding this spell's lock (`with spell:`) or another
Melder lock around a direct call brings the old order back. A direct
call racing its Book's teardown returns when the teardown cleaned the
spell first, or raises the Book's cleaned RuntimeError; it does not
hang. (direct_spell_cleanup_lock_order, 0.2.8233)
Runtime resolution and instance lifecycle remain owned by the Resolution
/ Meld layer, not by this class.
Returns:
None.
Raises:
RuntimeError: On a direct call, from
`Spellbook.cleanup_and_remove_spell`: the spell is not its Book's
current active version (a parked version, for one); it is the
active version of an index that also holds parked versions
(nothing changes: notch the index to one of them, dispose them
with `cleanup_spell`, or move them out with
`remove_from_spell_index` first); or the Book has been cleaned
and did not clean this spell.
"""
if self._cleaned:
return
direct_removal = self._direct_removal_route()
if direct_removal is not None:
# A direct call on a live spell: the Book takes it off its registries and calls back here with
# `_spellbook_cleanup` set. Run that outside `_lock` - Book and frame teardown and conjure take `_lock`
# under the Book lock - and serialize it per spell on the removal lock
# (direct_spell_cleanup_lock_order, 0.2.8233).
spellbook, removal_lock = direct_removal
with removal_lock:
if not self._cleaned:
spellbook.cleanup_and_remove_spell(self)
return
with self._lock:
if self._cleaned:
return
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._removal_lock
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._resolvable
del self._managed_spell_mode
del self._managed_spell_limit
del self._managed_spell_timeout_seconds
del self._managed_spell_reset_methods
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_failure
del self._creation_context_switch
del self._creation_gate
del self._caching_enabled
del self._compiler_artifact
del self.spell_index
def _direct_removal_route(self) -> Optional[tuple[Spellbook, RLock]]:
"""
Internal
Decide under `_lock` whether `cleanup()` must remove this spell through
its Spellbook, and hand back what that removal runs with.
Purpose:
Let `cleanup()` take its decision under the spell lock and run the
Book removal after leaving it, so the spell lock is never held while
the Spellbook, frame and Nexus locks are taken
(direct_spell_cleanup_lock_order, 0.2.8233).
Contract:
- Returns None for a cleaned spell, and for a cleanup its Spellbook
drives (`_spellbook_cleanup` set: Book teardown,
`cleanup_and_remove_spell`, the parked disposal, Bind's retirement
of an unpublished spell); `cleanup()` then runs the local teardown.
- Otherwise returns the owning Spellbook and this spell's removal
lock, creating the lock into `_removal_lock` on first use, so every
direct cleanup of this spell meets the same lock.
Threading:
Reads and creates under `_lock` - the lock the teardown deletes the
fields under - and calls nothing while holding it. The returned lock
is taken by `cleanup()` alone, after this returns.
Returns:
Optional[tuple[Spellbook, RLock]]:
The Book and the removal lock for a direct cleanup, or None.
"""
with self._lock:
if self._cleaned or self._spellbook_cleanup:
return None
removal_lock = self._removal_lock
if removal_lock is None:
removal_lock = RLock()
self._removal_lock = removal_lock
return self._spellbook, removal_lock
#endregion Disposal
@property
def resolvable(self) -> bool:
"""
Return this bound version's immutable resolution capability.
Contract:
Default True is established at bind. The value is independent of index
selection, permissions, existence, and current compilation validity.
No live setter is provided; changing capability requires another binding.
Returns:
bool: Whether this version is declared available for runtime resolution.
Raises:
RuntimeError: If this Spell has been cleaned.
"""
self.check_cleaned()
return self._resolvable
@property
def managed_spell_mode(self) -> Optional[ManagedSpellMode]:
"""
Return the pool policy of this managed version.
Contract:
Set once at bind (default elastic for a managed binding) and part of the binding manifest,
not the spell_id. No live setter; changing it takes another binding.
Returns:
Optional[ManagedSpellMode]: The mode, or None when the spell is not
`Existence.managed_spell_quantity`.
Raises:
RuntimeError: If this Spell has been cleaned.
"""
self.check_cleaned()
return self._managed_spell_mode
@property
def managed_spell_limit(self) -> Optional[int]:
"""
Return the limit of this managed version.
Contract:
elastic: the most idle objects its pool keeps (default 256). limited_quantity and queue: the
most objects alive at once, idle plus out. Set once at bind and part of the binding
manifest, not the spell_id.
Returns:
Optional[int]: The limit, or None when the spell is not managed.
Raises:
RuntimeError: If this Spell has been cleaned.
"""
self.check_cleaned()
return self._managed_spell_limit
@property
def managed_spell_timeout_seconds(self) -> Optional[float]:
"""
Return how long a queue lease of this version waits for a release.
Contract:
Set once at bind for the queue mode and part of the binding manifest, not the spell_id;
always None for elastic and limited_quantity, which never wait.
Returns:
Optional[float]: Seconds, or None.
Raises:
RuntimeError: If this Spell has been cleaned.
"""
self.check_cleaned()
return self._managed_spell_timeout_seconds
@property
def managed_spell_reset_methods(self) -> tuple[str, ...]:
"""
Return the reset method names of this managed version.
Contract:
Zero-argument methods of the class, run in this order on every release before the object
is leased again. Set once at bind and part of the binding manifest, not the spell_id.
Returns:
tuple[str, ...]: The names; empty when none were named or the spell is not managed.
Raises:
RuntimeError: If this Spell has been cleaned.
"""
self.check_cleaned()
return self._managed_spell_reset_methods
@property
def binding_hash(self) -> str:
"""
Return the SHA256 of this version's binding manifest.
Contract:
How this version is bound - address, spellframe and kind, existence, disposal order,
resolvable and managed settings - hashed apart from its code (`spell_id`). Equal bind
arguments give equal hashes in every process; a change of any of them gives another.
Readable after cleanup, like `spell_id` (`binding_manifest` is a value record).
Returns:
str: 64 hex characters (`binding_manifest.binding_hash`).
"""
return self.binding_manifest.binding_hash
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 _spell_index_unless_cleaned(self) -> Optional[SpellIndex]:
"""
Internal
Return this spell's SpellIndex, or None once the spell has been cleaned.
Purpose:
Let a reader that already holds this Spell read its index without
racing `cleanup()`, which deletes `spell_index` - the parked branch
of `Spellbook._find_contracted_spell_by_id`, the explicit index
steps that turn a spell found by spell_id into the spell its index
has active (the ownership transfer's spell_id handles and the
contract dependency walks), and the root's teardown, which
unregisters each parked version's index from the frame
(`Spellbook._unregister_conduit_spells_from_aether`,
minted_index_registry, 0.2.8259). Until 0.2.8253 its first reader
was the parked branch of `Spellbook.find_spell_by_id`.
Contract:
- Returns the index the spell belongs to while the spell is live, and
None after `cleanup()` has run. Never raises for a cleaned spell,
unlike a direct `spell_index` read.
- Pure read: creates, registers, and mutates nothing.
Threading:
Takes `self._lock`, the lock `cleanup()` sets `_cleaned` and deletes
`spell_index` under, so the check and the read cannot interleave with
a concurrent cleanup; `cleanup()` keeps `_lock`, so the read works on
a cleaned spell too. Holds no other lock and calls nothing while it
holds this one. Index moves repoint `spell_index` under the Spellbook
lock, not this one; a read racing a move returns the old or the new
index.
Returns:
Optional[SpellIndex]:
The spell's index, or None when the spell is cleaned.
"""
with self._lock:
if self._cleaned:
return None
return self.spell_index
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`.
- Drops the borrowed spell-index gate reference with it (the
gate is resolved again when a factory is configured); the gate
itself is owned by the frame's CreationGateController.
"""
if self._creation_context_factory is not None:
try:
self._creation_context_factory.cleanup()
except Exception:
pass
self._creation_context_factory = None
self._creation_gate = 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.
- In dynamic mode, resolves (creating when absent) the stable
spell-index CreationGate and keeps it on `_creation_gate` so
meld doors can admit before reading the context; None in
automatic mode.
Args:
dynamic_environment:
True when the owning conduit runs in dynamic mode.
creation_gate_controller:
Frame-owned CreationGateController used by the factory for
spell-index 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,
)
self._creation_gate = (
self._creation_context_factory.resolve_spell_index_gate(self)
)
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.
- Delegates build/get policy to CreationContextFactory, whose cold
path elects one builder through `_creation_context_switch`.
- Returns a live CreationContext instance bound to this spell.
Threading:
The ready path is one lock-free state read and one context read.
It takes no lock: in dynamic mode the caller already holds a
spell-index ticket on `_creation_gate`, and every conduit-local
rebuild (structural, resolution and deferred phases) runs inside
a CreationContextRebuild window that freezes that gate and drains
its tickets before phases replace the plan or reset this context,
so an admitted caller never sees the rebuild gap and never holds a
context that is cleaned under it (2026-09-26). Resets outside
those windows (ownership restamps, notch, teardown, transfer) are
not drained.
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 saving 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, which
writes only when cache was staged since its last save.
- Requires the spell to still have a live owning Spellbook.
- A save the operating system refuses (a read-only cache folder,
a full disk, or on Windows a bundle another handle holds open)
does not raise: the Spellbook's cache utility logs it, the
staged cache stays in memory for the next save, and this call
returns False.
Returns:
bool:
True when the Spellbook cache file was written; False when
caching is off, nothing was staged since the last save, or the
save was refused.
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. control-plane invalidation of its SpellIndex
- mark its SpellIndex 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 its SpellIndex's
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.
- Give an `Existence.managed_spell_quantity` version its pool in the owner's store
(`creations.ensure_managed_quantity(self)`, idempotent; 2026-10-15): every stamp - conjure, a
post-conjure bind or bind_inactive, the transfer flip - ends with the version's pool in the store it
was stamped with. No pool is composed when the stamp carries no store.
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
# Spell lock, then the store's leaf lock: the established build-lock-first order.
if self.existence is Existence.managed_spell_quantity and creations is not None:
creations.ensure_managed_quantity(self)
def _add_build_details(
self,
dependencies: list[str],
) -> None:
"""
Internal
Attach the static build-time dependency ids to this spell.
This is invoked by compiler phase 3 after it has resolved the spell's
constructor sockets into concrete dependency spell ids (and, later, by
the structural-snapshot hydrate that replays those ids from rows).
The per-spell dependency graph object that used to ride beside the ids
was retired on 2026-09-26; `dependency_graph` stays `None`.
Contract:
- Replaces the current `dependencies` reference with the given list
(the caller owns de-duplication and ordering).
- Invalidates any existing spell-owned CreationContext so the
runtime shape is rebuilt against the updated spell structure.
- Runs under the spell lock.
Args:
dependencies:
A list of spell_ids (SHA256 code digests) that this spell depends on.
Raises:
ValueError:
If `dependencies` is None.
"""
if dependencies is None:
raise ValueError("Dependencies cannot be None.")
with self._lock:
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 SpellIndex.
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 state of its SpellIndex.
- 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 SpellIndex is
not currently tracked.
Returns:
Optional[SpellSystemState]:
The state object for this spell's SpellIndex, 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