"""Persistent copy flags shared by page splitting, margin review and the GUI.""" from pathlib import Path from copienator import CliError, EvaluationWorkspace, atomic_update_json, read_json def _path(workspace: EvaluationWorkspace) -> Path: return workspace.metadata_dir / "copy_errors.json" def _validate(value) -> dict[str, str]: if not isinstance(value, dict) or any( not isinstance(name, str) or not isinstance(reason, str) or "/" in name or "\\" in name or Path(name).suffix.casefold() != ".pdf" for name, reason in value.items() ): raise CliError("Invalid copy_errors.json: expected PDF filenames and error descriptions") return value def copy_errors(workspace: EvaluationWorkspace) -> dict[str, str]: return _validate(read_json(_path(workspace), default={})) def mark_copy_error(workspace: EvaluationWorkspace, pdf: Path, reason: str) -> None: def update(errors): _validate(errors)[pdf.name] = reason atomic_update_json(_path(workspace), update, default_factory=dict) def clear_copy_error(workspace: EvaluationWorkspace, pdf: Path) -> None: if not _path(workspace).exists(): return def update(errors): _validate(errors).pop(pdf.name, None) atomic_update_json(_path(workspace), update, default_factory=dict) def marked_copy_paths(workspace: EvaluationWorkspace, *, originals: bool = False) -> list[Path]: directories = ([workspace.original_copies_dir, workspace.copies_dir, workspace.root] if originals else [workspace.copies_dir]) result = [] for name in sorted(copy_errors(workspace), key=str.casefold): path = next((directory / name for directory in directories if (directory / name).is_file()), None) if path is None: raise CliError(f"Marked copy not found: {name}") result.append(path) return result