docs_paths.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """Living-doc path joins and ``root_relative_docs`` normalization (§K6.5.2).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from pathlib import Path |
| 6 | |
| 7 | from adapters.config import OverseerConfig |
| 8 | from adapters.errors import ConfigError |
| 9 | |
| 10 | |
| 11 | def normalize_docs_root(root_relative_docs: str) -> str: |
| 12 | """Return stripped docs-root; ``\".\"`` is the only repo-root sentinel.""" |
| 13 | return root_relative_docs.strip() |
| 14 | |
| 15 | |
| 16 | def join_docs_rel(root_relative_docs: str, doc_name: str) -> str: |
| 17 | """Join docs root and filename into a repo-relative POSIX path. |
| 18 | |
| 19 | When ``root_relative_docs`` is exactly ``\".\"`` (after strip), return the bare |
| 20 | ``doc_name`` (never ``./name`` or ``/name``). |
| 21 | """ |
| 22 | root = normalize_docs_root(root_relative_docs) |
| 23 | if not root: |
| 24 | raise ConfigError("repo.root_relative_docs must be a non-empty string") |
| 25 | if root == ".": |
| 26 | return doc_name |
| 27 | return f"{root.rstrip('/')}/{doc_name}" |
| 28 | |
| 29 | |
| 30 | def living_doc_destinations(config: OverseerConfig) -> frozenset[str]: |
| 31 | """Return footprint destination paths for configured living docs (all lanes).""" |
| 32 | docs_root = config.repo.root_relative_docs |
| 33 | docs: set[str] = set() |
| 34 | if config.docs.lanes is None: |
| 35 | docs.add(join_docs_rel(docs_root, config.docs.handover)) |
| 36 | docs.add(join_docs_rel(docs_root, config.docs.roadmap)) |
| 37 | else: |
| 38 | for lane in config.docs.lanes.values(): |
| 39 | docs.add(join_docs_rel(docs_root, lane.handover)) |
| 40 | docs.add(join_docs_rel(docs_root, lane.roadmap)) |
| 41 | if config.docs.coordination: |
| 42 | docs.add(join_docs_rel(docs_root, config.docs.coordination)) |
| 43 | return frozenset(docs) |
| 44 | |
| 45 | |
| 46 | def lane_living_doc_abs( |
| 47 | repo_root: Path, |
| 48 | config: OverseerConfig, |
| 49 | lane_docs: LaneDocsConfig, |
| 50 | doc_name: str, |
| 51 | ) -> Path: |
| 52 | """Absolute path to a lane living doc under ``repo_root``.""" |
| 53 | return repo_root / join_docs_rel(config.repo.root_relative_docs, doc_name) |
| 54 | |
| 55 | |
| 56 | def living_doc_abs(repo_root: Path, config: OverseerConfig, doc_name: str) -> Path: |
| 57 | """Absolute path to a living doc under ``repo_root``.""" |
| 58 | return repo_root / join_docs_rel(config.repo.root_relative_docs, doc_name) |
| 59 | |
| 60 | |
| 61 | def validate_muse_working_dir(repo_root: Path, working_dir: str | None) -> Path | None: |
| 62 | """Resolve ``vcs.muse.working_dir`` inside ``repo_root``; raise ``ConfigError`` on escape. |
| 63 | |
| 64 | Returns the absolute Muse cwd, or ``None`` when ``working_dir`` is unset (install root). |
| 65 | Escape / absolute-outside-root → ``ConfigError`` (CLI maps to exit ``2``). |
| 66 | """ |
| 67 | if working_dir is None: |
| 68 | return None |
| 69 | text = working_dir.strip() |
| 70 | if not text: |
| 71 | raise ConfigError("vcs.muse.working_dir must be a non-empty string or null") |
| 72 | candidate = Path(text) |
| 73 | if candidate.is_absolute(): |
| 74 | raise ConfigError("vcs.muse.working_dir must be relative to the install root") |
| 75 | if ".." in candidate.parts: |
| 76 | raise ConfigError("vcs.muse.working_dir must not contain '..' path segments") |
| 77 | root = repo_root.resolve() |
| 78 | resolved = (root / candidate).resolve() |
| 79 | try: |
| 80 | resolved.relative_to(root) |
| 81 | except ValueError as exc: |
| 82 | raise ConfigError("vcs.muse.working_dir escapes install root") from exc |
| 83 | return resolved |