analysis: flow_stats.py и эталон метрик dx-flow — журнал самодостаточен для демона-аналитика
This commit is contained in:
parent
0683962cd2
commit
075f063ce4
|
|
@ -0,0 +1,125 @@
|
|||
# Конвейер разработки на агентах: этап → исполнение → проверки (dx-flow)
|
||||
|
||||
> ЖИВАЯ карточка процесса: совершенствуется ретроспективами (dx-retro-1),
|
||||
> оценки приёмов пересматриваются по новой фактуре, дописывается
|
||||
> доказательной базой следующих проектов. Первая доказательная база — Kit.RAG
|
||||
> (2026-10-30…11-01: 3 дня, 90 коммитов, 39 workflow-прогонов ~0,7 млрд
|
||||
> токенов, ~26 ревизий; гейт 104→607 монотонно, находки ревизий 53→1–2
|
||||
> low, ускорение модуль к модулю ×3–5). См. [readme.md](../../readme.md).
|
||||
|
||||
## Цикл этапа (проверен на 3 модулях)
|
||||
|
||||
```
|
||||
ТЗ этапа (/stage: «Среда и гейты» B1–B8 + RAG-темы с якорями)
|
||||
→ ИСПОЛНЕНИЕ: субагенты-workflow параллельно по зонам (или агент напрямую)
|
||||
→ ИНТЕГРАТОР: полный гейт (build+test решения), точечные — у исполнителей
|
||||
→ РЕВИЗИЯ СВЕЖИМ КОНТЕКСТОМ: независимый агент без контекста исполнителя;
|
||||
сверяет код↔канон, якоря перепроверяет живым запросом (dx-dev-8),
|
||||
числа пересчитывает сам (руками/своим парсером)
|
||||
→ ПРАВКИ по находкам (применить/отклонить с обоснованием)
|
||||
→ КОНТРОЛЬНЫЙ ГЕЙТ (правки ничего не сломали)
|
||||
→ КАСКАД статусов (одно место: readme модуля; plan — строка; handoff —
|
||||
хвосты/грабли) → GIT-КОММИТ (рецепт — в коммите этапа, B5/D3)
|
||||
```
|
||||
|
||||
## Роли проверок
|
||||
|
||||
| Роль | Что проверяет | Ключевое свойство |
|
||||
|---|---|---|
|
||||
| Исполнитель | свою зону (точечные тесты) | не видит чужих зон |
|
||||
| Интегратор | сборка целиком, швы (DI/slnx/фикстуры — dx-dev-3) | единственный полный гейт |
|
||||
| Ревизор («проверка разработчиком со стороны» + «архитектором») | код↔канон, псевдо-цитаты, якоря, числа | СВЕЖИЙ контекст — главный источник качества; ловил high, невидимые исполнителю |
|
||||
| Главная сессия | эскалации/развилки, инциденты вне зон | решает владелец |
|
||||
|
||||
Архитектурная проверка решений = протокол dx-dev-1 (до утверждения) +
|
||||
ревизия шага 0 (module_reqs/спеки как документы, B7).
|
||||
|
||||
## Приёмы (оценки по ретроспективе Kit.RAG; пересматриваются)
|
||||
|
||||
| Приём | Оценка | Комментарий |
|
||||
|---|---|---|
|
||||
| Ревизия свежим контекстом + контрольный гейт | ядро, не трогать | находки 53→1–2 low; ловила вне-зонные правки, псевдо-цитаты |
|
||||
| Авточемод с эскалациями на развилках | высокоэффективно | темп ×3–5 без потери контуров контроля; вводить ПОСЛЕ обкатки конвейера |
|
||||
| Workflow-исполнители этапов | высокоэффективно | цена: капы/redo-прогоны (6/39), кэш-переигрывания — лечится маркером «ТЗ vN» |
|
||||
| Honest-skip вместо молчаливого пропуска | высоко | все skip'ы позже сняты живыми прогонами |
|
||||
| TCP-стабы внешних сервисов + детерминированные векторы | высоко | приёмочные стабильны и доказательны, не зависят от живой LLM |
|
||||
| docChanges-реестр → легализация одним Изм. на замыкании | высоко (клапан) | симптом дрейфа канона — следить за возрастом |
|
||||
| Метрики замыкания с независимым пересчётом | высоко | «не замерено честно» — списком, не замалчивать |
|
||||
| Повторные полные гейты ×3–4 на этап | условно | после введения файл-лока основания нет — точечные + контрольный |
|
||||
| Дублирование статусов в 3+ документах | НЕ окупилось | ~68% документационных коммитов; одно место + ссылки |
|
||||
|
||||
## Анти-паттерны (пережито, не повторять)
|
||||
|
||||
- Канон утверждается до схождения решений → смена курса после начала
|
||||
кода (Изм. 5: переделка этапа, 53 находки). Лечение: dx-dev-1 до
|
||||
утверждения.
|
||||
- Вне-процессные правки «по ходу» с самовольной атрибуцией → откат,
|
||||
инцидент. Лечение: изменения — через записи канона/эскалации.
|
||||
- Закрытый этап «доезжает» потом дважды. Лечение: замыкание = контрольный
|
||||
гейт + хвосты в handoff, не «потом».
|
||||
|
||||
## Открытые хвосты (совершенствуется)
|
||||
|
||||
- Страж-навык d-proc-guard (авто-проверка чек-листа при генерации ТЗ) —
|
||||
решение на замыкании И4 по метрикам.
|
||||
- Фильтр `--tag env:*` для search — среда из текста запроса в фильтр.
|
||||
- Каноническая нарезка (чанк = законченная единица) — в очереди
|
||||
легализации; снимет и недетерминизм LLM-нарезки.
|
||||
- Формы исполнения вне workflow (короткие этапы) — накопить фактуру.
|
||||
|
||||
## Scorecard: оценки, по которым совершенствуется процесс
|
||||
|
||||
**Механика влияния на знания.** Замер (замыкание итерации, dx-retro-2)
|
||||
→ светофор 🟢/🟡/🔴 → порог-триггер → изменение КОНКРЕТНОГО артефакта
|
||||
знаний (карточка `_env` / правило dx / приём в dx-flow / строка dx-map /
|
||||
наполнение дерева) → коммит std1-ext с указанием источника-замера →
|
||||
переиндексация корпуса. Если правило соблюдается, а метрика хуже цели —
|
||||
пересматривается ОЦЕНКА правила/приёма (понижается, хвост в «Открытых
|
||||
хвостах»). Правки знаний — только через ретроспективу (владелец в
|
||||
контуре утверждения), никогда «по ходу».
|
||||
|
||||
**Фиксация.** Замер — факт в отчёте замыкания итерации проекта (формат:
|
||||
`итерация | метрика | значение | светофор | триггер | действие`), не
|
||||
редактируется. Светофор и цели — здесь, с датой и источником. Действие —
|
||||
git-коммит std1-ext («ретроспектива ИN, метрика M»). **Версионность** —
|
||||
git: вся история правил и оценок = история коммитов; откат = `git
|
||||
revert` правки знаний с обоснованием; замеры-факты не откатываются;
|
||||
корпус пересобирается из git (индекс — производное, не версионируется).
|
||||
|
||||
### Метрики (база / цель / триггер / текущий светофор 2026-11-02)
|
||||
|
||||
| # | Метрика | База (И1/И2) | Цель | Триггер (порог действия) | Сейчас |
|
||||
|---|---|---|---|---|---|
|
||||
| А1 | Находки ревизии/этап, медиана | ~7 (пик 53) / ~2 | ≤2, high=0 | high на этапе → усилить B-блок ТЗ и фокус ревизии | 🟡 (И3-э1: 2 high) |
|
||||
| А2 | Псевдо-якоря/псевдо-цитаты | 0 / 0 (И3-э1: 2) | 0 | >0 → приём в dx-flow + формулировка dx-dev-8 | 🟡 |
|
||||
| А3 | Дефекты после замыкания этапа | И1: этап доезжал ×2 | 0 | >0 → ужесточить замыкание (контрольный + хвосты в handoff) | 🟡 |
|
||||
| А4 | Повторы известных граблей | И2: 3 (кодировка/лок/байты) | 0 | 1-й повтор → инструмент (dx-retro-3); 2-й → эскалация владельцу | 🟡 (проверка И3: инструменты введены) |
|
||||
| Б1 | Доля документационных коммитов | ~70% / ~56% | ≤40% | >50% две итерации подряд → упрощать каскад (одно место статусов) | 🟡 |
|
||||
| Б2 | Redo-прогоны workflow/этап | 4 / 2 | ≤1 | >2 → стабильность скриптов (маркер «ТЗ vN», прогон сниппетов) | 🟡 |
|
||||
| Б3 | Wall-clock и токены этапа | база И1/И2 зафиксирована | тренд ↓ на однотипных этапах | рост ×2 → разбор причин | 🟢 (ускорение ×3–5) |
|
||||
| В1 | Возврат правил: батарея top-3 | 16/16 | 100% | <100% → чинить корпус/нарезку, не батарею | 🟢 |
|
||||
| В2 | Темы с первого захода | 2/4 до env-фикса | ≥75% | <75% → тюнинг запросов / ускорить `--tag env:*` | 🟡 (контрольный прогон /decision) |
|
||||
| В3 | Дыры корпуса (темы без находок) | механизм заведён | закрытие в ту же итерацию | каждая → кандидат наполнения дерева | 🟢 |
|
||||
| В4 | Возраст docChanges на легализации | до 4 этапов | ≤1 итерации | >1 итерации → выносить легализацию чаще | 🟡 |
|
||||
| Г1 | Honest-skip'ы сняты к замыканию | И1: все сняты живыми | 100% | остаток → в хвосты handoff | 🟢 |
|
||||
| Г2 | Эскалации закрыты владельцем | не замерялось | ≤1 дня | дольше → выносить в повестку явно | 🟢 (эмпирически быстро) |
|
||||
| Г3 | Инциденты среды: новые → карточка | правило заведено | в тот же день | дольше → помнить о dx-retro-3 | 🟢 |
|
||||
|
||||
**Пересмотр целей** — не чаще каждой второй ретроспективы, коммитом с
|
||||
обоснованием. Добавление метрики — через ретроспективу (какие решения
|
||||
она изменит? не изменяет — не заводить).
|
||||
|
||||
### История светофоров (полная — git log карточки)
|
||||
|
||||
- 2026-11-02 — базовая фиксация: А1–А4/Б1/Б2/В2/В4 🟡, Б3/В1/В3/Г* 🟢;
|
||||
источники — ретроспектива Kit.RAG (И1/И2) и живой прогон /decision.
|
||||
|
||||
## Правило обновления карточки
|
||||
|
||||
Ретроспектива проекта (dx-retro-1) дописывает: новые приёмы с оценкой и
|
||||
прецедентом-числом, пересматривает существующие оценки, закрывает хвосты,
|
||||
обновляет светофоры со ссылкой на замер. Числа обязательны
|
||||
(«быстрее/лучше» без чисел не принимается, dx-retro-2). Коммит в
|
||||
std1-ext с указанием источника-ретроспективы + переиндексация корпуса.
|
||||
Откат оценки/правила — git revert с обоснованием; замеры-факты
|
||||
последующих итераций не откатываются.
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
# -*- 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()
|
||||
Loading…
Reference in New Issue