narty-2027/check_okf.py

152 lines
5.3 KiB
Python
Raw Permalink Normal View History

#!/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())