Skip to content

๐Ÿ“˜ Technical Design 013 โ€” Bootstrap Plan Building via Package Scanning

Field Value
ID TD-013
Title Bootstrap Plan Building via Package Scanning
Status Complete
Owner Contributors
Audience Contributors, maintainers, DI system users
Created 2025-10-02
Last Updated 2025-10-11
Reviewers TBD
Approvers TBD
Tracking Phase 5 of DI Roadmap
Related TD-020 (Bootstrap Runtime), TD-012 (Enhanced Annotations), TD-021 (Application Entry Point)
Dependencies TD-020 (BootstrapRuntime, BootstrapPlan), TD-012 (Annotation system), Reflection System

๐Ÿงญ 1. Summary

This design implements Phase 5 of the DI roadmap by providing automatic service discovery through package scanning and rule-based registration. The design introduces BootstrapPlanBuilder with a fluent API for scanning packages, applying registration rules, and building bootstrap plans compatible with TD-020's external bootstrap runtime.

At a glance:

  • BootstrapPlanBuilder with fluent API for composing scans and manual registrations
  • RegistrationRule protocol for extensible annotation-to-step translation
  • Built-in rules for @service, @component, @configuration, and @bean annotations
  • Integration with loryth.common.reflection scanner infrastructure (predicate support confirmed)
  • Replaces TD-012's AnnotationProcessor pattern with composable rules
  • Full compatibility with TD-020 BootstrapRuntime and idempotent bootstrap semantics

๐Ÿ“š 2. Context and Problem Statement

2.0 Motivation

Spring Boot's component scanning eliminates boilerplate service registration and enables "convention over configuration" patterns. With TD-020 externalizing bootstrap orchestration and TD-012 providing annotation infrastructure, the framework now needs automatic service discovery to complete the core feature set.

Current Limitations

Phase 1-4 provides registration, bootstrapping, injection, and declarative annotations, but service discovery remains manual:

# Must explicitly import and list all services
from myapp.services import UserService, EmailService
from myapp.repositories import UserRepository

# Manual registration via TD-012 AnnotationProcessor
processor = AnnotationProcessor(...)
processor.process([UserService, EmailService, UserRepository])

Additionally, TD-012's AnnotationProcessor has design limitations: - Directly mutates container, preventing plan inspection before bootstrap - Tightly couples annotation detection to container registration - Requires InjectorBridge, ClassAnnotationMetadata, and BeanMetadata helper classes - Not compatible with TD-020's plan-based bootstrap pattern

Key limitations: 1. No Package Discovery - No automatic discovery of services within packages 2. Manual Service Lists - All services must be explicitly imported and enumerated 3. Processor Pattern Limitations - Cannot inspect or modify registration plan before bootstrap 4. No TD-020 Integration - AnnotationProcessor predates external bootstrap runtime

2.1 Goals

  1. Package Scanning - Automatic discovery of classes within specified packages using reflection scanner
  2. Rule-Based Translation - Extensible RegistrationRule protocol for annotation-to-step conversion
  3. Fluent Plan Building - Composable API for scanning, manual registration, and plan generation
  4. TD-020 Integration - Generate BootstrapPlan compatible with BootstrapRuntime.bootstrap()
  5. Replace AnnotationProcessor - Eliminate TD-012 processor pattern in favor of composable rules
  6. Built-In Rules - Provide default rules for common annotations (@service, @component, @configuration, @bean)

2.2 Non-Goals (Out of Scope)

  • Application entry point orchestration (moved to TD-021)
  • @application decorator and application lifecycle hooks (moved to TD-021)
  • Classpath scanning beyond Python package system
  • Dynamic module loading or hot-swapping at runtime
  • Complex dependency graph analysis during scanning (handled by TD-020 runtime)
  • Configuration file integration (future TD)

2.3 Assumptions and Constraints

  • TD-020 BootstrapRuntime, BootstrapPlan, and RegistrationStep types are implemented
  • TD-012 annotation system (ServiceAnnotation, ComponentAnnotation, etc.) exists
  • loryth.common.reflection scanner provides scan_package() with predicate support (confirmed)
  • loryth.common.annotations.AnnotationContext provides find_annotation() and has_annotation() utilities
  • Scanning happens during plan building, not at runtime
  • Container idempotency (TD-020) handles step deduplication during bootstrap
  • Typical applications have <1000 modules and <5000 classes

2.4 Compatibility

  • TD-012's AnnotationProcessor, InjectorBridge, ClassAnnotationMetadata, and BeanMetadata are deprecated and replaced
  • Backward compatibility maintained through migration guide (section 8)
  • All existing Phase 1-4 container functionality remains unchanged

๐Ÿงฉ 3. Design Overview

The scanning system provides automatic service discovery through package traversal, rule-based registration step generation, and fluent plan building. The system works in three stages:

  1. Discovery - Reflection scanner traverses packages and applies predicates to filter classes
  2. Rule Application - Discovered classes are analyzed by registration rules that produce steps
  3. Plan Building - Registration steps are collected and packaged into BootstrapPlan

High-Level Architecture:

graph TB
    subgraph "Plan Building (TD-013)"
        Builder[BootstrapPlanBuilder]
        Scanner[Reflection Scanner]
        Rules[RegistrationRule<br/>Collection]
        Config[ScanningConfiguration]
    end

    subgraph "Bootstrap (TD-020)"
        Plan[BootstrapPlan]
        Runtime[BootstrapRuntime]
        Container[DiContainer]
    end

    Builder --> Scanner
    Builder --> Rules
    Config --> Builder
    Builder --> Plan
    Plan --> Runtime
    Runtime --> Container

    classDef td013 fill:none,stroke:#4caf50,color:#4caf50
    classDef td020 fill:none,stroke:#2196f3,color:#2196f3

    class Builder,Scanner,Rules,Config td013
    class Plan,Runtime,Container td020

Scanning and Rule Application Flow:

graph LR
    Start[Packages] --> Scan[Reflection<br/>Scanner]
    Scan --> Filter{Predicate<br/>Filter}
    Filter -->|Match| Classes[Discovered<br/>Classes]
    Filter -->|No Match| Skip[Skip]
    Classes --> RuleLoop[For Each Rule]
    RuleLoop --> Rule1[ServiceAnnotationRule]
    RuleLoop --> Rule2[ComponentAnnotationRule]
    RuleLoop --> Rule3[ConfigurationAnnotationRule]
    Rule1 --> Steps[Registration<br/>Steps]
    Rule2 --> Steps
    Rule3 --> Steps
    Steps --> Plan[BootstrapPlan]

    classDef scanNode fill:none,stroke:#2196f3,color:#2196f3
    classDef ruleNode fill:none,stroke:#ff9800,color:#ff9800
    classDef planNode fill:none,stroke:#4caf50,color:#4caf50

    class Scan,Filter scanNode
    class RuleLoop,Rule1,Rule2,Rule3 ruleNode
    class Steps,Plan planNode

3.1 Components

Component Purpose
BootstrapPlanBuilder Fluent API for scanning packages, adding classes manually, and building bootstrap plans
RegistrationRule (Protocol) Protocol for extensible rules that translate classes into registration steps
ServiceAnnotationRule Built-in rule handling @service annotation
ComponentAnnotationRule Built-in rule handling @component annotation
ConfigurationAnnotationRule Built-in rule handling @configuration classes and @bean methods
ScanningConfiguration Immutable configuration for package scanning (packages, filters, recursion)

3.2 Breaking Changes

This design deprecates and replaces TD-012's processor-based pattern with composable registration rules.

Component Before (TD-012) After (TD-013)
AnnotationProcessor Mutates container directly via process(classes) Removed - replaced by RegistrationRule + BootstrapPlanBuilder
InjectorBridge Bridges annotations to container injectors Removed - rules produce RegistrationStep objects directly
AnnotationRegistry Stores metadata for reuse across processor invocations Removed - discovery rules emit registration steps directly
ClassAnnotationMetadata Stores annotation metadata for processor Removed - rules inspect annotations directly via AnnotationContext
BeanMetadata Stores @bean method metadata Removed - ConfigurationAnnotationRule handles bean discovery
Service Registration processor.process(classes) mutates container builder.scan_packages(config).build() returns inspectable plan

3.3 Rollout Policy

TD-013 implementation follows a two-phase rollout:

  • Phase 5a-c: Implement new components (RegistrationRule, BootstrapPlanBuilder, built-in rules)
  • Phase 5d: Remove deprecated TD-012 components in a separate cleanup phase after Phase 5c completes

Component removal follows these steps: 1. Remove AnnotationProcessor, InjectorBridge, AnnotationRegistry, ClassAnnotationMetadata, BeanMetadata from public API 2. Update all tests using deprecated components to use new BootstrapPlanBuilder API 3. Delete implementation code for removed components 4. Update documentation to remove all references to deprecated APIs

No deprecation warnings needed - components are removed immediately after TD-013 implementation completes. Migration guide in section 8 provides conversion examples.

๐Ÿ“ 4. Implementation Details

4.1 RegistrationRule Protocol

The core abstraction for translating annotated classes into registration steps:

from typing import Protocol
from collections.abc import Sequence

from loryth.common.di.bootstrap import RegistrationStep


class RegistrationRule(Protocol):
    """Protocol for rules that translate annotated classes into registration steps.

    Each rule inspects a class and optionally returns registration steps if the
    class matches the rule's criteria (e.g., has specific annotations). Multiple
    rules can apply to the same class (union semantics).

    Rules should be stateless and idempotent - calling apply() multiple times
    with the same class should produce equivalent results.
    """

    def apply(self, cls: type) -> Sequence[RegistrationStep]:
        """Analyze a class and return registration steps if applicable.

        :param cls: The class to analyze
        :return: Sequence of RegistrationStep objects, or empty sequence if rule
                 doesn't apply to this class
        :raises Exception: Rules may raise exceptions on invalid configuration;
                          builder wraps these in BootstrapError
        """
        ...

4.2 Built-In Rules

ServiceAnnotationRule

from dataclasses import dataclass

from loryth.common.annotations import find_annotation
from loryth.common.di.annotations import ServiceAnnotation
from loryth.common.di.bootstrap import (
    RegistrationStep,
    SingletonRegistrationStep,
    TransientRegistrationStep,
)
from loryth.common.di.container import ServiceLifecycle


@dataclass(frozen=True)
class ServiceAnnotationRule:
    """Rule that handles @service annotations."""

    def apply(self, cls: type) -> Sequence[RegistrationStep]:
        """Return registration step if class has @service annotation.

        Respects the lifecycle specified in the annotation (singleton/transient).

        :param cls: The class to analyze
        :return: Sequence containing singleton or transient registration step,
                 or empty sequence if class lacks @service annotation
        """
        annotation = find_annotation(cls, ServiceAnnotation)
        if annotation is None:
            return []

        if annotation.lifecycle == ServiceLifecycle.SINGLETON:
            return [SingletonRegistrationStep(cls, cls)]
        elif annotation.lifecycle == ServiceLifecycle.TRANSIENT:
            return [TransientRegistrationStep(cls, cls)]

        # Default to singleton if lifecycle not specified
        return [SingletonRegistrationStep(cls, cls)]

ComponentAnnotationRule

from loryth.common.di.annotations import ComponentAnnotation


@dataclass(frozen=True)
class ComponentAnnotationRule:
    """Rule that handles @component annotations."""

    def apply(self, cls: type) -> Sequence[RegistrationStep]:
        """Return singleton registration step if class has @component annotation.

        @component always produces singleton registrations.

        :param cls: The class to analyze
        :return: Sequence containing singleton registration step,
                 or empty sequence if class lacks @component annotation
        """
        annotation = find_annotation(cls, ComponentAnnotation)
        if annotation is None:
            return []

        return [SingletonRegistrationStep(cls, cls)]

ConfigurationAnnotationRule

import inspect
import typing
from typing import Any, Callable

from loryth.common.di.annotations import ConfigurationAnnotation, BeanAnnotation
from loryth.common.di.bootstrap import FactoryRegistrationStep


@dataclass(frozen=True)
class ConfigurationAnnotationRule:
    """Rule that handles @configuration classes and @bean methods.

    Produces:
    1. Singleton registration for the @configuration class itself
    2. Factory registrations for each @bean method on the class
    """

    def apply(self, cls: type) -> Sequence[RegistrationStep]:
        """Return registration steps for @configuration class and its @bean methods.

        The configuration class is registered as a singleton, and each @bean method
        produces a factory registration that resolves the configuration instance
        and invokes the method.

        :param cls: The class to analyze
        :return: Sequence containing singleton step for configuration class plus
                 factory steps for each @bean method, or empty sequence if class
                 lacks @configuration annotation
        """
        annotation = find_annotation(cls, ConfigurationAnnotation)
        if annotation is None:
            return []

        steps: list[RegistrationStep] = []

        # Register configuration class as singleton
        steps.append(SingletonRegistrationStep(cls, cls))

        # Scan for @bean methods
        for name, member in inspect.getmembers(cls, predicate=inspect.ismethod):
            bean_annotation = find_annotation(member, BeanAnnotation)
            if bean_annotation is None:
                continue

            # Extract return type from method signature
            return_type = self._extract_return_type(member)
            if return_type is None:
                continue

            # Create factory that resolves config and calls method
            factory = self._create_bean_factory(cls, member)
            steps.append(FactoryRegistrationStep(return_type, factory))

        return steps

    def _extract_return_type(self, method: Callable[..., Any]) -> type | None:
        """Extract return type annotation from method signature.

        :param method: Method to analyze
        :return: Return type annotation, or None if not present
        """
        hints = typing.get_type_hints(method)
        return hints.get('return')

    def _create_bean_factory(
        self,
        config_cls: type,
        method: Callable[..., Any]
    ) -> Callable[[Any], Any]:
        """Create factory function that resolves config and invokes bean method.

        The factory accepts a container, resolves the configuration instance,
        and calls the bean method on that instance.

        :param config_cls: Configuration class type
        :param method: Bean method to invoke
        :return: Factory function accepting container and returning bean instance
        """
        def factory(container: Any) -> Any:
            config_instance = container.resolve(config_cls)
            return method(config_instance)

        return factory

4.3 ScanningConfiguration

from dataclasses import dataclass
from collections.abc import Sequence

from loryth.common.annotations import Annotation


@dataclass(frozen=True)
class ScanningConfiguration:
    """Immutable configuration for package scanning behavior.

    :param packages: Sequence of package names to scan (e.g., ["myapp.services"])
    :param include_annotations: If non-empty, only classes with these annotations are included
    :param exclude_patterns: Glob patterns for excluding classes/modules (e.g., ["*_test.py"])
    :param recursive: Whether to recursively scan subpackages (default: True)
    """

    packages: Sequence[str]
    include_annotations: Sequence[type[Annotation]] = ()
    exclude_patterns: Sequence[str] = ()
    recursive: bool = True

4.4 BootstrapPlanBuilder

import fnmatch
from collections.abc import Sequence

from loryth.common.annotations import AnnotationContext
from loryth.common.annotations import AnnotationContext as AnnCtx
from loryth.common.di.bootstrap import BootstrapPlan, RegistrationStep
from loryth.common.di.exceptions import BootstrapError
from loryth.common.reflection import ReflectionContext
from loryth.common.reflection import reflection
from loryth.common.reflection.member_info import MemberInfo, TypeInfo


class BootstrapPlanBuilder:
    """Fluent API for building bootstrap plans via package scanning and manual registration.

    The builder accumulates discovered classes and registration steps through chained
    method calls, then produces a BootstrapPlan on demand. The builder is reusable -
    build() can be called multiple times to generate plans with the current state.

    Example:
        builder = BootstrapPlanBuilder.create()
        builder.scan_packages(services_config)
        builder.add_class(ManualService)
        plan = builder.build()

        # Builder is reusable
        builder.scan_packages(plugins_config)
        plan2 = builder.build()  # Includes previous scans + plugins
    """

    def __init__(
        self,
        reflection_context: ReflectionContext,
        annotation_context: AnnotationContext,
        rules: Sequence[RegistrationRule],
    ):
        """Initialize builder with required dependencies.

        :param reflection_context: Reflection context for package scanning
        :param annotation_context: Annotation context for annotation lookups
        :param rules: Sequence of registration rules to apply to discovered classes
        """
        self._reflection = reflection_context
        self._annotations = annotation_context
        self._rules = list(rules)
        self._discovered_classes: list[type] = []
        self._accumulated_steps: list[RegistrationStep] = []

    @staticmethod
    def create(
        reflection_context: ReflectionContext | None = None,
        annotation_context: AnnotationContext | None = None,
        rules: Sequence[RegistrationRule] | None = None,
    ) -> "BootstrapPlanBuilder":
        """Static factory with sensible defaults for all dependencies.

        :param reflection_context: Optional reflection context (defaults to singleton)
        :param annotation_context: Optional annotation context (defaults to singleton)
        :param rules: Optional rules (defaults to built-in rules)
        :return: Configured BootstrapPlanBuilder instance
        """
        default_rules = [
            ServiceAnnotationRule(),
            ComponentAnnotationRule(),
            ConfigurationAnnotationRule(),
        ]

        return BootstrapPlanBuilder(
            reflection_context=reflection_context or reflection,
            annotation_context=annotation_context or AnnCtx,
            rules=rules if rules is not None else default_rules,
        )

    def scan_packages(
        self,
        config: ScanningConfiguration
    ) -> "BootstrapPlanBuilder":
        """Scan configured packages and accumulate registration steps.

        This method is chainable - it returns self for fluent API usage.
        Discovered classes and generated steps are accumulated in the builder.

        :param config: Scanning configuration specifying packages and filters
        :return: Self for method chaining
        :raises BootstrapError: If package resolution or scanning fails
        """
        try:
            for package_name in config.packages:
                discovered = self._scan_package(package_name, config)
                self._discovered_classes.extend(discovered)

                # Apply all rules to discovered classes
                for cls in discovered:
                    self._apply_rules(cls)

        except Exception as e:
            raise BootstrapError(
                f"Failed to scan packages {list(config.packages)}: {e}"
            ) from e

        return self

    def add_class(self, cls: type) -> "BootstrapPlanBuilder":
        """Manually add a class and apply rules.

        This method is chainable - it returns self for fluent API usage.
        Useful for registering specific classes without scanning their package.

        :param cls: The class to register
        :return: Self for method chaining
        :raises BootstrapError: If rule application fails
        """
        self._discovered_classes.append(cls)
        self._apply_rules(cls)
        return self

    def build(self) -> BootstrapPlan:
        """Build bootstrap plan from accumulated steps.

        This method does NOT clear builder state - the builder remains reusable.
        Calling build() multiple times returns plans with the same accumulated steps.

        Step deduplication is delegated to the container (TD-020 idempotency).

        :return: BootstrapPlan containing all accumulated registration steps
        """
        return BootstrapPlan(registrations=tuple(self._accumulated_steps))

    def _scan_package(
        self,
        package_name: str,
        config: ScanningConfiguration
    ) -> list[type]:
        """Scan a single package using reflection scanner with filters.

        :param package_name: Package to scan
        :param config: Scanning configuration with filter criteria
        :return: List of discovered classes matching filter criteria
        """
        def predicate(member: MemberInfo) -> bool:
            return self._accept_member(member, config)

        discovered: list[type] = []

        # Use reflection scanner with predicate
        members = self._reflection.scan.package(
            package_name,
            recursive=config.recursive,
            public_only=True,  # Only public classes
        )

        # Apply predicate to filter members
        for member in members:
            if predicate(member) and isinstance(member, TypeInfo):
                discovered.append(member.python_type)

        return discovered

    def _accept_member(
        self,
        member: MemberInfo,
        config: ScanningConfiguration
    ) -> bool:
        """Filter predicate for reflection scanner.

        :param member: Member to evaluate
        :param config: Configuration with filter criteria
        :return: True if member should be included in scan results
        """
        if not isinstance(member, TypeInfo):
            return False

        python_type = member.python_type

        # Include annotation filter
        if config.include_annotations:
            if not any(
                self._annotations.has_annotation(python_type, annotation_type)
                for annotation_type in config.include_annotations
            ):
                return False

        # Exclude pattern filter
        qualname = member.qualname
        module_name = member.module.qualname

        for pattern in config.exclude_patterns:
            if fnmatch.fnmatch(qualname, pattern) or fnmatch.fnmatch(module_name, pattern):
                return False

        return True

    def _apply_rules(self, cls: type) -> None:
        """Apply all rules to a class and accumulate registration steps.

        Uses union semantics - all matching rules apply. If a rule raises an
        exception, it's wrapped in BootstrapError for fail-fast behavior.

        :param cls: Class to analyze
        :raises BootstrapError: If any rule fails
        """
        for rule in self._rules:
            try:
                steps = rule.apply(cls)
                self._accumulated_steps.extend(steps)
            except Exception as e:
                raise BootstrapError(
                    f"Rule {rule.__class__.__name__} failed on {cls.__name__}: {e}"
                ) from e

โš™๏ธ 5. Semantics

5.1 Rule Application

  • Union Semantics: All matching rules apply to each class (not first-match)
  • Idempotency: Rules should be stateless; calling apply() multiple times produces equivalent steps
  • Fail-Fast: If any rule raises an exception, scanning terminates with BootstrapError
  • Order Independence: Rules do not depend on execution order

5.2 Step Deduplication

  • Delegated to Container: Builder does not deduplicate steps explicitly
  • TD-020 Idempotency: Container handles duplicate registrations per idempotency rules
  • Multiple Scans: Scanning the same package twice produces duplicate steps that container filters

5.3 Builder Lifecycle

  • Reusable: Builder accumulates state across method calls
  • Non-Destructive Build: build() does not clear state; multiple builds return same plan
  • Fluent API: scan_packages() and add_class() return self for chaining

5.4 Error Handling

  • Package Resolution Errors: Raised as BootstrapError with original exception chained
  • Module Import Failures: Reflection scanner records metrics and continues (see reflection documentation)
  • Rule Failures: Raised as BootstrapError with rule name and class context

๐Ÿงช 6. Examples

6.1 Basic Scanning with Default Rules

from loryth.common.di.bootstrap import (
    BootstrapPlanBuilder,
    BootstrapRuntime,
    ScanningConfiguration,
)
from loryth.common.di.container import Container
from loryth.common.di.annotations import ServiceAnnotation, ComponentAnnotation

# Create builder with defaults
builder = BootstrapPlanBuilder.create()

# Configure scanning
config = ScanningConfiguration(
    packages=["myapp.services", "myapp.repositories"],
    include_annotations=[ServiceAnnotation, ComponentAnnotation],
    exclude_patterns=[],
    recursive=True,
)

# Scan and build plan
plan = builder.scan_packages(config).build()

# Bootstrap container
runtime = BootstrapRuntime()
container = Container()
runtime.bootstrap(container, plan)

# Resolve services
user_service = container.resolve(UserService)

6.2 Custom Filtering

from loryth.common.di.bootstrap import BootstrapPlanBuilder, ScanningConfiguration
from loryth.common.di.annotations import ServiceAnnotation

# Create builder
builder = BootstrapPlanBuilder.create()

# Scan with custom filters
config = ScanningConfiguration(
    packages=["myapp"],
    include_annotations=[ServiceAnnotation],  # Only @service
    exclude_patterns=[
        "*_legacy.py",      # Skip legacy modules
        "*.test.*",         # Skip test packages
        "*_mock.py",        # Skip mock implementations
    ],
    recursive=True,
)

plan = builder.scan_packages(config).build()

runtime = BootstrapRuntime()
container = DiContainer()
runtime.bootstrap(container, plan)

6.3 Multiple Scans and Manual Registration

from loryth.common.di.bootstrap import BootstrapPlanBuilder, ScanningConfiguration
from loryth.common.di.annotations import ServiceAnnotation, ComponentAnnotation

# Create builder
builder = BootstrapPlanBuilder.create()

# Scan services
services_config = ScanningConfiguration(
    packages=["myapp.services"],
    include_annotations=[ServiceAnnotation],
    exclude_patterns=[],
)
builder.scan_packages(services_config)

# Scan repositories
repositories_config = ScanningConfiguration(
    packages=["myapp.repositories"],
    include_annotations=[ComponentAnnotation],
    exclude_patterns=[],
)
builder.scan_packages(repositories_config)

# Add manual class
builder.add_class(LegacyService)

# Build combined plan
plan = builder.build()

# Bootstrap
runtime = BootstrapRuntime()
container = DiContainer()
runtime.bootstrap(container, plan)

# Reuse builder for additional scans
plugins_config = ScanningConfiguration(
    packages=["myapp.plugins"],
    include_annotations=[ServiceAnnotation],
    exclude_patterns=[],
)
builder.scan_packages(plugins_config)
plan2 = builder.build()  # Includes all previous scans + plugins

๐Ÿงช 7. Test Matrix

tests/unit/di/bootstrap/
โ”œโ”€โ”€ test_registration_rules.py       # Rule protocol implementation and behavior
โ”œโ”€โ”€ test_bootstrap_plan_builder.py   # Builder operations, fluent API, state management
โ””โ”€โ”€ test_scanning_configuration.py   # Configuration validation and defaults

tests/integration/di/bootstrap/
โ”œโ”€โ”€ test_scanning_workflows.py       # Package scanning with reflection integration
โ”œโ”€โ”€ test_rule_application_workflows.py # Union semantics, rule combination scenarios
โ””โ”€โ”€ test_builder_runtime_integration.py # End-to-end plan building and bootstrap execution

7.1 Unit Test Coverage

  • Rule Protocol Implementation: Each built-in rule (ServiceAnnotationRule, ComponentAnnotationRule, ConfigurationAnnotationRule) correctly identifies annotated classes and produces appropriate registration steps.
  • Rule Return Semantics: Rules return empty sequences for non-matching classes; matching rules produce singleton, transient, or factory steps as appropriate.
  • Configuration Bean Discovery: ConfigurationAnnotationRule scans for @bean methods and creates factory registrations with correct return types.
  • Builder State Management: Builder accumulates classes and steps across multiple scan_packages() and add_class() calls; build() does not clear state.
  • Fluent API Chaining: scan_packages() and add_class() return self for method chaining.
  • Static Factory Defaults: BootstrapPlanBuilder.create() provides working defaults for reflection context, annotation context, and built-in rules.
  • Scanning Configuration: Validate package lists, annotation filters, exclude patterns, and recursive flag behavior.

7.2 Integration Test Coverage

  • Package Scanning Workflow: Builder integrates with reflection scanner to discover classes matching predicate criteria.
  • Annotation Filtering: include_annotations filter correctly limits discovered classes to those with specific annotations.
  • Exclude Patterns: Glob patterns correctly filter out matching class names and module paths.
  • Union Semantics: Multiple rules apply to same class (e.g., class with both @service and custom annotation produces multiple steps).
  • Rule Composition: Custom rules integrate with built-in rules via constructor injection.
  • Builder-Runtime Integration: Plans built by BootstrapPlanBuilder successfully bootstrap containers via BootstrapRuntime.bootstrap().
  • Error Propagation: Scanning failures and rule failures raise BootstrapError with contextual messages.
  • Reusable Builder: Builder can be used for multiple scan-build cycles without clearing state.

Testing notes:

  • Unit tests use minimal mocks (e.g., reflection scanner stubs returning known classes).
  • Integration tests use real reflection scanner, annotation context, and bootstrap runtime.
  • Test coverage includes edge cases: empty packages, no matching classes, malformed predicates, rule exceptions.
  • No tests for removed TD-012 components (AnnotationProcessor, InjectorBridge, etc.) - these are deleted in Phase 5d.

โœ… 8. Acceptance Criteria

  • RegistrationRule protocol defined with apply(cls: type) -> Sequence[RegistrationStep]
  • BootstrapPlanBuilder implements fluent API with scan_packages(), add_class(), and build()
  • BootstrapPlanBuilder.create() static factory with default rules
  • Builder is reusable - build() does not clear state
  • Three built-in rules implemented: ServiceAnnotationRule, ComponentAnnotationRule, ConfigurationAnnotationRule
  • ScanningConfiguration dataclass with packages, filters, and recursive flag
  • Integration with reflection scanner (predicate-based filtering confirmed)
  • Union semantics - all matching rules apply to each class
  • Fail-fast error handling with BootstrapError
  • Step deduplication delegated to TD-020 container idempotency
  • TD-012 components deprecated: AnnotationProcessor, InjectorBridge, AnnotationRegistry, ClassAnnotationMetadata, BeanMetadata
  • Migration guide from processor pattern to rules (section 9)
  • Examples demonstrate basic scanning, custom filtering, and multiple scans
  • All test scenarios in test matrix covered by automated tests (section 7)

๐Ÿš€ 9. Rollout and Migration

9.1 Implementation Phases

Phase 5a: Core infrastructure - Implement RegistrationRule protocol - Implement BootstrapPlanBuilder with fluent API - Implement ScanningConfiguration - Add unit tests for builder operations

Phase 5b: Built-in rules - Implement ServiceAnnotationRule - Implement ComponentAnnotationRule - Implement ConfigurationAnnotationRule with @bean support - Add unit tests for each rule

Phase 5c: Integration and testing - Integration tests with TD-020 BootstrapRuntime - Update documentation with migration examples - Performance testing for scanning overhead

Phase 5d: TD-012 component removal - Remove AnnotationProcessor, InjectorBridge, ClassAnnotationMetadata, BeanMetadata from public API - Update all tests using deprecated components to use BootstrapPlanBuilder - Delete implementation code for removed components - Remove all documentation references to deprecated APIs

9.2 Migration from TD-012 AnnotationProcessor

Before (TD-012 Pattern):

from loryth.common.di.annotations import AnnotationProcessor
from loryth.common.di.container import Container

# Old processor pattern
container = Container()
processor = AnnotationProcessor(
    builder=AnnotationBuilder(),
    registry=AnnotationRegistry(),
    bridge=InjectorBridge(container),
)
processor.process([UserService, EmailService])  # Mutates container
container.bootstrap()  # Old bootstrap pattern

After (TD-013 Rules Pattern):

from loryth.common.di.bootstrap import (
    BootstrapPlanBuilder,
    BootstrapRuntime,
    ScanningConfiguration,
)
from loryth.common.di.container import Container
from loryth.common.di.annotations import ServiceAnnotation

# New rule-based pattern
builder = BootstrapPlanBuilder.create()
builder.add_class(UserService)
builder.add_class(EmailService)
plan = builder.build()

runtime = BootstrapRuntime()
container = Container()
runtime.bootstrap(container, plan)  # New bootstrap pattern (TD-020)

Migration with Scanning:

# Replace manual class lists with scanning
config = ScanningConfiguration(
    packages=["myapp.services"],
    include_annotations=[ServiceAnnotation],
    exclude_patterns=[],
)

builder = BootstrapPlanBuilder.create()
plan = builder.scan_packages(config).build()

runtime = BootstrapRuntime()
container = DiContainer()
runtime.bootstrap(container, plan)

9.3 Backward Compatibility

  • No backward compatibility maintained - TD-012 components removed in Phase 5d
  • Migration guide (section 9.2) provides step-by-step conversion examples
  • All existing tests updated to use new BootstrapPlanBuilder API

โš ๏ธ 10. Risks and Mitigations

Risk Mitigation
Scanning performance overhead Reflection scanner metrics track performance; caching at application level (future)
Rule application failures Fail-fast with detailed error messages; extensive unit tests for built-in rules
Import side effects during scanning Reflection scanner handles import errors gracefully; metrics track failures
Duplicate registrations from multiple scans TD-020 idempotency handles duplicates; builder accumulates steps without explicit deduplication
Migration complexity from TD-012 Comprehensive migration guide with examples; clear migration path in Phase 5d

๐Ÿ“ˆ 11. Progress

Legend: โณ Pending ยท ๐Ÿšง In Progress ยท โœ… Completed

  • โœ… Phase 5a โ€” Core Infrastructure

    • โœ… Implement RegistrationRule protocol
    • โœ… Implement BootstrapPlanBuilder with fluent API
    • โœ… Implement ScanningConfiguration dataclass
    • โœ… Unit tests for builder operations
  • โœ… Phase 5b โ€” Built-In Rules

    • โœ… Implement ServiceAnnotationRule
    • โœ… Implement ComponentAnnotationRule
    • โœ… Implement ConfigurationAnnotationRule
    • โœ… Unit tests for each rule
  • โœ… Phase 5c โ€” Integration and Testing

    • โœ… Integration tests with TD-020 BootstrapRuntime
    • โœ… Migration guide and examples
    • โณ Performance testing (deferred - not blocking)
  • โœ… Phase 5d โ€” TD-012 Component Removal

    • โœ… Remove deprecated components from public API (already complete - no code existed)
    • โœ… Update tests to use new BootstrapPlanBuilder (all tests use new API)
    • โœ… Delete implementation code (no deprecated code found in codebase)
    • โœ… Clean up documentation references (TD-012 marked as complete)

๐Ÿ“ 12. Changelog

  • 2025-10-11 โ€” TD-013 implementation complete:
  • All phases (5a-5d) completed successfully
  • Comprehensive test suite: 58 new tests (20 unit + 38 integration)
  • Total bootstrap test coverage: 125 tests (78 unit + 47 integration)
  • Test files created:
    • tests/unit/di/bootstrap/test_scanning_configuration.py (20 tests)
    • tests/integration/di/bootstrap/test_scanning_workflows.py (14 tests)
    • tests/integration/di/bootstrap/test_rule_application_workflows.py (12 tests)
    • tests/integration/di/bootstrap/test_builder_runtime_integration.py (12 tests)
  • All acceptance criteria verified and checked
  • TD-012 component removal confirmed (no deprecated code found)
  • Status updated to Complete
  • 2025-10-11 โ€” Refinement pass addressing structure and documentation standards:
  • Updated section 3.2 title to "Breaking Changes" and 3.3 to "Rollout Policy"
  • Converted all code examples to sphinx docstring style (:param, :return, :raises)
  • Removed function-scope imports from code examples (moved to module level)
  • Added comprehensive test matrix section (7) before acceptance criteria
  • Added Phase 5d for TD-012 component removal in rollout plan
  • Updated backward compatibility policy: no deprecation warnings, immediate removal in Phase 5d
  • Renumbered sections: acceptance criteria now section 8, rollout section 9, risks section 10, etc.
  • 2025-10-11 โ€” Major revision aligned with TD-020:
  • Removed @application and ApplicationBootstrap (moved to TD-021)
  • Replaced AnnotationProcessor pattern with RegistrationRule abstraction
  • Introduced BootstrapPlanBuilder with fluent API
  • Confirmed reflection scanner API supports predicate-based filtering
  • Documented TD-012 component deprecations and migration path
  • Added three comprehensive examples (basic, custom filtering, multiple scans)
  • Updated design to leverage TD-020 idempotency for step deduplication
  • 2025-10-08 โ€” Updated dependencies to reference TD-020 bootstrap runtime
  • 2025-10-02 โ€” Initial draft technical design created

๐Ÿ“– 13. Glossary

Term Definition
RegistrationRule Protocol for rules that translate annotated classes into registration steps
BootstrapPlanBuilder Fluent API for scanning packages, adding classes, and building bootstrap plans
ScanningConfiguration Immutable configuration specifying packages, filters, and scanning behavior
Rule Application Process of analyzing a class and producing registration steps based on annotations
Union Semantics Behavior where all matching rules apply to a class (not first-match)
Fluent API Method chaining pattern where methods return self for composition
Step Deduplication Process of removing duplicate registration steps (delegated to TD-020 container idempotency)