Зодчий — архитектурный аудит с измеримой материальностью
Тезис: материальность измеряется, а не утверждается. Остальные инструменты объявляют «цикл — находка, только если он чего-то стоит», но стоимость взять неоткуда: граф импортов её не содержит. Здесь она берётся из git-истории и служит условием допуска находки в отчёт.
Полный дизайн — SPEC.md. Ниже — как исполнять.
Железное правило
Число — из скрипта. Суждение — из модели. Не наоборот.
Ни одна метрика не оценивается «на глаз»: ни fan-in, ни сложность, ни цикл.
Ни одна находка не выносится до того, как построена полная карта.
Нарушение — брак прогона, а не мелочь: вердикт, вынесенный до карты, тянет за собой весь дальнейший разбор.
Число сопровождается ссылкой. Поле source находки — путь в measure.json
(behavior.hotspots[file=src/x.py].fix_share), а не проза «по данным замера».
Прозу нельзя отличить от числа, названного по памяти; путь проверяется
механически — zodchiy.py selfcheck. Полный перечень путей —
references/measure_schema.md.
Рядом с абсолютным числом — перцентиль. Пороги подобраны на двух
репозиториях и на третьем поплывут. fix_share 0.43 не переносится между
проектами, верхние 7% этого репозитория переносится. Поля *_pct есть у
всех ранжируемых метрик.
Бюджет чтения. Файл читается целиком только после того, как попал в топ по
метрике (hotspots, hubs, complex_files, slowest_files, unstable_files).
На пятисотфайловом репозитории обратный порядок топит контекст, и разбор
скатывается к суждению по именам каталогов — ровно то, что шаг 2 запрещает.
Модель по шагам. Шаг 1 — модель не нужна вовсе, это скрипт. Шаг 2 — механическое чтение, хватает средней. Шаг 4 — сильная: на слабой опровержение превращается в вежливое согласие.
Три оси
| Ось | Откуда | Чего НЕ видит |
|---|---|---|
| Намерение | ADR, CLAUDE.md, README, import-linter/ArchUnit/eslint-boundaries | врёт, когда документы отстали от кода |
| Структура | scripts/structure.py |
связи через DI, реестры, рефлексию, строковые ключи |
| Поведение | scripts/behavior.py |
код, который ещё не менялся |
Допуск находки — по сходимости:
| Осей сошлось | Статус | Судьба |
|---|---|---|
| 1 | hypothesis |
в отчёт не идёт, ждёт следующего прогона |
| 2 | finding |
в отчёт, с указанием недостающей оси |
| 3 | verdict |
первым, годится в основание ADR |
Ось намерения никогда не перебивает исполняемое поведение. Наличие ADR не доказывает, что так и сделано.
Режимы
| Режим | Когда | Справочник |
|---|---|---|
audit |
свой проект: что болит и что чинить | ниже + references/materiality.md |
recon |
чужой/незнакомый репо: как устроен и почему так | references/recon.md |
gate |
CI: не стало ли хуже | references/materiality.md |
plan |
из находок в решения и миграцию | references/remedy.md |
Режим не объявлен — выведи из просьбы и назови одной строкой.
Команда
Один вход, а не четыре скрипта. Дальше по тексту команды пишутся коротко
(zodchiy.py gate), полный путь — python3 ~/.claude/skills/zodchiy/zodchiy.py.
| Команда | Что делает |
|---|---|
measure <repo> --out .zodchiy/measure.json |
обе оси + калибровка; в stdout сводка, JSON в файле |
snapshot <measure> --out .zodchiy/baseline.json |
снимок метрик под сравнение |
diff <measure> --baseline <base> |
что изменилось между прогонами |
gate <measure> --baseline <base> |
то же + exit 1 при регрессии — для CI |
add --findings <csv> --json '{...}' |
дописать находку |
refute --json '{...}' |
вердикт линзы опровержения (шаг 4) |
selfcheck --findings <csv> --measure <json> |
проверка находок перед сдачей |
verify --findings <csv> --measure <json> |
сверить прогноз gain с новым замером |
export --findings <csv> --measure <json> [--format sarif] |
находки машиночитаемо: JSON по схеме или SARIF |
behavior <repo> и structure <repo> гоняют одну ось — для отладки, не для
отчёта: без калибровки числа не годятся в находку.
Деградация
Скилл переносим между харнессами, а фичи харнессов — нет. Каждая деградация объявляется в отчёте. Молчаливая запрещена: она превращает «проверка была» в неправду, и заметить это по отчёту нельзя.
| Чего нет | Что делаем | Чем платим и где это видно |
|---|---|---|
| субагентов | линзы шага 4 прогоняются последовательно, вердикт пишется с mode: sequential |
потолок находки — finding; selfcheck вернёт refutation.ceiling_cap и строку disclosure для «слепых зон» |
прогрессивной загрузки references/ |
справочник читается файлом по пути из таблицы ниже перед шагом, которому он нужен | ничем, если прочитан; по памяти — доктрина расходится с файлом молча |
tree-sitter |
разбор регулярками | parser.backends.regex > 0; такие файлы выпадают из метрик сложности, потолок по ним — finding |
| git-истории | две оси вместо трёх | behavior.available: false, потолок finding (см. references/materiality.md §6) |
Что каждый харнесс читает, куда класть адаптеры и чего у него нет —
references/harnesses.md. Факты там проверены 01.09.2026 по первоисточникам
и протухают: перед сборкой адаптеров сверяются заново, а не по памяти.
Порядок работ — audit
Каждый шаг — пункт в todo. Пропуск шага объявляется вслух с причиной.
1. Считать
python3 ~/.claude/skills/zodchiy/zodchiy.py measure <repo> --out .zodchiy/measure.json
В stdout — сводка, полный JSON в файле. Читай файл прицельно (adjacency,
temporal_coupling, hotspots), а не целиком.
Смотри calibration.passed и confidence.ceiling до всего остального.
Калибровка не прошла — заблокированные метрики не дают находок, и это
проговаривается в отчёте. Потолок finding означает, что verdict в этом
прогоне недостижим в принципе.
Скрипт молчит про то, чего не может: короткая история, regex вместо дерева, несвязный граф — всё выходит явными полями, а не тишиной.
Чекпоинт. calibration.passed == false или потолок ниже ожидаемого —
остановись и спроси, продолжать ли на оставшихся осях. Одна строка в конце
отчёта на сорок находок этого не заменяет: к тому моменту решение уже принято.
2. Понять — карта без вердиктов
Прочитай намерение: CLAUDE.md, README, docs/architecture/*, контракты
слоёв в pyproject.toml / .eslintrc / archunit. Прочитай ключевой код.
Построй карту: слои, модули, потоки, контракты, где чем владеют. Шаблон с
обязательными секциями — references/map_template.md.
Здесь запрещено: называть проблемы, ставить severity, предлагать лечение. Тянет назвать — запиши в черновик находок и вернись к карте.
Не верь именам. Каталог зовётся domain — проверь, что в нём домен.
Функция зовётся validate_* — прочитай тело. Имя не доказывает ничего.
Говори про ненайденное. Границы слоёв ничем не защищены, кроме соглашения —
это утверждение, а не молчание. Помечай: OBSERVED / INFERRED / UNKNOWN.
3. Судить
Только теперь — находки. Числа берутся из measure.json; перечитывать код ради
смысла можно, ради измерения — нет.
Каталог рисков R1–R6 и пороги — references/risks.md.
Гейт материальности и Pain × Spread — references/materiality.md.
Правила расхождения осей — references/axes.md.
Различай R2 и R3 через граф. Пара меняется вместе И связана импортом
(adjacency_through_barrels) — честная зависимость, R2. Меняется вместе БЕЗ
ребра — одно решение разложено в двух местах, R3, другое лечение. Проверять
надо по графу через barrel: from pkg import X даёт ребро в __init__.py,
и наивная проверка объявит связь скрытой, когда она прямая.
Цикл считай только рантаймовый. cycles — настоящие. cycles_type_only —
if TYPE_CHECKING: и import type, они ровно для разрыва цикла и заведены.
Смешать — выдать выдуманный дефект первой строкой.
Сложность сравнивай по функции, не по файлу: cyclomatic_per_function_max,
не cyclomatic_total. Порог McCabe задан на функцию.
Цена считается и во времени. pain × spread — цена в пространстве.
velocity.touch_cost говорит, сколько мест надо тронуть на одно изменение;
velocity.episodes.multi_commit_share — какая доля изменений потребовала
доделок. Это и есть «больно менять», выраженное числом.
rework_rate — не то же, что fix_share. fix_share говорит «файл часто
чинят» (сложное место). stability.rework_rate — «правки этого файла не
держатся» (обратной связи нет: ловит не тест, а пользователь). Диагнозы разные,
лечение разное. Ранжировать по rework_rate_lb, не по сырой доле: «5 из 5»
иначе обгоняет «55 из 67» и первой строкой отчёта идёт шум.
4. Опровергнуть
Каждая находка со статусом verdict и каждая с приоритетом ≥6 идёт на
опровержение — разными линзами, а не копиями одного промпта: N одинаковых
агентов дают одно мнение по цене N. Четыре линзы, что каждая читает и чем
убивает находку — references/refutation.md.
Как физически шли линзы — независимо или одним проходом — объявляется полем
mode, а не подразумевается: см. «Деградация» выше и references/refutation.md.
Вердикт линзы кладётся файлом, а не пересказывается:
zodchiy.py refute --json '{"finding_id":"F1","lens":"L4","verdict":"dropped",
"reason":"...","mode":"parallel"}'
Не устояла — вниз по статусу или прочь.
5. Отчёт
Открывается строкой Чеклист: X/Y. Незакрытые пункты перечисляются с причиной
и следствием — тихий пропуск запрещён.
Порядок: снимок (remote, ветка, HEAD, чистота дерева, дата) → калибровка и потолок → карта → находки по приоритету → слепые зоны.
Секция «слепые зоны» обязательна и собирается механически, не пересказом:
calibration.blocked_metrics + файлы, разобранные регулярками
(parser.backends.regex) + окно истории (snapshot.since) + связи, которых
граф импортов не видит (DI, реестры, рефлексия, строковые ключи) + режим шага 4
(selfcheck → refutation.disclosure, если линзы шли последовательно).
Тихое усечение запрещено. Обрезал список топ-N — скажи, сколько отброшено. Молчание читается как «покрыто всё», и это единственная ошибка отчёта, которую читатель не может заметить.
Перед сдачей: zodchiy.py selfcheck --findings ... --measure ... — поля на
месте, source разрешается, находки высокого приоритета прошли опровержение.
Отчёт — не единственный выход. zodchiy.py export отдаёт те же находки
машиночитаемо: JSON по schema/findings.schema.json либо SARIF для CI и code
scanning. Markdown пишет модель, и его форма зависит от харнесса — сравнивать
прогоны и гейтить сборку можно только по машинному выходу. Экспорт гоняет тот
же selfcheck и отказывается собирать документ из брака.
6. Артефакты
references/artifacts.md: docs/architecture/current-state.md, ADR,
диаграммы, план миграции, .zodchiy/findings.csv, baseline.json,
.zodchiy/verify/refutation.json.
Справочники
| Файл | Когда открывать |
|---|---|
references/measure_schema.md |
ищешь путь к числу или пишешь source находки |
references/risks.md |
шаг 3: каталог R1–R6 и разделы «что НЕ флагать» |
references/materiality.md |
шаг 3: гейт материальности, Pain × Spread |
references/axes.md |
оси разошлись — что перевешивает |
references/map_template.md |
шаг 2: секции карты и пометки OBSERVED/INFERRED/UNKNOWN |
references/refutation.md |
шаг 4: четыре линзы и запись вердикта |
references/remedy.md |
режим plan: из находок в решения |
references/recon.md |
режим recon: чужой репозиторий |
references/artifacts.md |
шаг 6: формы артефактов |
Форма находки
id · title · risk · symptom · axes · source · cost_pain · cost_spread ·
remedy · remedy_cost · gain · gain_metric · gain_target · gain_direction ·
alternatives · refutation · confidence · priority · status
Четыре поля, без которых находка — брак:
cost_pain— чем обходится сейчас, числом и ссылками на коммиты. Нет числа — находки нет.remedy_cost— во что обойдётся лечение, той же валютой. Без него линза «лечение дороже болезни» работает на глаз: сравнивать не с чем. Дороже болезни на горизонте — находка не выносится, идёт в «знаем, не чиним» с датой пересмотра.gain+gain_metric+gain_target— проверяемый прогноз в машинной форме. «Станет чище» — брак.gain_metric: containment_ratio,gain_target: 0.70— годится:zodchiy.py verifyсверит это со следующим прогоном и проставитgain_actualиgain_verdict. Прогноз, который не ложится на метрику снимка, помечаетсяgain_metric: manual— честно и видно, а не молча.refutation— какой факт снял бы находку. «Ничего не снимет, это очевидно» означает, что находку не проверяли.
Скилл требует измеримости от чужого кода. zodchiy.py verify — то же требование
к собственным советам: обещал containment 51% → 70%, через прогон видно, что
вышло. Без этой петли рекомендация ничем не отличается от мнения.
Что режется гейтом
Придирки · вкусовщина в именовании · теоретическая чистота · «так принято» · спекулятивный масштаб («а если 10 млн пользователей») · новизна ради новизны.
Модернизация оправдана текущим дефектом или измеримым упрощением. Точечный ремонт границы предпочтительнее переписывания, когда переписывание добавит больше переходной сложности, чем уберёт.
Что НЕ флагать
Разделы «что НЕ флагать» в references/risks.md — несущие, а не вежливые.
Проверено на живом коде: без них скилл объявил бы дефектами DI-корень с самым
высоким churn (74 правки, но 11% багфиксов — точка расширения по замыслу),
barrel с fan-in 109 и четыре «цикла», ни один из которых не существует в
рантайме.
Высокий churn при низкой доле багфиксов — точка расширения, не долг.
Смотри hotspots[].fix_share, а не только edits.
Эти четыре ложняка закреплены регрессией: evals/ держит по паре
«нарушение / похожий, но невиновный» на каждый из них. Правишь scripts/*.py —
гоняй python3 evals/run_evals.py (юнит + сквозной прогон на мини-репозиториях,
без модели, секунды). Подробности — evals/README.md.
Границы
Безопасность → fynd-dyrka · ревью диффа → code-review · внешний ресёрч →
deepdive · UI → design-modern.
Репо без git-истории или короче порога: поведенческая ось недоступна, скилл
говорит это прямо, работает на двух осях, потолок — finding.