textmachine/eval/conformance.py

119 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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