119 lines
6.6 KiB
Python
119 lines
6.6 KiB
Python
#!/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())
|