Skip to content

Scaffolding

scaffolding

Public scaffold primitives and optional domain-provider discovery.

Entry points in axm.scaffold_providers are named after the scaffold kind and load a zero-argument provider factory. Domain packages own their templates.

CheckResult

Bases: BaseModel

Result of a single audit check.

Note: type: ignore[explicit-any] flags pydantic BaseModel internals (third-party).

Source code in packages/axm-init/src/axm_init/models/check.py
Python
class CheckResult(BaseModel):  # type: ignore[explicit-any]
    """Result of a single audit check.

    Note: ``type: ignore[explicit-any]`` flags pydantic ``BaseModel``
    internals (third-party).
    """

    model_config = ConfigDict(extra="forbid")

    name: str
    category: str
    passed: bool
    weight: int
    message: str
    details: list[str]
    fix: str

    @computed_field  # type: ignore[prop-decorator]
    @property
    def earned(self) -> int:
        """Points earned: weight if passed, 0 otherwise."""
        return self.weight if self.passed else 0
earned property

Points earned: weight if passed, 0 otherwise.

CopierAdapter

Adapter for Copier template operations.

Wraps Copier's run_copy function with a Pydantic-based interface and returns structured ScaffoldResult.

Source code in packages/axm-init/src/axm_init/adapters/copier.py
Python
class CopierAdapter:
    """Adapter for Copier template operations.

    Wraps Copier's run_copy function with a Pydantic-based interface
    and returns structured ScaffoldResult.
    """

    @staticmethod
    def _do_copy(config: CopierConfig) -> None:
        """Run copier, offloading to a thread if an event loop is active.

        Copier (via prompt_toolkit) calls ``asyncio.run()`` internally.
        When we are already inside an async event loop (e.g. MCP server),
        this raises ``RuntimeError: asyncio.run() cannot be called from
        a running event loop``.  The fix: detect the running loop and
        execute the blocking copy in a **separate thread** which gets
        its own event loop context.
        """
        import asyncio

        def _run() -> None:
            # ``run_copy`` declares ``data: dict[str, Any] | None``;
            # converting our ``Mapping[str, object]`` to a plain ``dict``
            # widens cleanly to the expected type.
            run_copy(
                src_path=str(config.template_path),
                dst_path=config.destination,
                data=dict(config.data),
                defaults=config.defaults,
                overwrite=config.overwrite,
                unsafe=config.trust_template,
                skip_tasks=config.skip_tasks,
                answers_file=config.answers_file,
                exclude=config.exclude,
            )

        try:
            asyncio.get_running_loop()
        except RuntimeError:
            # No event loop — safe to call directly (CLI context).
            _run()
        else:
            # Inside an event loop (MCP server) — offload without blocking
            # the running loop on a synchronous ``future.result()``.
            _offload_to_thread(_run)

    def apply_chain(
        self,
        layers: list[TemplateLayer],
        destination: Path,
        data: Mapping[str, object],
        *,
        record_answers: bool = True,
    ) -> ScaffoldResult:
        """Apply ordered template layers to one destination.

        Each layer receives caller data overlaid with its own data and keeps a
        dedicated answers file so Copier can reapply its ownership rules
        independently from the other layers.

        With ``record_answers=False``, exclude the engine's answer destinations
        before rendering and omit fallback answer creation. Existing answers
        stay untouched, including answers belonging to the same layer.
        """
        result = ScaffoldResult(
            success=True,
            path=str(destination),
            message="No template layers to apply",
        )
        for layer in layers:
            layer_data = dict(data)
            layer_data.update(layer.data)
            answers_file = Path(f".copier-answers.{layer.name}.yml")
            layer_data["_src_path"] = str(layer.path)
            layer_data["_answers_file"] = str(answers_file)
            result = self.copy(
                CopierConfig(
                    template_path=layer.path,
                    destination=destination,
                    data=layer_data,
                    overwrite=True,
                    trust_template=True,
                    answers_file=answers_file,
                    exclude=()
                    if record_answers
                    else (f"/{answers_file}", "/.copier-answers.yml"),
                )
            )
            if not result.success:
                return result
            answers_path = destination / answers_file
            if record_answers and not answers_path.exists():
                answers_path.write_text(
                    json.dumps({"_src_path": str(layer.path)}, indent=2) + "\n"
                )
        return result

    def copy(self, config: CopierConfig) -> ScaffoldResult:
        """Execute Copier copy operation.

        Suppresses stdout/stderr via the scoped :func:`_suppress_output`
        context manager so that post-copy tasks (git init, uv sync,
        pre-commit install) don't pollute the parent process stdio —
        critical when running inside an MCP server.  Suppression is scoped
        to the interpreter-level streams and never mutates the process-global
        file descriptors 1/2, so concurrent writers on those fds are
        unaffected.

        Args:
            config: Copier configuration with template path, destination, and data.

        Returns:
            ScaffoldResult with success status and path.
        """
        if config.trust_template:
            logger.warning(
                "Running Copier with unsafe=True — template may execute "
                "arbitrary post-copy tasks."
            )
        try:
            with _suppress_output():
                self._do_copy(config)
            # Walk destination to collect created files, excluding noise
            # from post-copy tasks (.git, .venv, __pycache__, node_modules).
            _excluded = {
                ".git",
                ".venv",
                "__pycache__",
                "node_modules",
                ".mypy_cache",
            }
            created: list[str] = sorted(
                str(p.relative_to(config.destination))
                for p in config.destination.rglob("*")
                if p.is_file()
                and not any(
                    part in _excluded
                    for part in p.relative_to(config.destination).parts[:-1]
                )
            )
            return ScaffoldResult(
                success=True,
                path=str(config.destination),
                message="Project scaffolded via Copier",
                files_created=created,
            )
        except Exception as e:
            return ScaffoldResult(
                success=False,
                path=str(config.destination),
                message=f"Copier failed: {e}",
            )
apply_chain(layers, destination, data, *, record_answers=True)

Apply ordered template layers to one destination.

Each layer receives caller data overlaid with its own data and keeps a dedicated answers file so Copier can reapply its ownership rules independently from the other layers.

With record_answers=False, exclude the engine's answer destinations before rendering and omit fallback answer creation. Existing answers stay untouched, including answers belonging to the same layer.

Source code in packages/axm-init/src/axm_init/adapters/copier.py
Python
def apply_chain(
    self,
    layers: list[TemplateLayer],
    destination: Path,
    data: Mapping[str, object],
    *,
    record_answers: bool = True,
) -> ScaffoldResult:
    """Apply ordered template layers to one destination.

    Each layer receives caller data overlaid with its own data and keeps a
    dedicated answers file so Copier can reapply its ownership rules
    independently from the other layers.

    With ``record_answers=False``, exclude the engine's answer destinations
    before rendering and omit fallback answer creation. Existing answers
    stay untouched, including answers belonging to the same layer.
    """
    result = ScaffoldResult(
        success=True,
        path=str(destination),
        message="No template layers to apply",
    )
    for layer in layers:
        layer_data = dict(data)
        layer_data.update(layer.data)
        answers_file = Path(f".copier-answers.{layer.name}.yml")
        layer_data["_src_path"] = str(layer.path)
        layer_data["_answers_file"] = str(answers_file)
        result = self.copy(
            CopierConfig(
                template_path=layer.path,
                destination=destination,
                data=layer_data,
                overwrite=True,
                trust_template=True,
                answers_file=answers_file,
                exclude=()
                if record_answers
                else (f"/{answers_file}", "/.copier-answers.yml"),
            )
        )
        if not result.success:
            return result
        answers_path = destination / answers_file
        if record_answers and not answers_path.exists():
            answers_path.write_text(
                json.dumps({"_src_path": str(layer.path)}, indent=2) + "\n"
            )
    return result
copy(config)

Execute Copier copy operation.

Suppresses stdout/stderr via the scoped :func:_suppress_output context manager so that post-copy tasks (git init, uv sync, pre-commit install) don't pollute the parent process stdio — critical when running inside an MCP server. Suppression is scoped to the interpreter-level streams and never mutates the process-global file descriptors 1/2, so concurrent writers on those fds are unaffected.

Parameters:

Name Type Description Default
config CopierConfig

Copier configuration with template path, destination, and data.

required

Returns:

Type Description
ScaffoldResult

ScaffoldResult with success status and path.

Source code in packages/axm-init/src/axm_init/adapters/copier.py
Python
def copy(self, config: CopierConfig) -> ScaffoldResult:
    """Execute Copier copy operation.

    Suppresses stdout/stderr via the scoped :func:`_suppress_output`
    context manager so that post-copy tasks (git init, uv sync,
    pre-commit install) don't pollute the parent process stdio —
    critical when running inside an MCP server.  Suppression is scoped
    to the interpreter-level streams and never mutates the process-global
    file descriptors 1/2, so concurrent writers on those fds are
    unaffected.

    Args:
        config: Copier configuration with template path, destination, and data.

    Returns:
        ScaffoldResult with success status and path.
    """
    if config.trust_template:
        logger.warning(
            "Running Copier with unsafe=True — template may execute "
            "arbitrary post-copy tasks."
        )
    try:
        with _suppress_output():
            self._do_copy(config)
        # Walk destination to collect created files, excluding noise
        # from post-copy tasks (.git, .venv, __pycache__, node_modules).
        _excluded = {
            ".git",
            ".venv",
            "__pycache__",
            "node_modules",
            ".mypy_cache",
        }
        created: list[str] = sorted(
            str(p.relative_to(config.destination))
            for p in config.destination.rglob("*")
            if p.is_file()
            and not any(
                part in _excluded
                for part in p.relative_to(config.destination).parts[:-1]
            )
        )
        return ScaffoldResult(
            success=True,
            path=str(config.destination),
            message="Project scaffolded via Copier",
            files_created=created,
        )
    except Exception as e:
        return ScaffoldResult(
            success=False,
            path=str(config.destination),
            message=f"Copier failed: {e}",
        )

CopierConfig

Bases: BaseModel

Configuration for Copier execution.

Note: type: ignore[explicit-any] flags pydantic BaseModel internals (third-party).

Source code in packages/axm-init/src/axm_init/adapters/copier.py
Python
class CopierConfig(BaseModel):  # type: ignore[explicit-any]
    """Configuration for Copier execution.

    Note: ``type: ignore[explicit-any]`` flags pydantic ``BaseModel``
    internals (third-party).
    """

    template_path: Path
    destination: Path
    data: Mapping[str, object]
    defaults: bool = True
    overwrite: bool = False
    trust_template: bool = False
    skip_tasks: bool = False
    answers_file: Path | None = None
    """Render the template without running its ``_tasks``.

    The bundled templates declare post-copy tasks that shell out to ``git init``
    and two ``uv add`` invocations, so a single render resolves 72 packages and
    installs 72 of them — measured at 2.23s and 239 MB against 0.60s and 0.1 MB
    with tasks skipped. The rendered tree is identical either way: only the
    tasks' side-effects are absent, and the deterministic ones (LICENSE,
    ``.python-version``, ``uv.lock``, the pre-commit hook) are cheap to
    synthesize. Production scaffolding leaves this ``False``; a caller that only
    needs the rendered tree — a test asserting template structure, say — sets it
    to ``True`` rather than paying for a package installation it never inspects.
    """

    exclude: tuple[str, ...] = ()
    """Destination-relative patterns omitted by Copier during rendering."""

    model_config = ConfigDict(extra="forbid")
answers_file = None class-attribute instance-attribute

Render the template without running its _tasks.

The bundled templates declare post-copy tasks that shell out to git init and two uv add invocations, so a single render resolves 72 packages and installs 72 of them — measured at 2.23s and 239 MB against 0.60s and 0.1 MB with tasks skipped. The rendered tree is identical either way: only the tasks' side-effects are absent, and the deterministic ones (LICENSE, .python-version, uv.lock, the pre-commit hook) are cheap to synthesize. Production scaffolding leaves this False; a caller that only needs the rendered tree — a test asserting template structure, say — sets it to True rather than paying for a package installation it never inspects.

exclude = () class-attribute instance-attribute

Destination-relative patterns omitted by Copier during rendering.

Framework

Bases: StrEnum

Ecosystem a project is scaffolded/checked against.

Source code in packages/axm-init/src/axm_init/core/framework.py
Python
class Framework(StrEnum):
    """Ecosystem a project is scaffolded/checked against."""

    PYTHON = "python"
    NODE = "node"
    SVELTE = "svelte"
    REACT = "react"

ProviderError

Bases: ValueError

An installed provider is ambiguous, invalid, or cannot be loaded.

Source code in packages/axm-init/src/axm_init/scaffolding.py
Python
class ProviderError(ValueError):
    """An installed provider is ambiguous, invalid, or cannot be loaded."""

ProviderHook

Bases: Protocol

A provider capability; hooks are always invoked positionally.

Source code in packages/axm-init/src/axm_init/scaffolding.py
Python
class ProviderHook(Protocol):
    """A provider capability; hooks are always invoked positionally."""

    def __call__(self, *args: object) -> object:
        """Run the capability and return its provider-defined result."""
        ...
__call__(*args)

Run the capability and return its provider-defined result.

Source code in packages/axm-init/src/axm_init/scaffolding.py
Python
def __call__(self, *args: object) -> object:
    """Run the capability and return its provider-defined result."""
    ...

ScaffoldProvider

Bases: Protocol

Provider factory result; paths must remain available during rendering.

Providers may additionally define finalize(request, destination, data). It runs after successful rendering under the destination lock and may merge domain metadata. Raising an exception reports failure with partial output.

Source code in packages/axm-init/src/axm_init/scaffolding.py
Python
class ScaffoldProvider(Protocol):
    """Provider factory result; paths must remain available during rendering.

    Providers may additionally define ``finalize(request, destination, data)``.
    It runs after successful rendering under the destination lock and may merge
    domain metadata. Raising an exception reports failure with partial output.
    """

    def layers(self, request: ScaffoldRequest) -> tuple[TemplateLayer, ...]:
        """Return ordered layers, including any required standard base layer."""
        ...
layers(request)

Return ordered layers, including any required standard base layer.

Source code in packages/axm-init/src/axm_init/scaffolding.py
Python
def layers(self, request: ScaffoldRequest) -> tuple[TemplateLayer, ...]:
    """Return ordered layers, including any required standard base layer."""
    ...

ScaffoldRequest dataclass

Context used to select domain-owned layers (answers remain separate).

Source code in packages/axm-init/src/axm_init/scaffolding.py
Python
@dataclass(frozen=True)
class ScaffoldRequest:
    """Context used to select domain-owned layers (answers remain separate)."""

    kind: str
    framework: Framework | None = Framework.PYTHON
    member: bool = False
    existing: bool = False
    record_answers: bool = True

ScaffoldResult

Bases: BaseModel

Result of a scaffolding operation.

Note: type: ignore[explicit-any] flags pydantic BaseModel internals (third-party).

Source code in packages/axm-init/src/axm_init/models/results.py
Python
class ScaffoldResult(BaseModel):  # type: ignore[explicit-any]
    """Result of a scaffolding operation.

    Note: ``type: ignore[explicit-any]`` flags pydantic ``BaseModel``
    internals (third-party).
    """

    model_config = ConfigDict(extra="forbid")

    success: bool
    path: str
    message: str
    files_created: list[str] = Field(default_factory=list)
    profile: str | None = None
    mode: str | None = None
    root: str | None = None
    distribution: str | None = None
    preview: bool = False
    created: list[str] = Field(default_factory=list)
    updated: list[str] = Field(default_factory=list)
    unchanged: list[str] = Field(default_factory=list)
    conflicts: list[str] = Field(default_factory=list)
    protocols: list[str] = Field(default_factory=list)

TemplateLayer

Bases: BaseModel

One ordered Copier template application.

Source code in packages/axm-init/src/axm_init/core/templates.py
Python
class TemplateLayer(BaseModel):  # type: ignore[explicit-any]
    """One ordered Copier template application."""

    name: str
    path: Path
    data: dict[str, str]

    model_config = ConfigDict(extra="forbid", frozen=True)

TemplateType

Bases: StrEnum

Available scaffold template types.

Source code in packages/axm-init/src/axm_init/core/templates.py
Python
class TemplateType(StrEnum):
    """Available scaffold template types."""

    STANDALONE = "standalone"
    WORKSPACE = "workspace"
    MEMBER = "member"
    PAPER = "paper"
    EXPERIMENT = "experiment"
    LEARNING = "learning"

load_provider(kind)

Load only the requested kind; absence is distinct from broken installs.

Source code in packages/axm-init/src/axm_init/scaffolding.py
Python
def load_provider(kind: str) -> ScaffoldProvider | None:
    """Load only the requested kind; absence is distinct from broken installs."""
    entries = list(entry_points(group="axm.scaffold_providers", name=kind))
    if not entries:
        return None
    if len(entries) != 1:
        raise ProviderError(f"Duplicate scaffold providers for {kind!r}")
    try:
        provider = entries[0].load()()
    except Exception as exc:
        raise ProviderError(f"Cannot load scaffold provider {kind!r}: {exc}") from exc
    if not callable(getattr(provider, "layers", None)):
        raise ProviderError(f"Scaffold provider {kind!r} must define layers(request)")
    return cast(ScaffoldProvider, provider)

provider_hook(kind, name)

Resolve a required domain capability with an actionable version error.

Source code in packages/axm-init/src/axm_init/scaffolding.py
Python
def provider_hook(kind: str, name: str) -> ProviderHook:
    """Resolve a required domain capability with an actionable version error."""
    hook = getattr(require_provider(kind), name, None)
    if not callable(hook):
        raise ProviderError(
            f"Scaffold provider {kind!r} lacks {name}; "
            "install a compatible provider release."
        )
    return cast(ProviderHook, hook)

render_scaffold(kind, destination, data, *, framework=Framework.PYTHON, member=False, record_answers=True)

Create a scaffold in a missing/empty directory; never update user files.

Installed providers take precedence. Built-in kinds retain their historical template-chain fallback. Templates are trusted as with init itself, and may execute Copier tasks. This is not transactional: task/render failures may leave partial output. Existing learning overlays use their dedicated route. Set record_answers=False to omit engine-generated Copier answer files.

Source code in packages/axm-init/src/axm_init/scaffolding.py
Python
def render_scaffold(  # noqa: PLR0913 — public scaffold options remain keyword-only
    kind: str,
    destination: Path,
    data: Mapping[str, object],
    *,
    framework: Framework | None = Framework.PYTHON,
    member: bool = False,
    record_answers: bool = True,
) -> ScaffoldResult:
    """Create a scaffold in a missing/empty directory; never update user files.

    Installed providers take precedence. Built-in kinds retain their historical
    template-chain fallback. Templates are trusted as with init itself, and may
    execute Copier tasks. This is not transactional: task/render failures may
    leave partial output. Existing learning overlays use their dedicated route.
    Set ``record_answers=False`` to omit engine-generated Copier answer files.
    """
    try:
        with target_root_lock(destination):
            if destination.is_symlink() or (
                destination.exists()
                and (not destination.is_dir() or any(destination.iterdir()))
            ):
                raise ProviderError(f"Scaffold destination is not empty: {destination}")
            request = ScaffoldRequest(
                kind, framework, member, record_answers=record_answers
            )
            provider = load_provider(kind)
            if provider is None:
                try:
                    template_type = TemplateType(kind)
                except ValueError:
                    provider = require_provider(kind)
                    layers = provider.layers(request)
                else:
                    layers = template_chain(template_type, framework, member=member)
            else:
                layers = provider.layers(request)
            _validate_layers(layers)
            result = CopierAdapter().apply_chain(
                list(layers), destination, data, record_answers=record_answers
            )
            if result.success:
                _finalize_provider(provider, request, destination, data)
            return result
    except Exception as exc:
        logging.getLogger(__name__).exception("Scaffold rendering failed")
        return ScaffoldResult(success=False, path=str(destination), message=str(exc))

require_provider(kind)

Require installed domain ownership before executing a domain operation.

Source code in packages/axm-init/src/axm_init/scaffolding.py
Python
def require_provider(kind: str) -> ScaffoldProvider:
    """Require installed domain ownership before executing a domain operation."""
    provider = load_provider(kind)
    if provider is None:
        package = {
            "learning": "axm-learning[scaffold]",
            "experiment": "axm-lab",
            "investigation": "axm-lab",
            "project": "axm-lab",
            "paper": "axm-lab",
        }.get(kind, kind)
        raise ProviderError(
            f"No scaffold provider installed for {kind!r}; install {package} "
            "in the environment running axm-init."
        )
    return provider

requires_toml(check_name, category, weight, fix)

Decorator that loads pyproject.toml and passes data to the check.

If pyproject.toml is missing or unparsable, returns a failure CheckResult immediately — eliminating the repeated null-guard preamble from every check function.

The decorated function receives (project, data) instead of just (project) — where data is the parsed TOML dict.

Parameters:

Name Type Description Default
check_name str

Check result name (e.g. "pyproject.ruff").

required
category str

Category key (e.g. "pyproject").

required
weight int

Points weight for this check.

required
fix str

Fix message for the "not found" failure.

required
Source code in packages/axm-init/src/axm_init/checks/_utils.py
Python
def requires_toml(
    check_name: str,
    category: str,
    weight: int,
    fix: str,
) -> Callable[
    [Callable[[Path, TomlTable], CheckResult]],
    Callable[[Path], CheckResult],
]:
    """Decorator that loads pyproject.toml and passes data to the check.

    If pyproject.toml is missing or unparsable, returns a failure
    ``CheckResult`` immediately — eliminating the repeated null-guard
    preamble from every check function.

    The decorated function receives ``(project, data)`` instead of just
    ``(project)`` — where ``data`` is the parsed TOML dict.

    Args:
        check_name: Check result name (e.g. ``"pyproject.ruff"``).
        category: Category key (e.g. ``"pyproject"``).
        weight: Points weight for this check.
        fix: Fix message for the "not found" failure.
    """

    def decorator(
        fn: Callable[[Path, TomlTable], CheckResult],
    ) -> Callable[[Path], CheckResult]:
        """Wrap a check function with TOML pre-loading."""

        @functools.wraps(fn)
        def wrapper(project: Path) -> CheckResult:
            """Load TOML then delegate to the wrapped check."""
            data = load_toml_with_workspace_fallback(project)
            if data is None:
                return CheckResult(
                    name=check_name,
                    category=category,
                    passed=False,
                    weight=weight,
                    message="pyproject.toml not found or unparsable",
                    details=[],
                    fix=fix,
                )
            return fn(project, data)

        return wrapper

    return decorator

run_rules(path, rules)

Run explicit rules in order, preserving findings and propagating errors.

No discovery, score aggregation, exclusions, or domain interpretation is performed. Callers own their finding types and presentation policy.

Source code in packages/axm-init/src/axm_init/rules.py
Python
def run_rules[T](path: Path, rules: Iterable[Callable[[Path], T]]) -> list[T]:
    """Run explicit rules in order, preserving findings and propagating errors.

    No discovery, score aggregation, exclusions, or domain interpretation is
    performed. Callers own their finding types and presentation policy.
    """
    return [rule(path) for rule in rules]

section(data, key)

Return data[key] if it is a mapping, otherwise an empty mapping.

Helper for safely walking nested TOML structures without leaking object typing through .get() chains.

Source code in packages/axm-init/src/axm_init/checks/_utils.py
Python
def section(data: TomlTable, key: str) -> TomlTable:
    """Return ``data[key]`` if it is a mapping, otherwise an empty mapping.

    Helper for safely walking nested TOML structures without leaking
    ``object`` typing through ``.get()`` chains.
    """
    value = data.get(key)
    if isinstance(value, Mapping):
        return value
    return {}

table_at(container, key)

Return the table at key, creating an empty one when absent.

Parameters:

Name Type Description Default
container TomlContainer

The document or table to read the branch from.

required
key str

The key holding the nested table.

required

Returns:

Type Description
Table

The existing table, or the freshly created and attached one.

Raises:

Type Description
ValueError

When key already holds a non-table value.

Source code in packages/axm-init/src/axm_init/core/toml_edit.py
Python
def table_at(container: TomlContainer, key: str) -> Table:
    """Return the table at *key*, creating an empty one when absent.

    Args:
        container: The document or table to read the branch from.
        key: The key holding the nested table.

    Returns:
        The existing table, or the freshly created and attached one.

    Raises:
        ValueError: When *key* already holds a non-table value.
    """
    value = container.get(key)
    if value is None:
        created = table()
        container[key] = created
        return created
    if not isinstance(value, Table):
        msg = f"{key!r} must be a TOML table"
        raise ValueError(msg)
    return value

target_root_lock(root)

Hold the process-wide lock guarding writes on the canonical root.

Parameters:

Name Type Description Default
root Path

The target root, resolved before lookup so that distinct spellings of one directory share a single lock.

required

Yields:

Type Description
None

None, while the caller owns the root exclusively.

Source code in packages/axm-init/src/axm_init/core/root_lock.py
Python
@contextmanager
def target_root_lock(root: Path) -> Iterator[None]:
    """Hold the process-wide lock guarding writes on the canonical *root*.

    Args:
        root: The target root, resolved before lookup so that distinct
            spellings of one directory share a single lock.

    Yields:
        ``None``, while the caller owns the root exclusively.
    """
    canonical_root = root.resolve()
    with _ROOT_LOCKS_GUARD:
        entry = _ROOT_LOCKS.get(canonical_root)
        if entry is None:
            entry = _RootLockEntry(lock=_thread.RLock())
            _ROOT_LOCKS[canonical_root] = entry
        entry.users += 1
    entry.lock.acquire()
    try:
        yield
    finally:
        entry.lock.release()
        with _ROOT_LOCKS_GUARD:
            entry.users -= 1
            if entry.users == 0:
                _ROOT_LOCKS.pop(canonical_root, None)

template_chain(template_type, framework, *, member, existing=False)

Resolve the ordered Copier layers for a scaffold request.

Source code in packages/axm-init/src/axm_init/core/templates.py
Python
def template_chain(
    template_type: TemplateType,
    framework: Framework | None,
    *,
    member: bool,
    existing: bool = False,
) -> tuple[TemplateLayer, ...]:
    """Resolve the ordered Copier layers for a scaffold request."""
    if template_type in (TemplateType.LEARNING, TemplateType.EXPERIMENT):
        from axm_init.scaffolding import ScaffoldRequest, _provider_layers

        return _provider_layers(
            ScaffoldRequest(template_type.value, framework, member, existing)
        )
    return (
        TemplateLayer(
            name=template_type.value,
            path=get_template_path(template_type, framework),
            data={},
        ),
    )