#!/usr/bin/env python3 """СВЕРКА С БУКВОЙ ЗАКАЗА. $0. Инструмент зоны `eval/`, не одного пака. Зачем он есть. Эксп-22 сдал отчёт, у которого ВСЕ числа сходились с сырьём (два гейта, 85 проверок, пере-счёт двумя путями) — и при этом ШЕСТЬ требований промта не были выполнены. Числа и заказ — разные классы отказа, и прибор для первого не ловит второй. Все шесть пропусков стояли не в нумерованных блоках фаз, а в придаточных, и «критик полноты», написанный тем же, кто вёл пак, их не поймал: он проверял по МОДЕЛИ заказа, а не по заказу. Что делает. Читает из markdown-документа таблицу требований (маркер ``) и по КАЖДОЙ строке проверяет улику исполнения. Улика бывает двух видов: `$ <команда>` — команда должна завершиться кодом 0 (только $0-команды; сеть и покупки здесь неуместны и не предусмотрены); `<путь>` — артефакт должен существовать и быть непустым. Улика `—` означает «улики нет»: в режиме плана это законно (требование ещё не исполнено), в режиме сдачи — отказ. Режимы: --plan требование без улики = ОЖИДАЕТ (код 0, если ни одна улика не ПАДАЕТ); --final требование без улики = ПРОВАЛ (код ≠0). Гонять перед сдачей пака. Запуск: eval/.venv/bin/python eval/conformance.py <файл.md> [--plan|--final] ⚠ Границы инструмента, чтобы клейм не был шире прибора. Он проверяет, что у требования ЕСТЬ проходящая улика, а не что улика доказывает именно это требование: связь «требование ↔ улика» устанавливает автор таблицы, и подменить её ничего не мешает. Это сдвигает работу с «вспомнить всё из прозы» на «объявить улику заранее» — но не отменяет чтения. """ from __future__ import annotations import subprocess import sys from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") MARK = "" def rows(md: str) -> list[tuple[str, str, str]]: """Строки таблицы требований: (ID, требование, улика).""" if MARK not in md: raise SystemExit(f"в документе нет маркера {MARK} — реестр требований не объявлен") tail = md.split(MARK, 1)[1] out = [] for line in tail.splitlines(): s = line.strip() if not s.startswith("|"): if out: # таблица кончилась break continue cells = [c.strip() for c in s.strip("|").split("|")] if len(cells) < 3 or set(cells[0]) <= set("-: "): continue if cells[0].upper() in ("ID", "№"): continue out.append((cells[0], cells[1], cells[2])) if not out: raise SystemExit("реестр требований пуст") return out def check(ev: str) -> tuple[str, str]: """Вернуть (статус, деталь). Статусы: OK · ПРОВАЛ · ОЖИДАЕТ.""" ev = ev.strip().strip("`") if ev in ("—", "-", ""): return "ОЖИДАЕТ", "улика не объявлена" if ev.startswith("$"): cmd = ev[1:].strip() # ⚠ Улика не имеет права звать этот же инструмент в режиме сверки: реестр, чьей уликой # служит проверка самого реестра, уходит в бесконечную рекурсию. Поймано первым прогоном. if "conformance.py" in cmd and ("--plan" in cmd or "--final" in cmd): return "ПРОВАЛ", "улика рекурсивна: зовёт conformance.py в режиме сверки" try: r = subprocess.run(cmd, shell=True, cwd=REPO, capture_output=True, # noqa: S602 text=True, timeout=1800) except subprocess.TimeoutExpired: return "ПРОВАЛ", "таймаут" if r.returncode == 0: return "OK", "" return "ПРОВАЛ", (r.stderr or r.stdout).strip().splitlines()[-1][:120] if ( r.stderr or r.stdout).strip() else f"код {r.returncode}" p = (REPO / ev) if not ev.startswith("/") else Path(ev) if p.is_dir(): return ("OK", "") if any(p.iterdir()) else ("ПРОВАЛ", "каталог пуст") if p.exists() and p.stat().st_size > 0: return "OK", "" return "ПРОВАЛ", "артефакта нет или он пуст" def main() -> int: args = [a for a in sys.argv[1:] if a] if not args: print(__doc__) return 1 mode = "--final" if "--final" in args else "--plan" src = next((a for a in args if not a.startswith("--")), None) if not src: print("не указан файл") return 1 md = (REPO / src if not src.startswith("/") else Path(src)).read_text(encoding="utf-8") bad = pend = 0 print(f"СВЕРКА С БУКВОЙ ЗАКАЗА: {src} [{mode[2:]}]\n") for rid, req, ev in rows(md): st, detail = check(ev) if st == "ОЖИДАЕТ" and mode == "--final": st = "ПРОВАЛ" detail = "улики нет, а пак сдаётся" bad += st == "ПРОВАЛ" pend += st == "ОЖИДАЕТ" print(f"[{st:8s}] {rid:6s} {req[:78]}" + (f" ← {detail}" if detail else "")) total = len(rows(md)) print(f"\nтребований {total} · ожидают {pend} · провалов {bad}") if bad: print("СВЕРКА НЕ ПРОЙДЕНА") return 1 if bad else 0 if __name__ == "__main__": sys.exit(main())