#!/usr/bin/env python3 """Walidator konformancji OKF v0.1 (§9) dla tego bundle'a. Sprawdza: 1. Każdy nie-zarezerwowany plik .md ma parsowalny blok frontmattera YAML. 2. Każdy frontmatter ma niepuste pole `type`. 3. Pliki zarezerwowane (index.md, log.md) nie mają frontmattera — z jedynym wyjątkiem: root index.md może mieć wyłącznie `okf_version` (§11). Tylko biblioteka standardowa: minimalny parser YAML wystarczający dla frontmatterów w tym bundle'u (klucze skalarne, listy inline, listy blokowe). Uruchomienie: python3 check_okf.py [katalog-bundle'a] """ from __future__ import annotations import sys from pathlib import Path RESERVED = {"index.md", "log.md"} def split_frontmatter(text: str) -> tuple[str | None, str]: """Zwraca (blok frontmattera lub None, reszta dokumentu).""" if not text.startswith("---\n"): return None, text end = text.find("\n---", 3) if end == -1: raise ValueError("blok frontmattera otwarty '---', ale nigdy nie zamknięty") return text[4:end], text[end + 4 :] def parse_yaml(block: str) -> dict[str, object]: """Minimalny parser mapy YAML najwyższego poziomu.""" out: dict[str, object] = {} key: str | None = None for lineno, raw in enumerate(block.splitlines(), start=2): line = raw.rstrip() if not line.strip() or line.lstrip().startswith("#"): continue if line.startswith((" ", "\t")): item = line.strip() if not item.startswith("- "): raise ValueError(f"linia {lineno}: nieoczekiwane wcięcie: {line!r}") if key is None: raise ValueError(f"linia {lineno}: element listy bez klucza") out.setdefault(key, []) if not isinstance(out[key], list): out[key] = [] out[key].append(scalar(item[2:])) # type: ignore[union-attr] continue if ":" not in line: raise ValueError(f"linia {lineno}: brak ':' w {line!r}") key, _, value = line.partition(":") key = key.strip() if not key: raise ValueError(f"linia {lineno}: pusty klucz w {line!r}") out[key] = scalar(value.strip()) return out def scalar(value: str) -> object: value = value.strip() if not value: return "" if value.startswith("[") and value.endswith("]"): inner = value[1:-1].strip() return [scalar(v) for v in inner.split(",")] if inner else [] if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'": return value[1:-1] return value def check(root: Path) -> list[str]: errors: list[str] = [] files = sorted(p for p in root.rglob("*.md") if ".git" not in p.parts) if not files: return [f"brak plików .md w {root}"] for path in files: rel = path.relative_to(root).as_posix() text = path.read_text(encoding="utf-8") try: block, _ = split_frontmatter(text) except ValueError as exc: errors.append(f"{rel}: {exc}") continue if path.name in RESERVED: is_root_index = rel == "index.md" if block is None: continue if not is_root_index: errors.append( f"{rel}: plik zarezerwowany nie może mieć frontmattera (§6/§11)" ) continue try: fm = parse_yaml(block) except ValueError as exc: errors.append(f"{rel}: niepoprawny YAML we frontmatterze — {exc}") continue extra = set(fm) - {"okf_version"} if extra: errors.append( f"{rel}: root index.md może mieć wyłącznie okf_version, " f"znaleziono też: {', '.join(sorted(extra))} (§11)" ) elif not str(fm.get("okf_version", "")).strip(): errors.append(f"{rel}: puste okf_version (§11)") continue if block is None: errors.append(f"{rel}: brak bloku frontmattera YAML (§9.1)") continue try: fm = parse_yaml(block) except ValueError as exc: errors.append(f"{rel}: niepoprawny YAML we frontmatterze — {exc}") continue if not str(fm.get("type", "")).strip(): errors.append(f"{rel}: brak niepustego pola `type` (§9.2)") return errors def main() -> int: root = Path(sys.argv[1] if len(sys.argv) > 1 else Path(__file__).parent).resolve() files = sorted(p for p in root.rglob("*.md") if ".git" not in p.parts) errors = check(root) print(f"Bundle: {root}") print(f"Sprawdzono plików .md: {len(files)}") concepts = [p for p in files if p.name not in RESERVED] reserved = [p for p in files if p.name in RESERVED] print(f" koncepty: {len(concepts)}, zarezerwowane: {len(reserved)}") print() if errors: print(f"NIEZGODNE z OKF v0.1 — {len(errors)} problem(ów):") for err in errors: print(f" ✗ {err}") return 1 print("ZGODNE z OKF v0.1 (§9): frontmatter parsowalny, `type` niepusty,") print("pliki zarezerwowane bez frontmattera (poza okf_version w root index.md).") return 0 if __name__ == "__main__": raise SystemExit(main())