textmachine/docs/scripts/counts.py

160 lines
7.4 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
"""Печатает производные числа доков, которые до сих пор переписывались руками.
Заведён после верификации записей №15 (D39.112 п.4): за одну сессию четыре из десяти
рукописных копий производных чисел разошлись с источником. Норма D39.101 требует считать
очередь «только скриптом» — скрипта при этом не существовало, и норма исполнялась глазами.
Числа, которые печатает этот файл, в доках писать литералами больше не надо: строка носителя
несёт команду. Кто хочет литерал — обязан сверить его этой командой в том же касании.
Использование: python3 docs/scripts/counts.py [--check]
без флага — печатает числа для вставки в доки
--check — сверяет литералы, найденные в доках, с пере-счётом; ненулевой код = расхождение
"""
from __future__ import annotations
import re
import sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PROGRESS = ROOT / "docs" / "PROGRESS.md"
DLOG = ROOT / "docs" / "architecture" / "05-decisions-log.md"
REGISTER = ROOT / "platform" / "docs" / "DEFECT_REGISTER.md"
# Строка таблицы бэклога: «| 145 | ...» или «| 13а | ...». Заголовки-разделители («| **— ... —** |»)
# и шапка таблицы под этот вид не подходят и потому не считаются.
BACKLOG_ROW = re.compile(r"^\| (\d+[а-яё]?) \|")
REGISTER_ROW = re.compile(r"^\| (PD-\d+) \|")
NOTE = re.compile(r"^## (D39\.\d+)")
def cells(line: str) -> list[str]:
return [c.strip() for c in line.split("|")]
def backlog() -> dict:
rows = [l for l in PROGRESS.read_text(encoding="utf-8").splitlines() if BACKLOG_ROW.match(l)]
ids = [BACKLOG_ROW.match(l).group(1) for l in rows]
dupes = [i for i, n in Counter(ids).items() if n > 1]
strict = sum(1 for l in rows if cells(l)[3] == "бэкенд")
wide = sum(1 for l in rows if "бэкенд" in cells(l)[3])
soon = [BACKLOG_ROW.match(l).group(1) for l in rows if "скоро" in cells(l)[4]]
blockers = sum(1 for l in rows if "блокер-очереди" in cells(l)[4])
return {
"всего": len(rows),
"бэкенд строго": strict,
"бэкенд широко": wide,
"скоро": len(soon),
"скоро поимённо": soon,
"блокеров очереди": blockers,
"дубли ID": dupes,
}
def register() -> dict:
rows = [l for l in REGISTER.read_text(encoding="utf-8").splitlines() if REGISTER_ROW.match(l)]
weight = Counter()
status = Counter()
open_ids = []
for l in rows:
c = cells(l)
st, sev = c[6], c[3].replace("*", "")
if st == "open":
status["open"] += 1
open_ids.append(c[1])
# Вес читается по вхождению слова: колонка часто несёт оговорку («major (для промта…)»).
weight["major" if "major" in sev else "minor" if "minor" in sev else "info" if "info" in sev else sev] += 1
elif st.startswith("fixed"):
status["fixed"] += 1
elif st.startswith("accepted-risk"):
status["accepted-risk"] += 1
else:
status["иное: " + st[:32]] += 1
return {
"всего строк": len(rows),
"по статусу": dict(status),
"вес открытых": dict(weight),
"открытые": open_ids,
}
def head() -> dict:
"""Голова = последняя нота D-лога; она же обязана стоять в шапке CURRENT-STATE."""
notes = [NOTE.match(l).group(1) for l in DLOG.read_text(encoding="utf-8").splitlines() if NOTE.match(l)]
last = notes[-1] if notes else None
first = PROGRESS.read_text(encoding="utf-8").splitlines()[:6]
claimed = None
for l in first:
m = re.search(r"голова (D39\.\d+)", l)
if m:
claimed = m.group(1)
break
banner = re.search(r"D1(D39\.\d+)", DLOG.read_text(encoding="utf-8").splitlines()[0])
return {
"последняя нота D-лога": last,
"голова в CURRENT-STATE": claimed,
"шапка D-лога": banner.group(1) if banner else None,
"сходится": last == claimed == (banner.group(1) if banner else None),
}
def main() -> int:
b, r, h = backlog(), register(), head()
check = "--check" in sys.argv
print("ГОЛОВА")
for k, v in h.items():
print(f" {k}: {v}")
print("\nБЭКЛОГ (docs/PROGRESS.md)")
for k, v in b.items():
print(f" {k}: {v if k != 'скоро поимённо' else '·'.join(v)}")
print("\nРЕГИСТР ПЛАТФОРМЫ (platform/docs/DEFECT_REGISTER.md)")
for k, v in r.items():
print(f" {k}: {v if k != 'открытые' else ' '.join(v)}")
if not check:
return 0
bad = []
if not h["сходится"]:
bad.append(
f"голова разошлась: последняя нота {h['последняя нота D-лога']}, "
f"CURRENT-STATE {h['голова в CURRENT-STATE']}, шапка D-лога {h['шапка D-лога']}"
)
if b["дубли ID"]:
bad.append(f"дубли ID строк бэклога: {b['дубли ID']}")
# Литералы в доках: ищем только те формы, которые доки реально используют.
prog = PROGRESS.read_text(encoding="utf-8")
m = re.search(r"всего \*\*(\d+)\*\* строк", prog)
if m and int(m.group(1)) != b["всего"]:
bad.append(f"счёт очереди в CURRENT-STATE {m.group(1)} против пере-счёта {b['всего']}")
m = re.search(r"зона бэкенд \*\*(\d+)\*\* строго .*?/ \*\*(\d+)\*\* широко", prog)
if m and (int(m.group(1)), int(m.group(2))) != (b["бэкенд строго"], b["бэкенд широко"]):
bad.append(
f"счёт зоны бэкенд {m.group(1)}/{m.group(2)} против пере-счёта "
f"{b['бэкенд строго']}/{b['бэкенд широко']}"
)
for path in (PROGRESS, ROOT / "platform" / "docs" / "platform-PROGRESS.md"):
text = path.read_text(encoding="utf-8")
for mm in re.finditer(r"\*\*(\d+) major\*\*", text):
if int(mm.group(1)) != r["вес открытых"].get("major", 0):
bad.append(
f"{path.name}: «{mm.group(1)} major» против пере-счёта "
f"{r['вес открытых'].get('major', 0)}"
)
if bad:
print("\nРАСХОЖДЕНИЯ:")
for line in bad:
print("" + line)
return 1
print("\nЛитералы в доках сходятся с пере-счётом.")
return 0
if __name__ == "__main__":
sys.exit(main())