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