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