DB.FlowExp_WorkStatistics/scripts/flow_stats.py

203 lines
7.6 KiB
Python
Raw Permalink 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.

# -*- coding: utf-8 -*-
"""Агрегатор журнала опыта kit-dev-flow (общий git-репозиторий статистики).
Читает карточки прогонов runs/**/*.md (YAML-шапка между первыми двумя
строками '---'), считает агрегаты по всем проектам команды и печатает
markdown-отчёт. Точка опыта: --dir > env KIT_FLOW_EXP_DIR >
~/.zcode/kit-knowledge.json (flow_exp_dir) > I:\\Kit\\DB.FlowExp_WorkStatistics.
Примеры:
python flow_stats.py # всё
python flow_stats.py --kind этап-модуля # класс текущего шага
python flow_stats.py --days 30 --last 40
python flow_stats.py --write-summary # обновить _summary.md
"""
import argparse
import datetime as dt
import glob
import json
import os
import sys
DEFAULT_DIR = r"I:\Kit\DB.FlowExp_WorkStatistics"
NUM_FIELDS = ("agents", "revisions", "escalations", "duration_min")
STR_FIELDS = ("date", "project", "step", "kind", "form", "preset",
"model", "result", "run_id")
def resolve_dir(cli_dir):
if cli_dir:
return cli_dir
env = os.environ.get("KIT_FLOW_EXP_DIR")
if env:
return env
cfg_path = os.path.join(os.path.expanduser("~"),
".zcode", "kit-knowledge.json")
try:
with open(cfg_path, encoding="utf-8") as fh:
cfg = json.load(fh)
if cfg.get("flow_exp_dir"):
return cfg["flow_exp_dir"]
except (OSError, ValueError):
pass
return DEFAULT_DIR
def parse_card(path):
"""YAML-шапка карточки: ключ: значение (одна строка на ключ)."""
with open(path, encoding="utf-8") as fh:
lines = fh.read().splitlines()
if not lines or lines[0].strip() != "---":
return None
meta, end = {}, None
for i, line in enumerate(lines[1:], start=1):
s = line.strip()
if s == "---":
end = i
break
if ":" in s and not s.startswith("#"):
key, _, val = s.partition(":")
val = val.split(" #")[0].strip().strip('"').strip("'")
if key.strip() in STR_FIELDS or key.strip() in NUM_FIELDS:
meta[key.strip()] = val
if end is None or not meta.get("date"):
return None
lessons, in_lessons = [], False
for line in lines[end + 1:]:
bs = line.strip()
if bs.startswith("## Уроки"):
in_lessons = True
continue
if in_lessons and bs.startswith("## "):
in_lessons = False
elif in_lessons and bs.startswith("- "):
lessons.append(bs[2:].strip())
meta["_lessons"] = lessons
return meta
def num(meta, field):
try:
return float(meta.get(field, ""))
except (TypeError, ValueError):
return None
def load_cards(root, kind, project, days):
cards = []
for path in glob.glob(os.path.join(root, "runs", "**", "*.md"),
recursive=True):
meta = parse_card(path)
if not meta:
continue
if kind and meta.get("kind") != kind:
continue
if project and meta.get("project") != project:
continue
try:
d = dt.date.fromisoformat(meta["date"][:10])
except ValueError:
d = None
if days and d and (dt.date.today() - d).days > days:
continue
cards.append(meta)
cards.sort(key=lambda m: m.get("date", ""))
return cards
def avg(values):
vals = [v for v in values if v is not None]
return (sum(vals) / len(vals)) if vals else None
def fmt(v, suffix=""):
return "%.1f%s" % (v, suffix) if v is not None else "—"
def group_stats(cards, field):
"""{значение поля: [всего, success]} по карточкам с непустым полем."""
stats = {}
for m in cards:
key = m.get(field) or "—"
s = stats.setdefault(key, [0, 0])
s[0] += 1
if m.get("result") == "success":
s[1] += 1
return stats
def render(cards, title):
out = ["# Статистика kit-dev-flow — " + title, ""]
out.append("Карточек: %d (проектов: %d)" % (
len(cards), len({m.get("project") for m in cards if m.get("project")})))
out.append("")
for field, label in [("form", "форма"), ("preset", "пресет"),
("kind", "класс шага")]:
groups = group_stats(cards, field)
if len(groups) < 2 and field != "form":
continue
out.append("| %s | прогонов | success |" % label)
out.append("|---|---|---|")
for key, (total, ok) in sorted(groups.items(),
key=lambda kv: -kv[1][0]):
out.append("| %s | %d | %d%% |" % (key, total, ok * 100 // total))
out.append("")
out.append("Средние: агентов %s, ревизий %s, эскалаций %s, "
"длительность %s мин." % (
fmt(avg([num(m, "agents") for m in cards])),
fmt(avg([num(m, "revisions") for m in cards])),
fmt(avg([num(m, "escalations") for m in cards])),
fmt(avg([num(m, "duration_min") for m in cards]))))
out.append("")
fails = [m for m in cards if m.get("result") in ("fail", "partial")]
out.append("Провальных/частичных: %d. Уроки из последних:" % len(fails))
shown = 0
for m in reversed(fails):
if shown >= 8:
break
for lesson in m.get("_lessons", []):
out.append("- (%s, %s, %s) %s" % (
m.get("project", "?"), m.get("step", "?"),
m.get("form", "?"), lesson))
shown += 1
if shown >= 8:
break
if not shown:
out.append("- (нет провальных карточек в выборке)")
return "\n".join(out) + "\n"
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--dir", help="локальный клон репозитория опыта")
ap.add_argument("--kind", help="фильтр: класс шага (kind карточки)")
ap.add_argument("--project", help="фильтр: проект (по умолчанию все)")
ap.add_argument("--days", type=int, help="только последние N дней")
ap.add_argument("--last", type=int, help="только последние N карточек")
ap.add_argument("--write-summary", action="store_true",
help="записать отчёт в _summary.md репозитория")
args = ap.parse_args()
root = resolve_dir(args.dir)
if not os.path.isdir(root):
print("ОШИБКА: каталог журнала не найден: %s" % root, file=sys.stderr)
print("Настрой flow_exp_dir в ~/.zcode/kit-knowledge.json "
"или склонируй репозиторий опыта.", file=sys.stderr)
sys.exit(1)
cards = load_cards(root, args.kind, args.project, args.days)
if args.last:
cards = cards[-args.last:]
title = args.kind or ("проект " + args.project if args.project else "все проекты")
report = render(cards, title)
if args.write_summary:
path = os.path.join(root, "_summary.md")
with open(path, "w", encoding="utf-8", newline="\n") as fh:
fh.write(report)
print("Записан отчёт: %s" % path)
print(report)
if __name__ == "__main__":
main()