from pathlib import Path from config import STORAGE_DIR, PROJECTS_DIR, PROJECT_ROOT TOPICS_DIR = STORAGE_DIR / "topics" def _safe(name: str) -> str: return name.replace("/", "_").replace("\x00", "") def topic_dir(topic: str) -> Path: return TOPICS_DIR / _safe(topic) def arbeit_dir(topic: str) -> Path: return topic_dir(topic) / "arbeit" def blocks_path(topic: str) -> Path: return topic_dir(topic) / "blocks.md" def subblocks_path(topic: str) -> Path: """Sidecar: the subblocks with level per block (shared by all guides).""" return topic_dir(topic) / "subblocks.json" def question_pattern_path(topic: str) -> Path: """Sidecar: predefined question patterns per block (subblock × type → example question).""" return topic_dir(topic) / "question_pattern.json" def source_path(topic: str) -> Path: """Persisted source choice per topic: {type, location, spec}.""" return topic_dir(topic) / "source.json" def source_crawl_dir(topic: str) -> Path: """Target folder for crawled link sources (pages + PDF .txt).""" return topic_dir(topic) / "source" def safe_folder(location: str) -> Path | None: """Folder path relative to the repo root, sandboxed. None if empty/escaping (../, absolute outside).""" if not location or not location.strip(): return None p = (PROJECT_ROOT / location.strip()).resolve() try: p.relative_to(PROJECT_ROOT) except ValueError: return None return p def guide_content_path(topic: str, format_name: str) -> Path: return topic_dir(topic) / "guides" / f"{format_name}.json" def blocks_topics() -> list[str]: """Topics for which a topic folder exists.""" if not TOPICS_DIR.is_dir(): return [] return [d.name for d in TOPICS_DIR.iterdir() if d.is_dir()] def project_dir(name: str) -> Path: return PROJECTS_DIR / name