"""Note path grammar — mirrors KD-5a.0 (reject escape / non-note extensions).""" from __future__ import annotations import pathlib _NOTE_SUFFIXES = frozenset({".md", ".markdown", ".mdx"}) class PathGrammarError(Exception): """Raised when a vault note path fails grammar validation.""" def __init__(self, reason: str, detail: str = "") -> None: self.reason = reason self.detail = detail super().__init__(detail or reason) def validate_note_path(path: str) -> str: """Normalise and validate a vault-relative note path.""" if not path or not isinstance(path, str): raise PathGrammarError("PATH_INVALID", "path is required") raw = path.strip().replace("\\", "/").lstrip("/") if raw.startswith("/") or ".." in raw.split("/"): raise PathGrammarError("PATH_INVALID", "path escape rejected") suffix = pathlib.Path(raw).suffix.lower() if suffix not in _NOTE_SUFFIXES: raise PathGrammarError("NOT_A_NOTE", "not a note path") return raw