Source code for exlab_wizard.template.provenance

"""Frozen template provenance copy. Design spec (Phase 3b).

After a template renders into a destination folder, the exact template
source is copied -- verbatim, including ``copier.yml`` and ``.jinja``
files -- into that instance's own typed template store under
``<dst>/.exlab-wizard/templates/<own_type>/<name>/``. This freezes the
provenance so a later reader can see precisely which template produced
the instance, even if the shared templates directory drifts.

The copy path is recorded in ``creation.json`` (the ``template`` block's
``provenance_path``). This module is intentionally pure and dependency
light: it reads only ``resolved.name`` / ``resolved.path`` and performs a
single ``copytree``.
"""

from __future__ import annotations

import shutil
from typing import TYPE_CHECKING

from exlab_wizard.constants import TEMPLATES_SUBDIR
from exlab_wizard.paths import cache_dir

if TYPE_CHECKING:
    from pathlib import Path

    from exlab_wizard.template.copier_driver import ResolvedTemplate

__all__ = ["copy_template_into_instance"]


[docs] def copy_template_into_instance( resolved: ResolvedTemplate, dst: Path, own_type: str, ) -> str: """Copy the resolved template root into ``dst``'s own typed provenance store. Writes ``<dst>/.exlab-wizard/templates/<own_type>/<resolved.name>/`` as a verbatim copy (incl. ``copier.yml`` and ``.jinja`` files). The copy is a frozen snapshot of the exact template source that produced this instance. Args: resolved: The resolved template whose ``name`` / ``path`` (template root directory) are copied. Only these two attributes are read. dst: The instance destination directory the template rendered into. own_type: The instance's own template type segment -- ``"run"`` for a run, ``"project"`` for a project. Returns: The provenance copy's path RELATIVE to ``dst`` as a POSIX string, e.g. ``".exlab-wizard/templates/run/confocal_run"``. Raises: FileNotFoundError: ``resolved.path`` does not exist (should not happen after a successful render). The caller wraps this best-effort. """ source = resolved.path if not source.is_dir(): raise FileNotFoundError( f"template source {source} is missing; cannot copy provenance", ) target = cache_dir(dst) / TEMPLATES_SUBDIR / own_type / resolved.name shutil.copytree(source, target, dirs_exist_ok=True) return target.relative_to(dst).as_posix()