analysis: flow_stats.py и эталон метрик dx-flow — журнал самодостаточен для демона-аналитика

This commit is contained in:
Витковских Евгений 2026-11-05 18:16:09 +05:00
parent 0683962cd2
commit 075f063ce4
2 changed files with 327 additions and 0 deletions

125
dx-flow.md Normal file
View File

@ -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 с обоснованием; замеры-факты
последующих итераций не откатываются.

202
scripts/flow_stats.py Normal file
View File

@ -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()