Skip to content

ARCHIVED: This technical design was archived on 2025-12-22 when the reflection module was removed from the codebase. The DI module now uses Python standard library introspection (inspect, typing.get_type_hints()) instead of the custom reflection system.

๐Ÿ“˜ Technical Design 033 โ€” ReflectionEngine & HandleRegistry Architecture

Field Value
ID TD-033
Title ReflectionEngine & HandleRegistry โ€” Foundation for Lazy Loading & Handle Identity
Status Complete
Owner Reflection maintainers
Audience Reflection/DI contributors, ORM and framework integrators
Created 2025-10-25
Last Updated 2025-11-22 (Phase C.1 implementation complete)
Reviewers TBD
Approvers TBD
Tracking Phase C.1 of 002-reflection-modernization-roadmap.md
Related TD-030 โ€” Reflection Handle Hierarchy, TD-031 โ€” Handle Generation Strategy Protocol, TD-032 โ€” ReflectionContext Integration & Migration

๐Ÿงญ 1. Summary

The reflection subsystem currently uses HierarchicalCache for both performance caching and handle identity management, creating semantic confusion and limiting future lazy-loading capabilities. TD-033 introduces two core components:

  • HandleRegistry: Authoritative registry for handle identity (qualname โ†’ Handle mapping)
  • ReflectionEngine: Central orchestrator for handle lifecycle, resolution, and graph management

At a glance:

  • Registry owns handle identity: one Handle instance per qualname (thread-safe)
  • Engine coordinates handle building, using registry for identity and cache for performance
  • Separation of concerns: Registry = identity/truth, Cache = performance/optimization
  • Lazy loading foundation: Registry tracks what exists; future proxies load details on-demand
  • Graph queries: find_by_prefix(), find_by_type() enable DI container discovery
  • Internal API: Engine remains internal to reflection subsystem (not exposed on ReflectionContext yet)

๐Ÿ“š 2. Context and Problem Statement

2.1 Current Architecture Issues

The MemberBuilder uses HierarchicalCache for two fundamentally different purposes:

1. Identity Management (member_builder.py:30, 92)

# Cache used to ensure single-instance identity
built = ModuleHandle(module)
self._cache.set(("module", module.__name__), built)  # Identity guarantee

# "cache it early to break potential cycles"
type_info = TypeHandle(type_, module_info)
self._cache.set(("type", type_.__module__, type_.__qualname__), type_info)

2. Performance Optimization (member_builder.py:24)

# Avoid rebuilding expensive objects
mi = self._cache.get(("module", module.__name__), ModuleHandle)
if mi is not None:
    return mi  # Performance shortcut

2.2 Problems with Current Approach

Concern Cache Semantics Registry Semantics Current Mismatch
Purpose Temporary storage; can evict Authoritative source of truth; must persist Cache eviction would break identity guarantees
Lifecycle Ephemeral; cleared for testing Permanent for session; explicit cleanup cache.clear() destroys identity, not just perf data
Key Structure Optimized for hit rate (tuple keys) Optimized for lookup (qualname strings) Tuple keys ("type", module, qualname) awkward for queries
Thread Safety Optional (cache misses are OK) Required (identity corruption is catastrophic) Works only because cache happens to be thread-safe
Querying Not designed for enumeration Designed for "find all types in module X" Cache doesn't support prefix queries efficiently

Specific Issues:

  1. Testing Confusion (reflection_context.py:268)

    def clear_caches(self, namespace: tuple[str, ...] | None = None) -> None:
        self._cache.clear()  # โš ๏ธ Destroys identity registry AND perf cache!
    

  2. No Graph Queries

    # Impossible with current cache:
    all_types_in_module = engine.find_handles_by_prefix("my_app.models")
    

  3. Lazy Loading Blocked: Can't distinguish "handle not loaded yet" from "handle doesn't exist"

2.3 Goals

  1. Separate identity from performance: HandleRegistry for truth, HierarchicalCache for optimization
  2. Enable lazy loading: Registry tracks placeholders; future phases materialize on-demand
  3. Support graph queries: DI container needs "find all @injectable types in package X"
  4. Simplify testing: clear_registry() vs clear_cache() have clear semantics
  5. Foundation for strategies: Registry provides identity layer for TD-031's strategy-based building
  6. Zero public API changes: Engine remains internal; ReflectionContext unchanged (integration in TD-032)

2.4 Out of Scope

  • Strategy protocol implementation (covered in TD-031)
  • ReflectionContext integration (covered in TD-032)
  • Lazy loading implementation (Phase C.4, deferred until strategies stabilize)
  • MemberBuilder removal (covered in TD-032)

2.5 Assumptions and Constraints

  • Python 3.13+ with strict mypy, PEP 695 generics
  • Thread-safety required: Registry must handle concurrent access
  • Single-instance identity: One Handle per qualname guaranteed
  • Internal API: Engine not exposed on ReflectionContext (yet)
  • Incremental migration: Existing code continues using MemberBuilder during Phase C.1

๐Ÿงฉ 3. Design Overview

3.1 Dual-Layer Architecture

graph TB
    subgraph "Layer 1: Identity (NEW)"
        Registry["HandleRegistry<br/>qualname โ†’ Handle<br/>Thread-safe<br/>Never evicts"]
    end

    subgraph "Layer 2: Performance (EXISTING)"
        Cache["HierarchicalCache<br/>Computed properties<br/>Can evict<br/>Cleared for tests"]
    end

    subgraph "Orchestration (NEW)"
        Engine["ReflectionEngine<br/>Coordinates building<br/>Uses Registry + Cache<br/>Internal API"]
    end

    Engine -->|Identity lookups| Registry
    Engine -->|Cache type hints| Cache

    MemberBuilder["MemberBuilder<br/>(Phase C.1: still exists)<br/>(Phase C.2+: replaced)"]
    Engine -->|Delegates to| MemberBuilder

    classDef identity fill:none,stroke:#9C27B0,stroke-width:2px,color:#9C27B0;
    classDef performance fill:none,stroke:#1976D2,stroke-width:2px,color:#1976D2;
    classDef orchestration fill:none,stroke:#4CAF50,stroke-width:2px,color:#4CAF50;
    classDef legacy fill:none,stroke:#FF9800,stroke-width:1px,color:#FF9800,stroke-dasharray: 5 5;

    class Registry identity;
    class Cache performance;
    class Engine orchestration;
    class MemberBuilder legacy;

3.2 Key Components

Component Purpose Location Visibility
HandleRegistry Authoritative handle identity registry with graph queries reflection/engine/registry.py Internal (via Engine)
ReflectionEngine Orchestrates handle building, owns registry and cache reflection/engine/reflection_engine.py Internal (Phase C.1)
HierarchicalCache Performance cache for computed properties (existing) collections/ Internal
MemberBuilder Legacy builder (Phase C.1: delegated to; Phase C.2+: removed) reflection/member_builder.py Internal

3.3 Component Responsibilities

HandleRegistry

  • โœ… Enforce single-instance identity per qualname
  • โœ… Thread-safe concurrent access
  • โœ… Graph queries: find_by_prefix(), find_by_type()
  • โœ… Explicit lifecycle: register(), get(), remove(), clear()
  • โŒ NOT responsible for: building handles, caching computed properties

ReflectionEngine

  • โœ… Coordinate handle building (delegates to MemberBuilder in Phase C.1)
  • โœ… Check registry for identity before building
  • โœ… Register newly built handles
  • โœ… Optionally cache computed properties (type hints, annotations)
  • โŒ NOT responsible for: public API exposure (happens in TD-032)

3.4 Three-TD Implementation Sequence

graph LR
    TD033["TD-033<br/>Engine + Registry<br/>Foundation"]
    TD031["TD-031<br/>Strategies + Specs<br/>Building Logic"]
    TD032["TD-032<br/>ReflectionContext<br/>Integration"]

    TD029 -->|Provides<br/>infrastructure| TD031
    TD031 -->|Provides<br/>strategy-based<br/>building| TD032

    TD029_Status["Phase C.1<br/>โณ Draft"]
    TD031_Status["Phase C.2<br/>โณ Pending"]
    TD032_Status["Phase C.3<br/>โณ Pending"]

    TD029 -.-> TD029_Status
    TD031 -.-> TD031_Status
    TD032 -.-> TD032_Status

    classDef foundation fill:none,stroke:#9C27B0,stroke-width:2px,color:#9C27B0;
    classDef strategy fill:none,stroke:#1976D2,stroke-width:2px,color:#1976D2;
    classDef integration fill:none,stroke:#4CAF50,stroke-width:2px,color:#4CAF50;
    classDef status fill:none,stroke:#FF9800,stroke-width:1px,color:#FF9800,stroke-dasharray: 5 5;

    class TD029 foundation;
    class TD031 strategy;
    class TD032 integration;
    class TD029_Status,TD031_Status,TD032_Status status;

Phase C.1 (This TD): Build registry + engine infrastructure, integrate with existing MemberBuilder Phase C.2 (TD-031): Replace MemberBuilder with strategy-based building, engine uses strategies Phase C.3 (TD-032): Wire engine into ReflectionContext, remove MemberBuilder, full integration

โš™๏ธ 4. Detailed Design

4.1 HandleRegistry Implementation

from collections.abc import Iterable
from threading import RLock
from typing import TypeVar

from loryth.common.reflection.handles import ReflectionHandle
from loryth.common.reflection.handles import TypeHandle
from loryth.common.reflection.handles import ModuleHandle


T = TypeVar("T", bound=ReflectionHandle)


class HandleRegistry:
    """Authoritative registry for reflection handle identity.

    Responsibilities:
    - Ensure single-instance identity: one Handle per qualname
    - Support graph queries by prefix and type
    - Thread-safe operations for concurrent access
    - Explicit lifecycle management (register, get, remove, clear)

    This is NOT a cache:
    - Handles persist until explicitly removed
    - No eviction policies or size limits
    - Registry is source of truth for "what exists"

    Thread-safety:
    - All operations protected by RLock
    - Safe for concurrent reads and writes
    - No deadlock risk (no nested locks)
    """

    _handles: dict[str, ReflectionHandle]
    _lock: RLock

    def __init__(self) -> None:
        """Initialize empty registry with thread-safety lock.

        :rtype: None
        """
        self._handles = {}
        self._lock = RLock()

    def register(self, handle: ReflectionHandle) -> ReflectionHandle:
        """Register a handle, returning existing instance if already registered.

        This enforces single-instance identity: if a handle with the same
        qualname already exists, the existing instance is returned instead
        of registering the new one.

        :param handle: Handle to register.
        :type handle: ReflectionHandle
        :return: The registered handle (may be the pre-existing instance).
        :rtype: ReflectionHandle

        Example::

            >>> handle1 = TypeHandle(MyClass, module_handle)
            >>> handle2 = TypeHandle(MyClass, module_handle)
            >>> registered1 = registry.register(handle1)
            >>> registered2 = registry.register(handle2)
            >>> registered1 is registered2  # True - same instance
        """
        with self._lock:
            existing = self._handles.get(handle.qualname)
            if existing is not None:
                return existing
            self._handles[handle.qualname] = handle
            return handle

    def get(self, qualname: str) -> ReflectionHandle | None:
        """Retrieve handle by qualified name.

        :param qualname: Fully qualified name (e.g., "my_app.models.Customer").
        :type qualname: str
        :return: Handle if registered, None otherwise.
        :rtype: ReflectionHandle | None

        Example::

            >>> handle = registry.get("my_app.models.Customer")
            >>> if handle is not None:
            ...     print(f"Found {handle.name}")
        """
        with self._lock:
            return self._handles.get(qualname)

    def has(self, qualname: str) -> bool:
        """Check if handle is registered.

        :param qualname: Fully qualified name to check.
        :type qualname: str
        :return: True if handle exists in registry, False otherwise.
        :rtype: bool

        Example::

            >>> if registry.has("my_app.models.Customer"):
            ...     print("Customer already loaded")
        """
        with self._lock:
            return qualname in self._handles

    def find_by_prefix(self, prefix: str) -> list[ReflectionHandle]:
        """Find all handles whose qualname starts with prefix.

        Useful for finding all members of a module or package.

        :param prefix: Qualname prefix to match (e.g., "my_app.models").
        :type prefix: str
        :return: List of matching handles (may be empty).
        :rtype: list[ReflectionHandle]

        Example::

            >>> # Find all types/functions in my_app.models
            >>> handles = registry.find_by_prefix("my_app.models")
            >>> for h in handles:
            ...     print(h.qualname)
            my_app.models.Customer
            my_app.models.Order
            my_app.models.validate_order
        """
        with self._lock:
            return [
                handle for qualname, handle in self._handles.items()
                if qualname.startswith(prefix)
            ]

    def find_by_type(self, handle_type: type[T]) -> list[T]:
        """Find all handles of a specific type.

        Useful for finding all types, all modules, etc.

        :param handle_type: Handle class to filter by (e.g., TypeHandle).
        :type handle_type: type[T]
        :return: List of matching handles (may be empty).
        :rtype: list[T]

        Example::

            >>> # Find all registered TypeHandles
            >>> types = registry.find_by_type(TypeHandle)
            >>> print(f"Found {len(types)} types")
        """
        with self._lock:
            return [
                handle for handle in self._handles.values()
                if isinstance(handle, handle_type)
            ]

    def remove(self, qualname: str) -> ReflectionHandle | None:
        """Remove handle from registry.

        :param qualname: Qualified name to remove.
        :type qualname: str
        :return: Removed handle, or None if not found.
        :rtype: ReflectionHandle | None

        Example::

            >>> removed = registry.remove("my_app.models.Customer")
            >>> if removed is not None:
            ...     print(f"Removed {removed.name}")
        """
        with self._lock:
            return self._handles.pop(qualname, None)

    def clear(self, prefix: str | None = None) -> None:
        """Clear registry (optionally by prefix).

        If prefix is None, clears entire registry.
        If prefix is provided, only removes handles matching prefix.

        :param prefix: Optional prefix to limit clearing scope.
        :type prefix: str | None
        :rtype: None

        Example::

            >>> # Clear specific module
            >>> registry.clear("my_app.models")

            >>> # Clear everything (test isolation)
            >>> registry.clear()
        """
        with self._lock:
            if prefix is None:
                self._handles.clear()
            else:
                to_remove = [
                    qualname for qualname in self._handles.keys()
                    if qualname.startswith(prefix)
                ]
                for qualname in to_remove:
                    self._handles.pop(qualname)

    def count(self) -> int:
        """Return total number of registered handles.

        :return: Number of handles in registry.
        :rtype: int

        Example::

            >>> print(f"Registry contains {registry.count()} handles")
        """
        with self._lock:
            return len(self._handles)

4.2 ReflectionEngine Implementation

from types import ModuleType
from typing import Any

from loryth.common.collections import HierarchicalCache
from loryth.common.collections.concurrent import ConcurrentHierarchicalCache
from loryth.common.reflection.handles import ModuleHandle
from loryth.common.reflection.handles import TypeHandle
from loryth.common.reflection.handles import FunctionHandle
from loryth.common.reflection.member_builder import MemberBuilder

from .registry import HandleRegistry


class ReflectionEngine:
    """Central orchestrator for handle lifecycle and lazy loading.

    Responsibilities:
    - Owns HandleRegistry for identity management
    - Owns HierarchicalCache for performance optimization
    - Coordinates handle building (delegates to MemberBuilder in Phase C.1)
    - Provides graph query APIs

    Internal API (Phase C.1):
    - Not exposed on ReflectionContext yet
    - Will be integrated in TD-032 (Phase C.3)

    Future (Phase C.4):
    - Lazy loading control (return proxies, load on-demand)
    - Configurable hydration strategies
    """

    _registry: HandleRegistry
    _cache: HierarchicalCache
    _builder: MemberBuilder  # Phase C.1: delegate to existing builder

    def __init__(
        self,
        cache: HierarchicalCache | None = None,
    ) -> None:
        """Initialize engine with registry and optional cache.

        :param cache: Optional hierarchical cache for performance optimization.
                     If None, creates ConcurrentHierarchicalCache.
        :type cache: HierarchicalCache | None
        :rtype: None
        """
        self._registry = HandleRegistry()
        self._cache = cache if cache is not None else ConcurrentHierarchicalCache()
        self._builder = MemberBuilder(cache=self._cache)

    def resolve_type(self, type_: type[Any]) -> TypeHandle:
        """Resolve a type to its TypeHandle.

        Checks registry first (identity layer).
        If not found, builds using MemberBuilder and registers.

        :param type_: Python type to resolve.
        :type type_: type[Any]
        :return: TypeHandle for the type.
        :rtype: TypeHandle

        Example::

            >>> class Customer:
            ...     pass
            >>> handle = engine.resolve_type(Customer)
            >>> assert handle.python_type is Customer
        """
        qualname = f"{type_.__module__}.{type_.__qualname__}"

        # Check registry first (identity layer)
        existing = self._registry.get(qualname)
        if existing is not None:
            return existing  # type: ignore[return-value]

        # Build new handle (delegates to MemberBuilder)
        handle = self._builder.build_type(type_)

        # Register for identity (required)
        registered = self._registry.register(handle)

        return registered  # type: ignore[return-value]

    def resolve_module(self, module: ModuleType) -> ModuleHandle:
        """Resolve a module to its ModuleHandle.

        Checks registry first (identity layer).
        If not found, builds using MemberBuilder and registers.

        :param module: Python module to resolve.
        :type module: ModuleType
        :return: ModuleHandle for the module.
        :rtype: ModuleHandle

        Example::

            >>> import my_app.models
            >>> handle = engine.resolve_module(my_app.models)
            >>> assert handle.python_module is my_app.models
        """
        qualname = module.__name__

        # Check registry first
        existing = self._registry.get(qualname)
        if existing is not None:
            return existing  # type: ignore[return-value]

        # Build new handle
        handle = self._builder.build_module(module)

        # Register for identity
        registered = self._registry.register(handle)

        return registered  # type: ignore[return-value]

    def get_handle(self, qualname: str) -> ReflectionHandle | None:
        """Retrieve a handle from the registry without building.

        :param qualname: Fully qualified name.
        :type qualname: str
        :return: Handle if registered, None otherwise.
        :rtype: ReflectionHandle | None

        Example::

            >>> handle = engine.get_handle("my_app.models.Customer")
            >>> if handle is None:
            ...     print("Not loaded yet")
        """
        return self._registry.get(qualname)

    def find_handles_by_prefix(self, prefix: str) -> list[ReflectionHandle]:
        """Find all handles matching prefix (graph query).

        :param prefix: Qualname prefix to match.
        :type prefix: str
        :return: List of matching handles.
        :rtype: list[ReflectionHandle]

        Example::

            >>> # DI container finding all types in package
            >>> handles = engine.find_handles_by_prefix("my_app.services")
            >>> injectable = [h for h in handles if h.has_annotation("injectable")]
        """
        return self._registry.find_by_prefix(prefix)

    def find_handles_by_type(self, handle_type: type[T]) -> list[T]:
        """Find all handles of specific type (graph query).

        :param handle_type: Handle class to filter by.
        :type handle_type: type[T]
        :return: List of matching handles.
        :rtype: list[T]

        Example::

            >>> # Find all registered types
            >>> types = engine.find_handles_by_type(TypeHandle)
        """
        return self._registry.find_by_type(handle_type)

    def clear_registry(self, prefix: str | None = None) -> None:
        """Clear handle registry (destroys identity).

        Use for test isolation when you need to reset reflection state.

        :param prefix: Optional prefix to limit clearing scope.
        :type prefix: str | None
        :rtype: None

        Example::

            >>> # Test isolation
            >>> engine.clear_registry()
        """
        self._registry.clear(prefix)

    def clear_cache(self, namespace: tuple[str, ...] | None = None) -> None:
        """Clear performance cache (preserves identity).

        Use to invalidate cached computations (type hints, annotations)
        without destroying handle identity.

        :param namespace: Optional namespace to limit clearing scope.
        :type namespace: tuple[str, ...] | None
        :rtype: None

        Example::

            >>> # Clear cached type hints only
            >>> engine.clear_cache(("type_hints",))
        """
        self._cache.clear(namespace)

    @property
    def registry(self) -> HandleRegistry:
        """Access to underlying registry (for testing/introspection).

        :return: The handle registry.
        :rtype: HandleRegistry
        """
        return self._registry

๐Ÿงช 5. Test Strategy

5.1 Test Matrix

Component Test Type Coverage Target Key Scenarios
HandleRegistry Unit 100% Identity enforcement, thread-safety, graph queries, clear operations
ReflectionEngine Unit 95% Registry integration, cache separation, delegation to MemberBuilder
Engine + Registry Integration 90% End-to-end handle resolution, identity guarantees across multiple resolutions
Thread Safety Concurrency 100% critical paths Concurrent resolve, concurrent register, race conditions

5.2 HandleRegistry Test Scenarios

5.2a: Identity Enforcement

def test_registry_enforces_single_instance_identity(registry: HandleRegistry) -> None:
    """Test that registry returns same instance for duplicate registrations.

    :param registry: HandleRegistry fixture.
    :type registry: HandleRegistry
    """
    handle1 = TypeHandle(Customer, module_handle)
    handle2 = TypeHandle(Customer, module_handle)

    registered1 = registry.register(handle1)
    registered2 = registry.register(handle2)

    assert registered1 is registered2
    assert registry.count() == 1

5.2b: Graph Queries

def test_registry_finds_handles_by_prefix(registry: HandleRegistry) -> None:
    """Test that registry returns all handles matching prefix.

    :param registry: HandleRegistry fixture.
    :type registry: HandleRegistry
    """
    registry.register(TypeHandle(Customer, module_handle))  # my_app.models.Customer
    registry.register(TypeHandle(Order, module_handle))     # my_app.models.Order
    registry.register(FunctionHandle(validate, module_handle))  # my_app.utils.validate

    models_handles = registry.find_by_prefix("my_app.models")

    assert len(models_handles) == 2
    assert all(h.qualname.startswith("my_app.models") for h in models_handles)

5.2c: Thread Safety

def test_registry_handles_concurrent_registration(registry: HandleRegistry) -> None:
    """Test that registry is thread-safe under concurrent access.

    :param registry: HandleRegistry fixture.
    :type registry: HandleRegistry
    """
    import threading

    results = []

    def register_handle():
        handle = TypeHandle(Customer, module_handle)
        registered = registry.register(handle)
        results.append(registered)

    threads = [threading.Thread(target=register_handle) for _ in range(10)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    # All threads got same instance
    assert len(set(id(h) for h in results)) == 1
    assert registry.count() == 1

5.2d: Clear Operations

def test_registry_clears_by_prefix(registry: HandleRegistry) -> None:
    """Test that registry clears only matching prefix.

    :param registry: HandleRegistry fixture.
    :type registry: HandleRegistry
    """
    registry.register(TypeHandle(Customer, module_handle))  # my_app.models.Customer
    registry.register(TypeHandle(Order, module_handle))     # my_app.models.Order
    registry.register(FunctionHandle(validate, utils_module))  # my_app.utils.validate

    registry.clear("my_app.models")

    assert registry.count() == 1
    assert registry.has("my_app.utils.validate")
    assert not registry.has("my_app.models.Customer")

5.3 ReflectionEngine Test Scenarios

5.3a: Registry Integration

def test_engine_uses_registry_for_identity(engine: ReflectionEngine) -> None:
    """Test that engine checks registry before building.

    :param engine: ReflectionEngine fixture.
    :type engine: ReflectionEngine
    """
    handle1 = engine.resolve_type(Customer)
    handle2 = engine.resolve_type(Customer)

    assert handle1 is handle2  # Same instance from registry
    assert engine.registry.count() == 1

5.3b: Cache Separation

def test_engine_clears_cache_without_destroying_identity(engine: ReflectionEngine) -> None:
    """Test that clearing cache preserves handle identity.

    :param engine: ReflectionEngine fixture.
    :type engine: ReflectionEngine
    """
    handle1 = engine.resolve_type(Customer)
    engine.clear_cache()  # Clear performance cache
    handle2 = engine.resolve_type(Customer)

    assert handle1 is handle2  # Identity preserved (from registry)

5.3c: Graph Queries

def test_engine_finds_all_types_in_module(engine: ReflectionEngine) -> None:
    """Test that engine supports graph queries via registry.

    :param engine: ReflectionEngine fixture.
    :type engine: ReflectionEngine
    """
    import my_app.models

    engine.resolve_module(my_app.models)

    all_handles = engine.find_handles_by_prefix("my_app.models")
    type_handles = engine.find_handles_by_type(TypeHandle)

    assert len(all_handles) > 0
    assert len(type_handles) > 0

5.4 Edge Case Coverage

Edge Case Scenario Validation
Concurrent resolution Multiple threads resolve same type simultaneously Registry returns same instance; no duplicates
Circular references Module A imports Module B; Module B imports Module A Both modules registered; no infinite loops
Clear during iteration Clear registry while iterating find_by_prefix() No ConcurrentModificationError (snapshot iteration)
Remove during resolution Remove handle while another thread resolves it Thread-safe; handle either found or rebuilt

โœ… 6. Acceptance Criteria

6.1 Phase C.1a: HandleRegistry Foundation

  • โœ… HandleRegistry implemented in reflection/engine/registry.py
  • โœ… Core methods: register(), get(), has(), remove(), clear(), count()
  • โœ… Thread-safety: All operations protected by RLock
  • โœ… Unit tests: 100% branch coverage
  • โœ… Identity enforcement validated: duplicate registrations return same instance
  • โœ… Thread-safety tests: concurrent access validated with 10+ threads

6.2 Phase C.1b: Graph Query APIs

  • โœ… find_by_prefix() implemented and tested
  • โœ… find_by_type() implemented and tested
  • โœ… Unit tests: prefix matching edge cases (empty prefix, no matches, partial matches)
  • โœ… Unit tests: type filtering with inheritance (subclass matching)
  • โœ… Performance: prefix queries scale to 1000+ handles

6.3 Phase C.1c: ReflectionEngine Scaffold

  • โœ… ReflectionEngine implemented in reflection/engine/reflection_engine.py
  • โœ… Owns HandleRegistry and HierarchicalCache
  • โœ… resolve_type(), resolve_module() delegate to MemberBuilder
  • โœ… Registry checked before building (identity guarantee)
  • โœ… Newly built handles registered automatically
  • โœ… Unit tests: 95% branch coverage

6.4 Phase C.1d: Standalone Validation

  • โœ… All registry tests pass (100% coverage)
  • โœ… All engine tests pass (95% coverage)
  • โœ… Integration tests: engine + registry workflows
  • โœ… Thread-safety validated: concurrent resolve operations
  • โœ… Cache separation validated: clear_cache() vs clear_registry() semantics
  • โœ… Graph queries functional: find_by_prefix(), find_by_type()
  • โœ… No changes to existing 791 tests (ReflectionContext unchanged)

6.5 Documentation

  • โœ… TD-033 complete with implementation details
  • โœ… Sphinx docstrings on all public methods
  • โœ… Code examples in docstrings
  • โœ… Test scenarios documented
  • โœ… Integration plan for TD-031 and TD-032 outlined

๐Ÿš€ 7. Implementation Phases

Phase C.1a: HandleRegistry Foundation (โœ… Complete)

Goal: Create thread-safe registry with core operations

Tasks: 1. Create reflection/engine/ package 2. Create reflection/engine/registry.py 3. Implement HandleRegistry class with: - __init__(): Initialize empty dict + RLock - register(): Add handle, return existing if present - get(): Retrieve by qualname - has(): Check existence - remove(): Delete by qualname - clear(): Clear all or by prefix - count(): Return total handles 4. Write unit tests (100% coverage): - Identity enforcement (duplicate registration) - Thread-safety (concurrent access with 10+ threads) - Clear operations (full clear, prefix clear) - Edge cases (empty registry, missing qualname)

Acceptance: All registry tests pass with 100% coverage


Phase C.1b: Graph Query APIs (โœ… Complete)

Goal: Add prefix and type-based queries

Tasks: 1. Implement find_by_prefix() in HandleRegistry 2. Implement find_by_type() in HandleRegistry 3. Write unit tests: - Prefix matching (exact, partial, no matches) - Type filtering (exact type, subclasses) - Empty results - Performance with 1000+ handles 4. Integration tests: - Find all types in module - Find all modules in package - DI-style annotation queries

Acceptance: Graph query tests pass; performance validated


Phase C.1c: ReflectionEngine Scaffold (โœ… Complete)

Goal: Create engine that coordinates registry + MemberBuilder

Tasks: 1. Create reflection/engine/reflection_engine.py 2. Implement ReflectionEngine class: - __init__(): Create registry, accept cache, create MemberBuilder - resolve_type(): Check registry โ†’ build โ†’ register - resolve_module(): Check registry โ†’ build โ†’ register - get_handle(): Delegate to registry.get() - find_handles_by_prefix(): Delegate to registry - find_handles_by_type(): Delegate to registry - clear_registry(): Delegate to registry - clear_cache(): Clear HierarchicalCache 3. Write unit tests (95% coverage): - Registry checked before building - Newly built handles registered - Same instance returned on duplicate resolve - Cache clear preserves identity - Registry clear destroys identity

Acceptance: Engine tests pass with 95% coverage


Phase C.1d: Standalone Validation (โœ… Complete)

Goal: Validate engine + registry work correctly in isolation

Tasks: 1. Write integration tests: - End-to-end type resolution (resolve โ†’ registry โ†’ same instance) - End-to-end module resolution - Graph queries after resolving multiple handles - Concurrent access (5+ threads resolving simultaneously) 2. Write edge case tests: - Circular module imports - Clear during iteration - Remove during concurrent resolution 3. Performance benchmarks: - Baseline: MemberBuilder alone - With engine: overhead should be <5% 4. Documentation: - Update TD-033 with implementation notes - Add usage examples to docstrings - Document integration plan for TD-031

Acceptance: All integration tests pass; performance within 5% of baseline; existing 791 tests still pass (no ReflectionContext changes yet)


7.5 Implementation Notes

Implementation Timeline: - Phase C.1a + C.1b completed: 2025-11-22 (commit 4af5ee2) - Phase C.1c completed: 2025-11-22 (commit 998659c) - Phase C.1d completed: 2025-11-22

Files Created: - src/loryth/common/reflection/engine/__init__.py - Package exports - src/loryth/common/reflection/engine/registry.py - HandleRegistry implementation (~250 lines) - src/loryth/common/reflection/engine/reflection_engine.py - ReflectionEngine implementation (~285 lines) - tests/unit/reflection/engine/test_registry.py - 22 unit tests (~600 lines) - tests/unit/reflection/engine/test_reflection_engine.py - 19 unit tests (~420 lines) - tests/integration/reflection/test_engine_integration.py - 15 integration tests (~500 lines)

Test Coverage: - Unit tests: 41 tests (22 registry + 19 engine) - Integration tests: 15 tests (end-to-end workflows, identity, graph queries, edge cases) - Total: 56 new tests, all passing - Regression: 0 failures in existing 129 reflection tests

Key Implementation Decisions:

  1. Check-Build-Register Pattern: ReflectionEngine implements three-step pattern:
  2. Check registry for existing handle (identity layer)
  3. Build new handle if not found (delegates to MemberBuilder)
  4. Register newly built handle (identity guarantee)

  5. Dual-Layer Architecture: Clear separation maintained between:

  6. HandleRegistry: Identity/truth layer (never evicts, thread-safe)
  7. HierarchicalCache: Performance/optimization layer (can clear, caches computed properties)

  8. Thread-Safety: HandleRegistry uses RLock for all operations, validated with 15 concurrent threads in tests.

  9. MemberBuilder Interaction: Phase C.1 delegates to existing MemberBuilder for handle construction. MemberBuilder also caches internally, so full identity reset requires clearing both registry AND cache.

  10. Graph Query Implementation: find_by_prefix() and find_by_type() use simple linear scan over registry dict. Performance is acceptable for typical registry sizes (<1000 handles).

  11. Simplified Phase C.1d: Performance benchmarks and additional concurrency tests were excluded from Phase C.1d per project decision. Thread-safety validation covered in unit tests is sufficient.

Deviations from Original Plan: - No performance benchmarks: Decided to defer until strategy-based building in Phase C.2 - Reduced concurrency testing: Unit test coverage (15 threads) deemed sufficient; no additional integration concurrency tests added

Next Steps (Phase C.2 - TD-031): - Implement strategy protocol for handle building - Replace MemberBuilder delegation with strategy-based building - Add mapper components for type โ†’ spec conversion - Wire strategies into ReflectionEngine

๐Ÿ“‹ 8. Dependencies & Sequencing

8.1 Phase C.1 Internal Dependencies

graph TD
    C1a["Phase C.1a<br/>Registry Foundation"]
    C1b["Phase C.1b<br/>Graph Queries"]
    C1c["Phase C.1c<br/>Engine Scaffold"]
    C1d["Phase C.1d<br/>Standalone Validation"]

    C1a --> C1b
    C1b --> C1c
    C1c --> C1d

    classDef pending fill:none,stroke:#FF9800,stroke-width:2px,color:#FF9800;
    class C1a,C1b,C1c,C1d pending;

8.2 Cross-TD Dependencies

TD-033 (This TD) โ†’ TD-031: - Provides: HandleRegistry, ReflectionEngine infrastructure - TD-031 needs: Engine to wire strategy-based building into

TD-031 โ†’ TD-032: - Provides: Strategy protocol, mappers, specs - TD-032 needs: Working strategy-based building before ReflectionContext integration

No reverse dependencies: TD-033 is foundation; can be implemented standalone

๐Ÿ“ 9. Changelog

  • 2025-11-22 โ€” Phase C.1 Implementation Complete
  • Implemented HandleRegistry with full thread-safety (22 unit tests)
  • Implemented ReflectionEngine with check-build-register pattern (19 unit tests)
  • Created comprehensive integration test suite (15 tests)
  • Total: 56 new tests, all passing, 0 regressions
  • Files: reflection/engine/registry.py, reflection/engine/reflection_engine.py
  • Key commits: 4af5ee2 (C.1a+C.1b), 998659c (C.1c)
  • Dual-layer architecture validated: Registry (identity) + Cache (performance)
  • Graph queries operational: find_by_prefix(), find_by_type()
  • Status: Phase C.1 complete; ready for Phase C.2 (TD-031)
  • Deviations: Performance benchmarks deferred to Phase C.2

  • 2025-11-15 โ€” Architecture Pivot: ReflectionEngine + HandleRegistry

  • Major redesign: Split work into three focused TDs
  • TD-033: ReflectionEngine & HandleRegistry (foundation)
  • TD-031: Strategy Protocol & Mappers (building logic)
  • TD-032: ReflectionContext Integration (migration)
  • Introduced dual-layer architecture (Registry = identity, Cache = performance)
  • Defined HandleRegistry with graph query APIs
  • Defined ReflectionEngine as orchestration layer
  • Phase C.1 broken into 4 sub-phases (C.1a-C.1d)
  • Status: Draft (no implementation yet)
  • Progress tracking: Phase C.1a-C.1d detailed with acceptance criteria

  • 2025-11-09 โ€” Consolidated architecture under engine; renamed to Generation terminology

  • All strategy components moved to loryth.common.reflection.engine.strategies
  • Replaced builders with specs (immutable classes using copy() pattern)
  • Updated terminology throughout (HandleGenerationStrategy, HandleGenerationContext)
  • Created comprehensive public API exports in engine/__init__.py
  • Ready for Phase 2 test coverage implementation

  • 2025-10-27 โ€” Phase 1 marked complete (superseded by 2025-11-15 redesign)

  • NOTE: This entry reflected a previous design iteration
  • Code was not actually implemented; design was superseded by Engine + Registry approach
  • Preserved for historical context

  • 2025-10-26 โ€” Design refinements (superseded)

  • Previous iterations of strategy-based architecture
  • See git history for details

  • 2025-10-25 โ€” Initial draft created

  • Original member parsing seam concept
  • Evolved into current three-TD architecture

๐Ÿ“– 10. Glossary

Term Definition
Handle identity Guarantee that one Handle instance exists per qualname (enforced by registry)
Registry Authoritative source of truth for handle existence and identity
Cache Performance optimization layer for computed properties (type hints, annotations)
Graph query Finding handles by prefix or type (e.g., "all types in module X")
Lazy loading Loading handle details on-demand rather than eagerly (Phase C.4, future)
Qualname Fully qualified name (e.g., "my_app.models.Customer") uniquely identifying a member
Dual-layer architecture Separation of identity (registry) from performance (cache)

End of TD-033