Skip to content

The Module Architecture

At its core, Protostar is not a monolithic script; it is a polymorphic module resolution engine. The CLI parser's sole responsibility is translating a matrix of boolean flags (e.g., --python --astro --ruff) into an ordered array of instantiated module objects.

These modules act as autonomous, stateless plugins that interact strictly with the EnvironmentManifest. They do not know about each other, they do not read the host filesystem, and they do not execute system commands directly.

  • Polymorphic Contracts

    Every toolchain component inherits from a base abstract class (BootstrapModule or PresetModule). This enforces a standardized API (pre_flight and build) that the Orchestrator can blindly iterate over.

  • Topological Sequencing

    Modules are loaded into the Orchestrator in a highly specific layering hierarchy (OS $\rightarrow$ IDE $\rightarrow$ Language $\rightarrow$ Tooling $\rightarrow$ Presets). This prevents dependency race conditions during AST compilation.

  • Strict Decoupling

    A module only declares intent. Because modules never execute their own side-effects, testing a new language implementation simply requires asserting the state of the manifest in-memory.


The Layering Model

If multiple modules attempt to modify the same conceptual space (e.g., both the Python module and the Ruff module modifying IDE telemetry), the Orchestrator relies on sequence order to determine precedence.

graph TD
    %% Styling
    classDef layer fill:#1e293b,stroke:#3b82f6,stroke-width:2px,color:#fff,font-weight:bold;
    classDef base fill:#0f172a,stroke:#00e5ff,stroke-width:3px,color:#fff;
    classDef top fill:#334155,stroke:#f43f5e,stroke-width:2px,color:#fff;

    subgraph Stack [ ]
        direction BT

        L1[1. System Layer]:::base
        L2[2. Language Layer]:::layer
        L3[3. Tooling Layer]:::layer
        L4[4. Presets]:::top

        %% Relationships showing precedence flow
        L1 --> L2 --> L3 --> L4
    end

    %% Annotations
    Note1[<b>Foundation</b><br/>Universal Hygiene] -- Initialized first --> L1
    Note2[<b>Final Overrides</b><br/>Domain-specific wrappers] -- Loaded last --> L4

    style Stack fill:transparent,stroke:#475569,stroke-dasharray: 5 5

The stack is resolved and executed in the following strict order:

1. System Layer

Configures universal environment artifacts and workspace hygiene. The deterministic SystemWorkspaceModule ignores standard host artifacts (.DS_Store), IDE directories (.idea/, .vscode/), and credentials (.env) across all initialized environments.

2. Language Layer

The core runtime environment (i.e., Python). This layer establishes the primary dependency managers (like uv or pip), injects the baseline project configuration files (like pyproject.toml), and conditionally evaluates the global configuration to inject IDE-specific setup (such as pointing VS Code to the generated Python interpreter).

3. Tooling Layer

Ancillary development tools that latch onto the language layer. Tools like ruff, mypy, or pre-commit evaluate the manifest to inject specific configuration blocks into the language layer's files.

4. Presets

Domain-specific wrappers. Presets are loaded last because they often act as "meta-modules," bundling libraries for specific use cases (like astrophysics data pipelines or machine learning environments) and overriding default tool configurations.


The Module Contract

When extending Protostar, developers implement specific methods dictated by the base classes. The Orchestrator guarantees these methods are called at the correct execution boundary during the lifecycle.

pre_flight()

The fail-fast perimeter. If a module requires external binaries (e.g., git, uv, cargo) to function, it must verify their presence in the system $PATH here. If the check fails, an exception is raised before any filesystem mutation occurs, protecting the workspace from partial scaffolding.

build(manifest: EnvironmentManifest)

The aggregation phase. Modules receive the mutable manifest object and use its API to register dependencies, directory structures, ignored files, and AST payloads.

# Example: A simplified tool implementation
class MyPyModule(BootstrapModule):

    def build(self, manifest: EnvironmentManifest) -> None:
        # Register the dependency
        manifest.add_dev_dependency("mypy")

        # Inject the AST payload for pyproject.toml
        manifest.add_file_append("pyproject.toml", """
[tool.mypy]
strict = true
warn_return_any = true
        """)

API Reference

Core Interface: BootstrapModule

protostar.modules.base.BootstrapModule

Bases: ABC

Appends module-specific requirements to the environment manifest.

Source code in src/protostar/modules/base.py
class BootstrapModule(abc.ABC):
    """Appends module-specific requirements to the environment manifest."""

    cli_flags: ClassVar[tuple[str, ...]] = ()
    """The CLI flags to trigger this module (e.g., ('-p', '--python'))."""

    cli_help: ClassVar[str] = ""
    """The help description for the CLI flag."""

    config_key: ClassVar[str] = ""
    """The global configuration key used to evaluate if this module is active."""

    @property
    @abc.abstractmethod
    def name(self) -> str:
        """Returns the human-readable identifier for the module."""
        pass

    @property
    def aliases(self) -> list[str]:
        """Returns a list of configuration aliases that map to this module.

        Used for dynamic resolution from the global configuration file.
        """
        return []

    @property
    def collision_markers(self) -> list[Path]:
        """Returns a list of critical filesystem paths to evaluate for collisions during pre-flight.

        Returns:
            A list of Path objects representing critical configuration files or directories
            managed by this module. Defaults to an empty list.
        """
        return []

    def pre_flight(self) -> None:  # noqa: B027
        """Verifies system prerequisites before manifest building begins.

        Raises:
            RuntimeError: If a critical dependency (e.g., 'uv', 'cargo') is missing.
        """
        pass

    @abc.abstractmethod
    def build(self, manifest: "EnvironmentManifest") -> None:
        """Appends module-specific requirements to the environment manifest.

        Args:
            manifest (EnvironmentManifest): The centralized state object.
        """
        pass

cli_flags class-attribute

cli_flags = ()

The CLI flags to trigger this module (e.g., ('-p', '--python')).

cli_help class-attribute

cli_help = ''

The help description for the CLI flag.

config_key class-attribute

config_key = ''

The global configuration key used to evaluate if this module is active.

name abstractmethod property

name

Returns the human-readable identifier for the module.

aliases property

aliases

Returns a list of configuration aliases that map to this module.

Used for dynamic resolution from the global configuration file.

collision_markers property

collision_markers

Returns a list of critical filesystem paths to evaluate for collisions during pre-flight.

Returns:

Type Description
list[Path]

A list of Path objects representing critical configuration files or directories

list[Path]

managed by this module. Defaults to an empty list.

pre_flight

pre_flight()

Verifies system prerequisites before manifest building begins.

Raises:

Type Description
RuntimeError

If a critical dependency (e.g., 'uv', 'cargo') is missing.

Source code in src/protostar/modules/base.py
def pre_flight(self) -> None:  # noqa: B027
    """Verifies system prerequisites before manifest building begins.

    Raises:
        RuntimeError: If a critical dependency (e.g., 'uv', 'cargo') is missing.
    """
    pass

build abstractmethod

build(manifest)

Appends module-specific requirements to the environment manifest.

Parameters:

Name Type Description Default
manifest EnvironmentManifest

The centralized state object.

required
Source code in src/protostar/modules/base.py
@abc.abstractmethod
def build(self, manifest: "EnvironmentManifest") -> None:
    """Appends module-specific requirements to the environment manifest.

    Args:
        manifest (EnvironmentManifest): The centralized state object.
    """
    pass
Core Interface: PresetModule

protostar.presets.base.PresetModule

Bases: ABC

Appends module-specific requirements to the environment manifest.

Source code in src/protostar/presets/base.py
class PresetModule(abc.ABC):
    """Appends module-specific requirements to the environment manifest."""

    cli_flags: ClassVar[tuple[str, ...]] = ()
    """The CLI flags to trigger this preset (e.g., ('-a', '--astro'))."""

    cli_help: ClassVar[str] = ""
    """The help description for the CLI flag."""

    @property
    @abc.abstractmethod
    def name(self) -> str:
        """Returns the human-readable identifier for the preset."""
        pass

    @property
    def config_key(self) -> str:
        """Returns the dictionary key used in config.toml for overrides."""
        return self.__class__.__name__.replace("Preset", "").lower()

    def _apply_overrides(self, manifest: "EnvironmentManifest") -> bool:
        """Applies user-defined overrides from the global configuration if present.

        Returns:
            True if overrides were applied (and defaults should be skipped), False otherwise.
        """
        # Late import to prevent circular dependency at module initialization
        from protostar.config import ProtostarConfig

        config = ProtostarConfig.load()
        overrides = config.presets.get(self.config_key)

        if not isinstance(overrides, dict):
            return False

        logger.debug(f"Applying custom configuration overrides for {self.name} preset.")

        for dep in overrides.get("dependencies", []):
            manifest.add_dependency(dep)

        for dev_dep in overrides.get("dev_dependencies", []):
            manifest.add_dev_dependency(dev_dep)

        for directory in overrides.get("directories", []):
            manifest.add_directory(directory)

        return True

    @property
    def default_dependencies(self) -> list[str]:
        """Returns a list of default packages to inject for this preset."""
        return []

    @property
    def default_directories(self) -> list[str]:
        """Returns a list of default directories to scaffold for this preset."""
        return []

    @property
    def default_ignores(self) -> list[str]:
        """Returns a list of default VCS ignore patterns for this preset."""
        return []

    def build(self, manifest: "EnvironmentManifest") -> None:
        """Appends preset-specific dependencies and directories to the manifest.

        Automatically applies configuration overrides if present. Otherwise, injects
        the default packages, directories, and ignores defined by the preset subclass.

        Args:
            manifest (EnvironmentManifest): The centralized state object.
        """
        logger.debug(f"Building {self.name} preset layer.")

        if self._apply_overrides(manifest):
            return

        for dep in self.default_dependencies:
            manifest.add_dependency(dep)

        for directory in self.default_directories:
            manifest.add_directory(directory)

        for artifact in self.default_ignores:
            manifest.add_vcs_ignore(artifact)

cli_flags class-attribute

cli_flags = ()

The CLI flags to trigger this preset (e.g., ('-a', '--astro')).

cli_help class-attribute

cli_help = ''

The help description for the CLI flag.

name abstractmethod property

name

Returns the human-readable identifier for the preset.

config_key property

config_key

Returns the dictionary key used in config.toml for overrides.

default_dependencies property

default_dependencies

Returns a list of default packages to inject for this preset.

default_directories property

default_directories

Returns a list of default directories to scaffold for this preset.

default_ignores property

default_ignores

Returns a list of default VCS ignore patterns for this preset.

build

build(manifest)

Appends preset-specific dependencies and directories to the manifest.

Automatically applies configuration overrides if present. Otherwise, injects the default packages, directories, and ignores defined by the preset subclass.

Parameters:

Name Type Description Default
manifest EnvironmentManifest

The centralized state object.

required
Source code in src/protostar/presets/base.py
def build(self, manifest: "EnvironmentManifest") -> None:
    """Appends preset-specific dependencies and directories to the manifest.

    Automatically applies configuration overrides if present. Otherwise, injects
    the default packages, directories, and ignores defined by the preset subclass.

    Args:
        manifest (EnvironmentManifest): The centralized state object.
    """
    logger.debug(f"Building {self.name} preset layer.")

    if self._apply_overrides(manifest):
        return

    for dep in self.default_dependencies:
        manifest.add_dependency(dep)

    for directory in self.default_directories:
        manifest.add_directory(directory)

    for artifact in self.default_ignores:
        manifest.add_vcs_ignore(artifact)