from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING from tht.config import ConfigError if TYPE_CHECKING: from tht.config import Config @dataclass(frozen=True) class ResolvedPaths: workspace: Path sessions: Path artifacts: Path indexes: Path corpus: Path def _resolve_root(path: Path, workspace: Path, name: str) -> Path: if path.is_absolute(): # Existing deployments commonly point at an external workspace checkout. Keep # those paths working while `tht doctor` identifies them for migration. return path resolved = (workspace / path).resolve() if not resolved.is_relative_to(workspace): raise ConfigError(f"paths.{name} resolves outside workspace root") return resolved def resolve_workspace_paths( config_path: Path, cfg: Config, data_root: Path ) -> ResolvedPaths: """Resolve portable paths under ``/workspaces/``. Relative roots are sandboxed to the logical workspace. Absolute roots are a compatibility bridge for existing installations and are never rewritten. """ canonical_data_root = data_root.resolve() workspaces_root = canonical_data_root / "workspaces" workspace = (workspaces_root / config_path.stem).resolve() if not workspace.is_relative_to(workspaces_root): raise ConfigError("workspace resolves outside workspaces root") roots = cfg.roots return ResolvedPaths( workspace=workspace, sessions=_resolve_root(roots.sessions, workspace, "sessions"), artifacts=_resolve_root(roots.artifacts, workspace, "artifacts"), indexes=_resolve_root(roots.indexes, workspace, "indexes"), corpus=_resolve_root(Path("corpus"), workspace, "corpus"), )