from __future__ import annotations from dataclasses import dataclass from pathlib import Path @dataclass(frozen=True) class PathConfig: project_root: Path pdf_root: Path database: Path publish_database: Path tmp_dir: Path text_dir: Path log_dir: Path papers_org: Path @dataclass(frozen=True) class RemoteConfig: host: str database_dir: str pdf_dir: str @dataclass(frozen=True) class AIConfig: backend: str claude_permission_mode: str claude_allowed_tools: tuple[str, ...] codex_model: str | None codex_sandbox: str codex_full_auto: bool @property def claude_allowed_tools_argument(self) -> str: return ",".join(self.claude_allowed_tools) @dataclass(frozen=True) class ProcessingConfig: scan_directories: tuple[str, ...] ignore_directories: tuple[str, ...] max_papers: int initial_pages: int fallback_pages: int min_text_length: int update_database: bool validate_database: bool export_org: bool backup_database: bool @dataclass(frozen=True) class DomainConfig: name: str description: str categories: tuple[str, ...] default_category: str summary_length_ja: str summary_focus: tuple[str, ...] relevance_criteria: tuple[str, ...] priorities: tuple[str, ...] = ("A", "B", "C", "D") def validate(self) -> None: if not self.name.strip(): raise ValueError("domain.nameが空です") if not self.description.strip(): raise ValueError("domain.descriptionが空です") if not self.categories: raise ValueError("domain.categoriesが空です") if len(set(self.categories)) != len(self.categories): raise ValueError("domain.categoriesに重複があります") if self.default_category not in self.categories: raise ValueError( "default_categoryをcategoriesに含めてください" ) if len(self.relevance_criteria) != 6: raise ValueError( "relevance_criteriaは0から5の6項目にしてください" ) def categories_prompt(self) -> str: return "\n".join( f"- {category}" for category in self.categories ) def summary_focus_prompt(self) -> str: return "\n".join( f"- {item}" for item in self.summary_focus ) def relevance_prompt(self) -> str: return "\n".join( f"- {score}: {description}" for score, description in enumerate( self.relevance_criteria ) ) @dataclass(frozen=True) class Config: path: PathConfig remote: RemoteConfig ai: AIConfig processing: ProcessingConfig domain: DomainConfig @property def scan_paths(self) -> tuple[Path, ...]: return tuple( self.path.pdf_root / directory for directory in self.processing.scan_directories ) def validate(self, *, require_remote: bool = False) -> None: root = self.path.project_root.resolve() if not root.is_dir(): raise NotADirectoryError(root) if not self.path.pdf_root.is_dir(): raise NotADirectoryError( f"PDF_ROOTがありません: {self.path.pdf_root}" ) if not self.processing.scan_directories: raise ValueError("scan_directoriesが空です") for path in self.scan_paths: if not path.is_dir(): raise NotADirectoryError( f"探索ディレクトリがありません: {path}" ) if self.processing.max_papers < 1: raise ValueError("max_papersは1以上にしてください") if self.processing.initial_pages < 1: raise ValueError("initial_pagesは1以上にしてください") if ( self.processing.fallback_pages < self.processing.initial_pages ): raise ValueError( "fallback_pagesはinitial_pages以上にしてください" ) if self.processing.min_text_length < 0: raise ValueError( "min_text_lengthは0以上にしてください" ) if self.ai.backend.lower() not in {"claude", "codex"}: raise ValueError( "ai.backendはclaudeまたはcodexにしてください" ) self.domain.validate() if require_remote: values = ( self.remote.host, self.remote.database_dir, self.remote.pdf_dir, ) if any(not value.strip() for value in values): raise ValueError("公開サーバ設定が不足しています")