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