av-dev-pipeline стал av-dev-code, review-pipeline — review
Имя описывало устройство, а не предмет: «пайплайн» говорит, что внутри конвейер, — а плагин занят кодом по задачам, и с появлением чекпоинтов он уже не конвейер в чистом виде. Набор имён стал параллельным: docs / tasks / code / git, каждое называет материал. Заодно review-pipeline стал review — слово ушло из плагина целиком, а не наполовину; скиллы выровнялись: resolve / review / openspec. Журнал версий канона переписан вместе со всеми, DECISIONS.md — нет. Разрез по типу высказывания, а не файла: наблюдение и причина неприкосновенны, предписание и адрес обязаны оставаться исполнимыми. Запись версии 10 велит «проверить, что плагин av-dev-pipeline установлен» — проект, дошедший до неё, выполнил бы невыполнимое.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
---
|
||||
name: openspec
|
||||
description: "Завести и настроить OpenSpec в проекте — openspec init --tools claude, замена закомментированного примера в openspec/config.yaml на настройку канонической формы (язык, правила именования capability, придирки валидатора, адреса паспорта и CLAUDE.md), проверка формы своим скриптом openspec.py (имя файла, схема, незаменённый пример, адреса документов, ключи rules против артефактов схемы) и сверка слепка с живой версией инструмента. Использовать, когда в проекте нет каталога openspec/, когда config.yaml остался примером из коробки, когда заводят новый проект или переводят чужой и дошли до шага OpenSpec, а также когда конвейер отказался работать без источника требований. Каталог openspec нужен именно конвейеру: без него не работают ни opsx:propose, ни ревью дизайна, ни сверка требований."
|
||||
---
|
||||
|
||||
# OpenSpec в проекте
|
||||
|
||||
Каталог `openspec/` — **предпосылка конвейера**, а не канона документов. Без него
|
||||
не работают ни `opsx:propose`, ни ревью дизайна, ни `review-specs`: у требований
|
||||
не остаётся дома. Поэтому заводит и настраивает его этот плагин — тот, кто по
|
||||
OpenSpec и работает.
|
||||
|
||||
Канон документов о файле не высказывается вовсе: `docs.py` его не открывает и об
|
||||
его отсутствии молчит. Проект без конвейера живёт без OpenSpec законно, и
|
||||
проверять там нечего. За каноном остаётся одно — **единственный дом**: не
|
||||
пересказан ли в `context` документ, у которого есть свой файл. Это суждение, а не
|
||||
форма, и смотрит его агент.
|
||||
|
||||
## Два шага, и второй важнее первого
|
||||
|
||||
**1. Завести.**
|
||||
|
||||
```
|
||||
openspec init --tools claude
|
||||
```
|
||||
|
||||
Команда кладёт ещё `.claude/skills/openspec-*` и `.claude/commands/opsx/*` — это
|
||||
её нормальная работа, не трогай их.
|
||||
|
||||
**2. Заменить пример.** `openspec init` кладёт `config.yaml`, где `context` и
|
||||
`rules` — закомментированный пример на английском. **Файл из коробки хуже
|
||||
отсутствующего:** он есть, он валиден, имя правильное, — и читается как
|
||||
настроенный, работая как пустой. Узнаётся это по уже написанному предложению: на
|
||||
другом языке, с capability по имени пакета, без единого `SHALL`.
|
||||
|
||||
Пример **заменяется целиком** по образцу:
|
||||
[references/config-skeleton.md](references/config-skeleton.md).
|
||||
|
||||
## Что туда пишут, а что нет
|
||||
|
||||
**Это маршрутизатор, а не второй дом фактов.** Внутрь идёт ровно то, что нужно
|
||||
**в момент порождения артефакта** и чего в этот момент ещё никто не открыл: язык,
|
||||
правила именования capability, придирки валидатора и **адреса** документов
|
||||
проекта.
|
||||
|
||||
Сюда же — **требования к форме `proposal` и `design`**, на которых стоит чекпоинт
|
||||
скилла `av-dev-code:resolve`: объяснение человеку собирается из этих двух
|
||||
артефактов, и требование к ним обязано применяться в момент, когда их пишут, а не
|
||||
вспоминаться шагом позже. Образец их содержит.
|
||||
|
||||
Пересказ паспорта, инвариантов, конвенций и правил ревью сюда **не переносится**.
|
||||
Место для второго дома здесь самое частое: `context` читается при порождении
|
||||
каждого артефакта, туда удобно дописать «чтобы агент знал», и так заводятся копии
|
||||
инвариантов, состава гейта и правил выбора метки. Расходятся они молча, а
|
||||
замечают это в уже написанном предложении.
|
||||
|
||||
Разрез, по которому отличают одно от другого: **утверждение, которое можно
|
||||
опровергнуть, открыв другой файл проекта, — пересказ; строка, которая говорит,
|
||||
какой файл открыть, — ссылка.** Машина этот разрез не проверяет; его смотрит
|
||||
агент `doc-consistency` из плагина канона, когда тот подключён.
|
||||
|
||||
Два адреса обязательны — `docs/passport.md` и `CLAUDE.md`: предложение пишется до
|
||||
того, как кто-либо откроет `docs/`, и без них его пишут, не зная ни границы
|
||||
домена, ни инвариантов. Отсутствие адреса к **существующему** документу
|
||||
`openspec.py check` называет отказом; документа нет в проекте — нет и требования.
|
||||
|
||||
## Инструмент
|
||||
|
||||
```
|
||||
os="$CLAUDE_PLUGIN_ROOT/skills/openspec/scripts/openspec.py"
|
||||
|
||||
python3 $os check --dir <корень> # форма config.yaml в проекте
|
||||
python3 $os form # слепок формы против живого OpenSpec
|
||||
```
|
||||
|
||||
**Коды выхода — общий словарь скриптов av-dev:** 0 сошлось, 1 дрейф, 2 ошибка
|
||||
употребления, 3 окружение, 4 внутренний сбой. Ветвись на коде, а не на тексте.
|
||||
Различать 1 и 3 обязательно: «форма разошлась» — рабочая ситуация, «openspec не
|
||||
отвечает» — нерабочая.
|
||||
|
||||
`check` проверяет пять вещей, и каждая — про молчащий пробел, а не про вкус:
|
||||
каталог есть; имя именно `config.yaml` (`config.yml` OpenSpec не читает и об этом
|
||||
не сообщает); `context` и `rules.specs` не остались примером, а правила называют
|
||||
`SHALL`; `context` называет паспорт и `CLAUDE.md`; ключи под `rules:` — имена
|
||||
артефактов схемы, а не свободные слова.
|
||||
|
||||
**Адреса требуются только к тем документам, которые в проекте есть.** Канон
|
||||
документов ставится отдельным плагином и может быть не подключён; требовать
|
||||
ссылку на несуществующий файл значит требовать битую ссылку. Нет
|
||||
`docs/passport.md` — проверка по нему идёт строкой «не проверялось», и там же
|
||||
сказано, что без канона конвейер работает вслепую.
|
||||
|
||||
### Форма сверяется с живым инструментом
|
||||
|
||||
Схема (`spec-driven`) и перечень артефактов (`proposal`, `specs`, `design`,
|
||||
`tasks`) — **состояние чужого инструмента**, а не наше решение. OpenSpec
|
||||
переименует артефакт: правила под прежним именем перестанут применяться, конфиг
|
||||
останется выглядеть написанным, и молчат при этом все три стороны.
|
||||
|
||||
Сторож — сравнение версий. `check` каждым прогоном спрашивает `openspec
|
||||
--version` (десятые доли секунды) и сравнивает `major.minor` с той версией, на
|
||||
которой форма сверялась; разошлось — **замечание**, не отказ, с именем команды.
|
||||
Патч-версия в сравнение не берётся намеренно: формы она не меняет, а нагоняй на
|
||||
каждый багфикс приучает пролистывать весь блок.
|
||||
|
||||
Перепроверяет `openspec.py form`: он спрашивает `openspec templates --json`, то
|
||||
есть перечень артефактов текущей схемы, и печатает, что разошлось с константами.
|
||||
Дорогой вызов вынесен из `check` сознательно — он стоит втрое дороже опроса
|
||||
версии, а ответ меняется только вместе с версией. **Чинится расхождение в
|
||||
плагине, а не в проекте:** константы скрипта, образец
|
||||
[references/config-skeleton.md](references/config-skeleton.md) и запись в журнал
|
||||
версий канона.
|
||||
|
||||
## Кто зовёт этот скилл
|
||||
|
||||
- `av-dev-docs:init` — шагом заведения нового проекта, до первого документа;
|
||||
- `av-dev-docs:canon` в режиме `adopt` — если на переводимом проекте каталога нет
|
||||
или `config.yaml` остался примером;
|
||||
- человек — когда конвейер отказался работать без источника требований.
|
||||
|
||||
**Копия.** Дом правила — `shared/plugin-boundary.md` в репозитории плагинов.
|
||||
Правится дом, а не этот файл.
|
||||
|
||||
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
|
||||
|
||||
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
|
||||
месте.
|
||||
|
||||
**Чужой скилл зовётся полным именем** — `av-dev-docs:canon`, `av-dev-tasks:tasks`,
|
||||
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
|
||||
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
|
||||
|
||||
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
|
||||
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
|
||||
прочитает его сам.
|
||||
|
||||
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
|
||||
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
|
||||
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
|
||||
|
||||
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
|
||||
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
|
||||
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` —
|
||||
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
|
||||
|
||||
<!-- /копия: граница-плагинов -->
|
||||
|
||||
Здесь это значит: вызов не разрешился — плагина конвейера в проекте нет, и тогда
|
||||
OpenSpec заводит человек командой выше.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
- **Не пишет спеки и предложения.** Это `opsx:propose` и пайплайн задачи.
|
||||
- **Не ведёт документы канона** — их дом плагин `av-dev-docs`, и адреса в
|
||||
`context` только на них ссылаются.
|
||||
- **Не чинит расхождение формы с версией OpenSpec в проекте.** Оно чинится в
|
||||
плагине: константы скрипта, образец здесь, запись в журнал версий канона.
|
||||
- **Не судит, ссылается `context` на документы или пересказывает их.** Машине
|
||||
этот разрез не виден; его смотрит агент `doc-consistency` из плагина канона.
|
||||
Плагина нет — эту проверку не делает никто, и так и скажи.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Образец `openspec/config.yaml`
|
||||
|
||||
Каталог `openspec/` заводится командой — `openspec init --tools claude`, — и она
|
||||
кладёт `config.yaml` с закомментированным примером внутри. Пример **заменяется
|
||||
целиком**: нетронутый файл выглядит настроенным, а работает как пустой.
|
||||
|
||||
**Это маршрутизатор, а не второй дом фактов.** Сюда пишут ровно то, что нужно
|
||||
**в момент порождения артефакта** и чего в этот момент ещё никто не открыл:
|
||||
язык, правила именования capability, придирки валидатора и **адреса** документов
|
||||
канона. Пересказ паспорта, инвариантов, конвенций и правил ревью сюда не
|
||||
переносится: расходится он молча, а замечают это в уже написанном предложении.
|
||||
|
||||
```yaml
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Language: Russian
|
||||
Пиши на русском, но:
|
||||
- Структурные заголовки оставляй на английском:
|
||||
## ADDED/MODIFIED/REMOVED Requirements, ### Requirement:, #### Scenario:
|
||||
- Ключевые слова GIVEN/WHEN/THEN и RFC 2119 (SHALL/MUST/SHOULD) — на английском
|
||||
- Технические термины, пути и код — на английском
|
||||
|
||||
Имена capabilities:
|
||||
- Capability — это ПОВЕДЕНИЕ или домен системы, а не пакет кода (совпадение с
|
||||
именем пакета допустимо, но не критерий).
|
||||
- Существительное, понятное без знания кода: ingest, parsing, storage,
|
||||
read-api. НЕ store/httpapi — это реализация.
|
||||
- Гранулярность по принципу «требования меняются вместе». Дробить, когда в
|
||||
одной спеке смешиваются разные заботы. Переименовать дёшево (RENAMED
|
||||
Requirements) — не дроби преждевременно в маленьком проекте.
|
||||
|
||||
RFC 2119 — требование валидатора, не стиль:
|
||||
- Каждое ### Requirement ОБЯЗАНО содержать литерал SHALL или MUST, иначе
|
||||
`openspec validate` падает. Поэтому эти слова и WHEN/THEN не русифицируем.
|
||||
|
||||
Что это за проект — читай перед предложением, а не отсюда:
|
||||
- docs/passport.md — цель, её граница (чем проект НЕ является), потребители,
|
||||
типовые сценарии, референсы;
|
||||
- CLAUDE.md — инварианты с severity и семантика гейта;
|
||||
- docs/architecture.md — устройство; docs/security.md — периметр;
|
||||
docs/adr/ — почему решено так; docs/research/ — что уже измерено.
|
||||
Пересказа этих документов здесь нет намеренно: второй дом факта расходится с
|
||||
первым молча, и заметно это становится в предложении, которое уже написано.
|
||||
|
||||
Ревью: правило выбора метки и состав проходов здесь не пересказываем — их дом
|
||||
скилл av-dev-code:review, проектная настройка — docs/review.md.
|
||||
|
||||
Конвенции кода: механизированное проверяет гейт, прозой остаётся
|
||||
docs/conventions/. Ни состав шагов гейта, ни перечень конвенций здесь не
|
||||
пересказываем: и то и другое растёт по ходу задач.
|
||||
|
||||
Развилка или блокер — сперва prior art. Готовые решения смотрим в референсах
|
||||
паспорта, отвергаем — с названной причиной, и причина идёт в design.md этого
|
||||
же изменения.
|
||||
|
||||
rules:
|
||||
proposal:
|
||||
- Capabilities называй по поведению или домену системы, не по пакету кода
|
||||
- "Why и What Changes — языком домена из docs/passport.md: без SHALL, без имён модулей и функций там, где вещь называется по-русски"
|
||||
design:
|
||||
- "Назови рассмотренные варианты и причину отказа от каждого — из них потом пишется ADR"
|
||||
- "Решение объясняется через то, что человек увидит иначе, а не через устройство кода"
|
||||
specs:
|
||||
# Кавычки обязательны: без них YAML обрежет строку на первом '#'.
|
||||
- "Каждое ### Requirement обязано содержать SHALL или MUST (иначе валидация падает)"
|
||||
- "Сценарий — ровно #### (четыре решётки); три или список молча теряются"
|
||||
- "SHALL/MUST должно стоять в ПЕРВОМ абзаце требования: валидатор смотрит только его"
|
||||
- "Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском"
|
||||
```
|
||||
|
||||
**Четыре правила для `specs` сняты отказами валидатора, а не выведены из
|
||||
документации** — потому и записаны дословно: без них каждое второе предложение
|
||||
узнаёт их падением `openspec validate --strict`.
|
||||
|
||||
**Правила для `proposal` и `design` держат чекпоинт скилла
|
||||
`av-dev-code:resolve`.** Там работа останавливается и человеку объясняют, в
|
||||
чём проблема и как её решают, — а объяснение **собирается из этих двух
|
||||
артефактов**, а не сочиняется заново: третий пересказ одного и того же разошёлся
|
||||
бы с обоими. Требование поэтому стоит здесь, в момент порождения артефакта, а не
|
||||
в скилле, который спохватится позже. `design.md` при этом ещё и **сырьё для
|
||||
ADR** — отвергнутый вариант с названной причиной и есть половина будущей записи. Блок `context` проект
|
||||
дополняет своим (стек, разведка, особенности домена), но **адреса паспорта и
|
||||
`CLAUDE.md` обязательны** — отсутствие адреса к существующему документу
|
||||
`openspec.py check` называет отказом.
|
||||
|
||||
**Ключи под `rules:` — имена артефактов схемы**, а не свободные слова:
|
||||
`proposal`, `specs`, `design`, `tasks`. Правило под чужим именем не применяется
|
||||
и об этом не сообщает, поэтому `rules.spec` вместо `rules.specs` даёт конфиг,
|
||||
выглядящий написанным и не работающий; `openspec.py check` такой ключ называет.
|
||||
Перечень артефактов задаёт OpenSpec, а не мы, — за его актуальностью следит
|
||||
`openspec.py form`.
|
||||
@@ -0,0 +1,375 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Форма `openspec/config.yaml`: проверка проекта и сверка слепка с инструментом.
|
||||
|
||||
Каталог `openspec/` — предпосылка **конвейера**, а не канона документов: без него
|
||||
не работают ни `opsx:propose`, ни ревью дизайна, ни сверка требований. Поэтому и
|
||||
проверка формы живёт здесь, рядом со скиллом, который каталог заводит. Раньше она
|
||||
жила в `docs.py` плагина канона, и у файла было два владельца: один заводит,
|
||||
другой проверяет.
|
||||
|
||||
Проверяется то, что **молчит при поломке**. Файл из коробки хуже отсутствующего:
|
||||
`openspec init` кладёт `config.yaml`, где `context` и `rules` — закомментированный
|
||||
пример на английском. Он есть, он валиден, имя правильное — и читается как
|
||||
настроенный, работая как пустой. Узнаётся это по уже написанному предложению.
|
||||
|
||||
Разбираем текстом, а не YAML-парсером: у скриптов ноль внешних зависимостей, а
|
||||
PyYAML в стандартной библиотеке нет. Всё проверяемое различимо построчно,
|
||||
комментарии отброшены, ключи верхнего уровня стоят в первой колонке.
|
||||
|
||||
Коды выхода — общий словарь скриптов av-dev:
|
||||
0 сошлось
|
||||
1 дрейф: форма разошлась с ожидаемой
|
||||
2 ошибка употребления: аргументы
|
||||
3 окружение: не тот каталог, инструмент не отвечает
|
||||
4 внутренний сбой
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import NoReturn
|
||||
|
||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||
|
||||
# Команда заведения. Названа поимённо потому, что её печатает отказ, а отказ без
|
||||
# команды заставляет искать её в другом месте.
|
||||
OPENSPEC_INIT = "openspec init --tools claude"
|
||||
|
||||
# Адреса, которые обязан назвать блок context. Не пересказ документов, а именно
|
||||
# ссылки: предложение пишется до того, как кто-либо откроет docs/, и без этих
|
||||
# двух строк его пишут, не зная ни границы домена, ни инвариантов. Список
|
||||
# короткий намеренно — длинный превращает context во второй дом фактов.
|
||||
#
|
||||
# Третий элемент — путь, по которому проверяется, есть ли документ в проекте
|
||||
# вообще. Канон документов ставится отдельным плагином и может быть не подключён;
|
||||
# требовать ссылку на файл, которого нет, значит требовать битую ссылку.
|
||||
OPENSPEC_POINTERS = [
|
||||
("passport", "docs/passport.md",
|
||||
"граница домена и «чем НЕ является» останутся непрочитанными"),
|
||||
("CLAUDE.md", "CLAUDE.md",
|
||||
"инварианты и семантика гейта останутся непрочитанными"),
|
||||
]
|
||||
|
||||
# --- Слепок чужого инструмента ----------------------------------------------
|
||||
#
|
||||
# Схема, перечень артефактов и версия, на которой это проверено, живут в OpenSpec
|
||||
# и меняются без нашего участия; здесь они записаны, чтобы проверка шла без
|
||||
# запуска node на каждом прогоне.
|
||||
#
|
||||
# Слепок стареет, и потому есть кто это замечает: `check` сравнивает major.minor
|
||||
# установленного OpenSpec с OPENSPEC_CHECKED и, если они разошлись, говорит
|
||||
# замечанием «форма не перепроверена». Перепроверяет команда `form` — она
|
||||
# спрашивает сам инструмент и печатает, что разошлось. Патч-версия сравнением
|
||||
# намеренно не берётся: форма конфига в ней не меняется, а замечание на каждый
|
||||
# багфикс приучило бы пролистывать весь блок.
|
||||
OPENSPEC_CHECKED = "1.5"
|
||||
OPENSPEC_SCHEMA = "spec-driven"
|
||||
|
||||
# Артефакты схемы. Ключ `rules:` адресуется артефакту, и адресованный
|
||||
# несуществующему **молча не действует** — ровно тот класс, ради которого вся
|
||||
# проверка и заведена.
|
||||
OPENSPEC_ARTIFACTS = ("proposal", "specs", "design", "tasks")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Report:
|
||||
errors: list[str] = field(default_factory=list)
|
||||
notes: list[str] = field(default_factory=list)
|
||||
skipped: list[str] = field(default_factory=list)
|
||||
|
||||
def error(self, msg: str) -> None:
|
||||
self.errors.append(msg)
|
||||
|
||||
def note(self, msg: str) -> None:
|
||||
self.notes.append(msg)
|
||||
|
||||
def skip(self, msg: str) -> None:
|
||||
self.skipped.append(msg)
|
||||
|
||||
|
||||
def fail(code: int, msg: str) -> NoReturn:
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def openspec_cli(args: list[str]) -> str | None:
|
||||
"""Спросить сам инструмент. None — его нет или он не ответил."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["openspec", *args], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
except (FileNotFoundError, OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
return out.stdout.strip() if out.returncode == 0 else None
|
||||
|
||||
|
||||
def rules_keys(live: str) -> list[str]:
|
||||
"""Имена артефактов, которым адресованы правила, — и только они.
|
||||
|
||||
Идём от строки `rules:` до следующего ключа нулевой колонки, а не ищем
|
||||
отступ по всему файлу: блок `context: |` — литеральный скаляр, внутри него
|
||||
строки вида «Language: Russian» и «av-dev-code:review» выглядят
|
||||
ключами и дали бы находку на ровном месте. Проверено на живом конфиге,
|
||||
который так и падал.
|
||||
"""
|
||||
out: list[str] = []
|
||||
inside = False
|
||||
for line in live.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if not line[0].isspace():
|
||||
inside = line.startswith("rules:")
|
||||
continue
|
||||
if not inside:
|
||||
continue
|
||||
m = re.fullmatch(r" ([A-Za-z_-]+):\s*", line)
|
||||
if m:
|
||||
out.append(m.group(1))
|
||||
return out
|
||||
|
||||
|
||||
def check_form(root: Path, rep: Report) -> None:
|
||||
"""Настройка заведена и не осталась примером из коробки."""
|
||||
os_dir = root / "openspec"
|
||||
if not os_dir.is_dir():
|
||||
# Здесь это отказ, а не «неприменимо»: скрипт принадлежит конвейеру, а
|
||||
# конвейер без OpenSpec не работает вовсе. Тот же вопрос со стороны
|
||||
# канона документов звучит иначе, и `docs.py` отвечает на него молчанием.
|
||||
rep.error(
|
||||
"нет openspec/ — там дом темы requirements (openspec/specs/) и "
|
||||
f"настройка генерации артефактов; заводится `{OPENSPEC_INIT}`"
|
||||
)
|
||||
return
|
||||
|
||||
if (os_dir / "config.yml").is_file():
|
||||
rep.error(
|
||||
"openspec/config.yml — читается только config.yaml, и этот файл "
|
||||
"останется незамеченным: настройка будет пустой, а выглядеть будет "
|
||||
"заполненной"
|
||||
)
|
||||
|
||||
path = os_dir / "config.yaml"
|
||||
if not path.is_file():
|
||||
rep.error(
|
||||
"нет openspec/config.yaml — язык, правила именования capability и "
|
||||
"придирки валидатора будут заново угадываться на каждом предложении"
|
||||
)
|
||||
return
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
live = "\n".join(
|
||||
line for line in text.splitlines() if not line.lstrip().startswith("#")
|
||||
)
|
||||
keys = set(re.findall(r"(?m)^([A-Za-z_]+):", live))
|
||||
|
||||
schema = re.search(r"(?m)^schema:\s*(\S+)", live)
|
||||
if schema is None:
|
||||
rep.error(
|
||||
f"в openspec/config.yaml нет ключа schema — ожидается {OPENSPEC_SCHEMA}"
|
||||
)
|
||||
elif schema.group(1) != OPENSPEC_SCHEMA:
|
||||
rep.error(
|
||||
f"schema в openspec/config.yaml — {schema.group(1)}, а форма описана "
|
||||
f"для {OPENSPEC_SCHEMA}"
|
||||
)
|
||||
|
||||
if "context" not in keys:
|
||||
rep.error(
|
||||
"в openspec/config.yaml нет ключа context: файл остался примером из "
|
||||
"коробки — предложение пишется без языка, правил именования "
|
||||
"capability и адресов документов проекта"
|
||||
)
|
||||
else:
|
||||
for pointer, where, why in OPENSPEC_POINTERS:
|
||||
if not (root / where).exists():
|
||||
rep.skip(
|
||||
f"{where} в проекте нет — ссылка на него в context не "
|
||||
f"требуется. Документы канона ведёт отдельный плагин "
|
||||
f"(av-dev-docs), и без него конвейер работает вслепую"
|
||||
)
|
||||
continue
|
||||
if pointer not in live:
|
||||
rep.error(f"openspec/config.yaml не называет {pointer} — {why}")
|
||||
|
||||
if "rules" not in keys or "specs:" not in live:
|
||||
rep.error(
|
||||
"в openspec/config.yaml нет rules.specs — придирки валидатора "
|
||||
"нигде не записаны, и каждое предложение узнаёт их отказом"
|
||||
)
|
||||
elif "SHALL" not in live:
|
||||
rep.error(
|
||||
"rules.specs в openspec/config.yaml не называет SHALL — "
|
||||
"требование без этого литерала валидатор отвергает, а правило "
|
||||
"проекта об этом молчит"
|
||||
)
|
||||
|
||||
# Ключ под rules: — имя артефакта схемы. Опечатка или устаревшее имя не
|
||||
# ломает ничего видимого: правила просто не применяются, а конфиг выглядит
|
||||
# написанным.
|
||||
for name in rules_keys(live):
|
||||
if name not in OPENSPEC_ARTIFACTS:
|
||||
rep.error(
|
||||
f"rules.{name} в openspec/config.yaml — такого артефакта у схемы "
|
||||
f"{OPENSPEC_SCHEMA} нет ({', '.join(OPENSPEC_ARTIFACTS)}): правила "
|
||||
f"под ним не применяются и молчат об этом"
|
||||
)
|
||||
|
||||
|
||||
def check_fresh(rep: Report) -> None:
|
||||
"""Не устарел ли слепок формы.
|
||||
|
||||
Стоит один запуск `openspec --version` — десятые доли секунды. Перечень
|
||||
артефактов и имя схемы отсюда не спрашиваются намеренно: они стоят втрое
|
||||
дороже, а меняются только вместе с версией, и потому за ними ходит команда
|
||||
`form`, а эта проверка говорит, когда её звать.
|
||||
"""
|
||||
got = openspec_cli(["--version"])
|
||||
if got is None:
|
||||
rep.skip(
|
||||
"openspec не отвечает (нет на PATH?) — актуальность формы "
|
||||
"config.yaml не проверялась"
|
||||
)
|
||||
return
|
||||
installed = ".".join(got.split(".")[:2])
|
||||
if installed != OPENSPEC_CHECKED:
|
||||
rep.note(
|
||||
f"форма openspec/config.yaml сверена с OpenSpec {OPENSPEC_CHECKED}, "
|
||||
f"установлен {got}: перепроверить — `openspec.py form`. Пока не "
|
||||
f"перепроверено, проверки формы судят по прежней схеме"
|
||||
)
|
||||
|
||||
|
||||
def report(rep: Report) -> int:
|
||||
for msg in rep.errors:
|
||||
print(f"ДРЕЙФ {msg}")
|
||||
for msg in rep.notes:
|
||||
print(f"ЗАМЕЧАНИЕ {msg}")
|
||||
if rep.skipped:
|
||||
print("\nНЕ ПРОВЕРЯЛОСЬ:")
|
||||
for msg in rep.skipped:
|
||||
print(f" {msg}")
|
||||
|
||||
print(
|
||||
"\nМашина проверила форму: имя файла, схему, незаменённый пример, адреса\n"
|
||||
"документов проекта и ключи rules против артефактов схемы. Чего она не\n"
|
||||
"видит — **пересказ вместо ссылки**: утверждение, которое можно\n"
|
||||
"опровергнуть, открыв другой файл проекта, от строки «открой такой-то\n"
|
||||
"файл» она не отличает. Это суждение агента `doc-consistency` из плагина\n"
|
||||
"канона документов; нет плагина — нет и этой проверки, и так и скажи."
|
||||
)
|
||||
if rep.errors:
|
||||
print(f"\nИтог: дрейф, {len(rep.errors)} пунктов.")
|
||||
return DRIFT
|
||||
print("\nИтог: форма сошлась в механизируемой части.")
|
||||
return OK
|
||||
|
||||
|
||||
def cmd_check(args: argparse.Namespace) -> int:
|
||||
root = Path(args.dir).resolve()
|
||||
if not root.is_dir():
|
||||
fail(ENV, f"нет каталога {root}")
|
||||
rep = Report()
|
||||
check_form(root, rep)
|
||||
check_fresh(rep)
|
||||
return report(rep)
|
||||
|
||||
|
||||
def cmd_form(args: argparse.Namespace) -> int:
|
||||
"""Перепроверить слепок формы по живому OpenSpec.
|
||||
|
||||
Ничего не правит и не трогает проект: спрашивает инструмент и печатает, что
|
||||
разошлось с константами скрипта. Чинит человек — правкой констант, образца в
|
||||
references/config-skeleton.md и записью в журнал версий канона, если форма
|
||||
действительно поменялась.
|
||||
"""
|
||||
version = openspec_cli(["--version"])
|
||||
if version is None:
|
||||
fail(
|
||||
ENV,
|
||||
"openspec не отвечает: поставь его или проверь PATH — "
|
||||
"перепроверять форму нечем",
|
||||
)
|
||||
raw = openspec_cli(["templates", "--json"])
|
||||
if raw is None:
|
||||
fail(ENV, "`openspec templates --json` не отработал — схему не спросить")
|
||||
try:
|
||||
artifacts = tuple(json.loads(raw))
|
||||
except json.JSONDecodeError as exc:
|
||||
fail(ENV, f"`openspec templates --json` отдал неразбираемое: {exc}")
|
||||
|
||||
print(f"OpenSpec установлен: {version}")
|
||||
print(f"форма сверена с: {OPENSPEC_CHECKED}")
|
||||
print(f"артефакты схемы: {', '.join(artifacts)}")
|
||||
print(f"записано в скрипте: {', '.join(OPENSPEC_ARTIFACTS)}")
|
||||
|
||||
diffs: list[str] = []
|
||||
if ".".join(version.split(".")[:2]) != OPENSPEC_CHECKED:
|
||||
diffs.append(
|
||||
f"версия: поднять OPENSPEC_CHECKED до "
|
||||
f"{'.'.join(version.split('.')[:2])} — но только после того, как "
|
||||
f"остальные строки этого отчёта сойдутся"
|
||||
)
|
||||
for name in artifacts:
|
||||
if name not in OPENSPEC_ARTIFACTS:
|
||||
diffs.append(
|
||||
f"новый артефакт {name}: решить, нужны ли ему правила в rules, "
|
||||
f"и добавить имя в OPENSPEC_ARTIFACTS"
|
||||
)
|
||||
for name in OPENSPEC_ARTIFACTS:
|
||||
if name not in artifacts:
|
||||
diffs.append(
|
||||
f"артефакта {name} у схемы больше нет: правила под ним в конфигах "
|
||||
f"проектов молчат — убрать из OPENSPEC_ARTIFACTS, из образца и "
|
||||
f"записать в журнал версий канона"
|
||||
)
|
||||
|
||||
print()
|
||||
if not diffs:
|
||||
print("Слепок сходится. Осталось глазами: не изменились ли придирки")
|
||||
print("валидатора — их скрипт проверить не может, они проявляются только")
|
||||
print("отказом `openspec validate --strict` на живой спеке.")
|
||||
return OK
|
||||
print("Разошлось:")
|
||||
for line in diffs:
|
||||
print(f" - {line}")
|
||||
print()
|
||||
print("Правится в трёх местах сразу: константы этого скрипта, образец")
|
||||
print("`references/config-skeleton.md` и запись в журнал версий канона —")
|
||||
print("иначе проекты останутся на прежней форме молча.")
|
||||
return DRIFT
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="openspec.py",
|
||||
description="форма openspec/config.yaml: проверка проекта и сверка слепка",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_check = sub.add_parser("check", help="форма config.yaml в проекте")
|
||||
p_check.add_argument("--dir", default=".", help="корень проекта")
|
||||
p_check.set_defaults(func=cmd_check)
|
||||
|
||||
p_form = sub.add_parser(
|
||||
"form", help="перепроверить слепок формы по живому OpenSpec"
|
||||
)
|
||||
p_form.set_defaults(func=cmd_form)
|
||||
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
return args.func(args)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001 — последний рубеж, код 4 по словарю
|
||||
print(f"ВНУТРЕННИЙ СБОЙ: {exc}", file=sys.stderr)
|
||||
return INTERNAL
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,629 @@
|
||||
---
|
||||
name: resolve
|
||||
description: "Решить одну задачу от постановки до закрытия. На входе путь к файлу задачи, её слаг или просто текст. Обычная задача идёт циклом Spec Driven Development: opsx propose → разметка → ревью дизайна → чекпоинт с объяснением человеческим языком → opsx apply → ревью кода → archive → синк документации → коммит → закрытие. Исследовательская (тип research, сырая идея, мутная постановка) начинается раньше: opsx explore и чекпоинт вариантов — два-четыре способа решить, с ценой каждого и рекомендацией; выбор оседает по адресу, который назвала сама задача. Между чекпоинтами работа идёт без согласований. Использовать, когда просят взять, сделать или решить задачу, довести идею до реализации, разобраться с записью из беклога."
|
||||
---
|
||||
|
||||
# Решение одной задачи
|
||||
|
||||
Проводит **одну** задачу от постановки до закрытия. Между плановыми
|
||||
остановками — без согласований: механику не обсуждаем, делаем.
|
||||
|
||||
Тонкая обёртка над каноническими скиллами `opsx:explore` / `opsx:propose` /
|
||||
`opsx:apply` / `opsx:archive` — зови их через Skill, не переизобретай их шаги.
|
||||
Ревью — скилл `av-dev-code:review`; он же держит правило выбора
|
||||
метки, а называет её агент `review-scope` — один раз на задачу, для обеих стадий
|
||||
ревью.
|
||||
|
||||
## Предпосылки
|
||||
|
||||
- **OpenSpec и скиллы `opsx:*` — жёсткая предпосылка, а не опция.** На них стоят
|
||||
шаги 2, 6 и 8, проход `review-specs` и ревью дизайна (они завязаны на
|
||||
`openspec/changes/<id>/specs/*/spec.md` и на `openspec validate --strict`).
|
||||
**Проект без OpenSpec этим скиллом не ведётся** — подключай OpenSpec, а не
|
||||
вырождай цикл: ветка деградации здесь не пишется, потому что непроверенная
|
||||
ветка деградации хуже честного отказа.
|
||||
- **Проектные копии этих скиллов и агентов удаляются при установке плагина**
|
||||
(`.claude/skills/` — и голые имена `resolve`, `task-pipeline`,
|
||||
`review-pipeline`, и с префиксом проекта: `<проект>-task-pipeline`,
|
||||
`<проект>-review-pipeline`; `.claude/agents/<проект>-review-*.md`). Две копии
|
||||
одного скилла расходятся, и побеждает та, что короче названа.
|
||||
|
||||
### Обращение к соседним плагинам
|
||||
|
||||
**Копия.** Дом — `shared/plugin-boundary.md` в репозитории плагинов: правило
|
||||
общее для всех, кто зовёт чужое, и ни один плагин им не владеет. Правится дом, а
|
||||
не этот файл.
|
||||
|
||||
<!-- копия: граница-плагинов из shared/plugin-boundary.md -->
|
||||
|
||||
Плагины `av-dev` ставятся порознь, и ни один не вправе считать, что сосед на
|
||||
месте.
|
||||
|
||||
**Чужой скилл зовётся полным именем** — `av-dev-docs:canon`, `av-dev-tasks:tasks`,
|
||||
`av-dev-code:review`. Короткое имя может разрешиться в устаревшую проектную копию
|
||||
из `.claude/skills/`, и подмены не будет видно ни в докладе, ни в поведении.
|
||||
|
||||
**Путь в дерево чужого плагина не пишется никогда.** `$CLAUDE_PLUGIN_ROOT` ведёт
|
||||
только в свой плагин; вычисленный от него путь к соседу либо не откроется, либо
|
||||
откроет чужую установку. Нужен чужой справочник — зови владеющий им скилл, он
|
||||
прочитает его сам.
|
||||
|
||||
**Вызов не разрешился — плагина в проекте нет.** Это исход, а не поломка: назови
|
||||
строкой доклада, чего теперь не делает никто, и продолжай работу. Молчать нельзя,
|
||||
пропуск неотличим от сделанного; выдумывать обходной путь нельзя тоже.
|
||||
|
||||
**Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня
|
||||
установленных плагинов проект не ведёт — он разошёлся бы с действительностью
|
||||
молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` —
|
||||
канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер.
|
||||
|
||||
<!-- /копия: граница-плагинов -->
|
||||
|
||||
Скилл зовёт `av-dev-code:review`, `av-dev-docs:docs` и
|
||||
`av-dev-tasks:tasks`. Чем оборачивается отсутствие каждого — на самих шагах и в
|
||||
разделе «Границы».
|
||||
|
||||
Перед стартом прочитай `CLAUDE.md` проекта и то, на что он ссылается, если ещё
|
||||
не в контексте. Проектные факты, нужные ревью — инварианты, семантика гейта,
|
||||
объёмы, модель угроз, прецеденты, — живут в **документах канона** `av-dev-docs`;
|
||||
карта «что где» — `references/project-facts.md` конвейера ревью.
|
||||
|
||||
**Документов канона нет — проект к нему не приведён.** Скажи это строкой и
|
||||
предложи скилл `av-dev-docs:canon`: одна операция на проект против поразрядной
|
||||
деградации на каждой задаче. Работу при этом не останавливай.
|
||||
|
||||
## Вход
|
||||
|
||||
Задача задаётся **путём к файлу, именем файла, слагом или просто текстом**.
|
||||
Ничего из этого не задано — попроси у вызывающего и остановись; сам в беклог не
|
||||
лезь и приоритеты не интерпретируй: что делать дальше, решает не этот скилл.
|
||||
|
||||
**Запись, не готовая к работе, в работу не берётся.** У типа `research` это
|
||||
проверяемо и потому обязательно: нет непустого раздела **«Вопрос»** — это не
|
||||
разведка, а сырьё, и его сперва доводят до вопроса. Скажи это исходом и назови,
|
||||
чего не хватает; штурмовать сырьё за автора — не работа этого скилла.
|
||||
|
||||
У остальных типов схему держит `av-dev-tasks` (обязательные разделы, не меньше
|
||||
двух критериев приёмки). Прочитай запись и, если видно, что схема не выполнена,
|
||||
скажи это строкой — но работу не останавливай: у типов действия пробел лечится
|
||||
по ходу, а у разведки без вопроса лечить нечего.
|
||||
|
||||
## Две ветки
|
||||
|
||||
Развилка одна и стоит на входе:
|
||||
|
||||
- **обычная задача** — что делать, понятно; спорно только как. Идёт с шага 1;
|
||||
- **исследовательская** — тип `research`, сырая идея, новое и незнакомое, мутная
|
||||
постановка. Идёт с шага Р1, и там её ждёт **свой** чекпоинт: варианты решения
|
||||
обсуждаются **до** того, как написано первое требование.
|
||||
|
||||
Признак не в объёме работы, а в том, **есть ли у задачи один очевидный способ
|
||||
решения**. Его нет — обсуждать варианты после `propose` поздно: предложение уже
|
||||
воплотило один из них, и разговор пойдёт не о выборе, а о переделке.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
in["вход: файл, слаг или текст"]
|
||||
fork{"есть очевидный<br/>способ решения?"}
|
||||
r1["Р1. понять вопрос<br/>сырьё без «Вопроса» — отказ"]
|
||||
r2["Р2. opsx:explore — груминг"]
|
||||
r3(["Р3. ЧЕКПОИНТ: варианты<br/>2–4 способа, цена каждого,<br/>рекомендация"])
|
||||
rout["исход без кода:<br/>ответ записан / отказ / родились задачи"]
|
||||
s1["1. прочитать задачу<br/>критерии приёмки выписать сразу"]
|
||||
s2["2. opsx:propose — change, дельта-спеки, tasks.md"]
|
||||
s3["3. разметка — review-scope:<br/>размер, сложность, метка, план тем"]
|
||||
s4["4. ревью дизайна, состав по метке<br/>+ отработка замечаний"]
|
||||
s5(["5. ЧЕКПОИНТ: объяснение<br/>в чём проблема, как решаем,<br/>чем рискуем"])
|
||||
s6["6. opsx:apply — код, гейт,<br/>поведенческая верификация"]
|
||||
s7["7. ревью кода, та же метка<br/>+ отработка замечаний"]
|
||||
s8["8. opsx:archive"]
|
||||
s9["9. синк документации — av-dev-docs:docs"]
|
||||
s10["10. коммит работы — av-dev-git:commit"]
|
||||
s11["11. закрыть задачу — av-dev-tasks:tasks,<br/>вторым коммитом учёта"]
|
||||
|
||||
in --> fork
|
||||
fork -->|"да"| s1
|
||||
fork -->|"нет"| r1
|
||||
r1 --> r2 --> r3
|
||||
r3 -->|"выбран способ"| s1
|
||||
r3 -.->|"кода не будет"| rout
|
||||
s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 --> s8 --> s9 --> s10 --> s11
|
||||
s3 -.->|"план задачи: та же метка"| s7
|
||||
s7 -.->|"находка отменяет дизайн"| s5
|
||||
```
|
||||
|
||||
Схема — **сводка**: содержание каждого шага в его разделе ниже, и при
|
||||
расхождении прав текст.
|
||||
|
||||
## Автономность и два плановых стопа
|
||||
|
||||
**Между чекпоинтами умолчание прежнее — делать, а не спрашивать.** Чекпоинты не
|
||||
отменяют автономность, они дают развилкам плановое место, куда копиться.
|
||||
|
||||
Разрез простой:
|
||||
|
||||
- развилка найдена **до** ближайшего чекпоинта — она его и ждёт. Не спрашивай
|
||||
отдельно: чекпоинт рядом и стоит дёшево, а три вопроса подряд стоят дороже
|
||||
одного разговора;
|
||||
- развилка найдена **после** последнего чекпоинта — старое правило: **запиши
|
||||
вопрос и доведи остаток**, не останавливаясь.
|
||||
|
||||
Запись вопроса устроена так:
|
||||
|
||||
1. **Запиши там, где проект держит вопросы** (секция беклога, файл задачи,
|
||||
трекер — это знает проект). Проект не сказал, куда, — отдельной секцией
|
||||
`Вопросы` в своём докладе, и это тоже исход. Тело отвечает на три вещи: **что
|
||||
именно решить**, **какие есть варианты и цена каждого**, **что стоит, пока
|
||||
решения нет**. Плюс твоя рекомендация — человек чаще соглашается, чем выбирает
|
||||
заново, и готовое суждение экономит ему весь контекст.
|
||||
2. **Переформулируй задачу на остаток** — то, что делается без этого решения.
|
||||
Назови границу: докуда доводим сейчас.
|
||||
3. Доведи остаток до конца и закоммить. Задача не «висит на вопросе», она сделана
|
||||
в объявленных границах.
|
||||
|
||||
**Что остатком не является — правило живёт не здесь.** Канонический текст с обеими
|
||||
оговорками — в плагине `av-dev-tasks`, скилл `av-dev-tasks:session`, раздел
|
||||
`## Вопрос, блокер, необратимое`, подраздел «Отличать вопрос от застревания».
|
||||
Правило принадлежит управлению задачами, потому что решает **сделана задача или
|
||||
вышла**, — это исход планирования, а не исполнения. **Ссылайся, не
|
||||
пересказывай:** копия, заведённая здесь, уже однажды разошлась с оригиналом и
|
||||
потеряла из перечня самое необратимое — запись **наружу**.
|
||||
|
||||
Коротко, чтобы знать, когда идти читать: остаток проверяется двумя порогами —
|
||||
**материализация нерешённого** (запись состояния, зависящего от неотвеченного
|
||||
вопроса) и **пол по пользе** (из остатка пропала польза, названная в постановке).
|
||||
Оба порога — стоп: первый поднимает решение до начала записи, второй даёт исход
|
||||
«не доведена».
|
||||
|
||||
Плагин `av-dev-tasks` не подключён — правило не отменяется, а становится
|
||||
осторожнее: прежде чем записать зависящее от нерешённого куда бы то ни было —
|
||||
в хранилище, в журнал, в витрину или наружу, — спрашивай человека.
|
||||
|
||||
Нет полезного остатка — задача заканчивается исходом «не доведена», вопрос
|
||||
записан, ничего не коммитится наполовину.
|
||||
|
||||
### Когда спрашивать вне чекпоинтов
|
||||
|
||||
По другому основанию — не «сложное решение», а **необратимое действие**:
|
||||
|
||||
- деплой, выкладка наружу, смена публичного адреса или токенов;
|
||||
- удаление или перезапись рабочих данных, включая подрезку архивов;
|
||||
- всё, что уходит за пределы машины.
|
||||
|
||||
Здесь ошибка не откатывается коммитом, поэтому спрашиваем даже когда решение
|
||||
кажется очевидным.
|
||||
|
||||
Стиль правок — заточка под проект и конвенции, right-size, без золочения.
|
||||
|
||||
## Границы: чем этот скилл не владеет
|
||||
|
||||
- **Беклогом, целями и приоритетами.** Задача приходит извне. Скилл её не
|
||||
выбирает, не приоритизирует, не заводит и не переоценивает.
|
||||
- **Форматом задач.** Индексы руками не правятся, путь к скрипту учёта не
|
||||
выдумывается: этим владеет `av-dev-tasks:tasks` (шаг 11). Закрытие — работа
|
||||
этого скилла, и это осознанное решение с названной ценой: **приёмщик и
|
||||
исполнитель совпали**. Закрытие поэтому **не окончательно** — человек на сессии
|
||||
возвращает задачу `reopen` с причиной, а доклад по критериям приёмки становится
|
||||
единственным, по чему приёмка вообще возможна.
|
||||
- **Заведением задач из урожая ревью.** Отложенные находки отдаются **списком**;
|
||||
превращать их в задачи — работа того, кто ведёт задачи проекта.
|
||||
- **Определением ценности.** «Нужна ли эта функциональность» — не вопрос этого
|
||||
скилла ни на одном шаге. Чекпоинт спрашивает «так ли решаем», а не «надо ли».
|
||||
|
||||
## Наблюдаемые исходы
|
||||
|
||||
Ровно четыре, и каждый обязан быть назван в докладе прямо:
|
||||
|
||||
- **сделана** — определение готовности выполнено целиком;
|
||||
- **не доведена** — с причиной и с записанным вопросом; что именно сделано и до
|
||||
какой границы, названо явно. Сюда же попадает чекпоинт, на котором человек
|
||||
решение не одобрил;
|
||||
- **оказалась крупнее задачи** — распознаётся **до заведения change**, иначе его
|
||||
придётся выбрасывать. Дальше — декомпозиция, и это не работа этого скилла;
|
||||
- **знание вместо изменения** — только у исследовательской ветки: ответ записан
|
||||
по названному адресу, кода задача не потребовала. Это полноправный исход, а не
|
||||
недоведённая работа.
|
||||
|
||||
## Определение готовности
|
||||
|
||||
Задача сделана, когда верно всё:
|
||||
|
||||
1. гейт проекта зелёный;
|
||||
2. ревью проведено, **план прогона сверен с исходом по каждой теме**, темы без
|
||||
отчёта и без дома названы в границах покрытия;
|
||||
3. решение прошло чекпоинт и не разошлось с одобренным — либо разошлось, и
|
||||
чекпоинт был пройден заново;
|
||||
4. change заархивирован, дельты влиты в актуальные спеки;
|
||||
5. коммит сделан в текущую ветку;
|
||||
6. **критерии приёмки, если проект их дал, выписаны поимённо, и по каждому назван
|
||||
оракул и наблюдаемый исход** — «прогнал вот это, увидел вот то». Это **доклад,
|
||||
а не сертификация: приёмка — не работа этого скилла.** Исполнитель, ставящий
|
||||
себе галочку «принято», проверяет свою работу своим же взглядом — по границе
|
||||
это может делать только приёмщик, разведённый с исполнителем. Критерии приходят
|
||||
снаружи; скилл их не сочиняет и не занижает. Расхождение «по каждому критерию
|
||||
исход есть, а суть задачи не достигнута» — дефект критериев, и о нём
|
||||
сообщается, а не молча дорабатывается.
|
||||
|
||||
У разведки, кончившейся знанием, определение своё и короткое: **ответ записан по
|
||||
адресу, который назвала задача**, и в нём есть провенанс у каждого числа.
|
||||
|
||||
## Исследовательская ветка
|
||||
|
||||
### Р1. Понять вопрос
|
||||
|
||||
Прочитай запись. У типа `research` в ней два обязательных раздела, и оба нужны
|
||||
тебе прямо сейчас: **«Вопрос»** — на что отвечаем, **«Куда ляжет ответ»** — по
|
||||
какому адресу он ляжет. Адрес назван заранее не из аккуратности: ответ, не
|
||||
имеющий дома, остаётся в переписке, и через квартал разведку заказывают заново.
|
||||
|
||||
Вопроса нет — исход «не доведена» с причиной «запись это сырьё»: назови, что
|
||||
нужно дописать, и остановись. Адреса нет, а вопрос есть — назначь адрес сам и
|
||||
скажи об этом строкой: разведка без дома для ответа хуже несделанной.
|
||||
|
||||
Задача не из каталога (пришла текстом) — адреса у неё нет по построению. Тогда
|
||||
дом ответа — `design.md` того change, который родится дальше; кода не будет —
|
||||
`docs/research/`, и это тоже говорится строкой.
|
||||
|
||||
### Р2. Груминг — `opsx:explore`
|
||||
|
||||
Вызови Skill `opsx:explore`. Читай документы проекта, а не только запись:
|
||||
граница домена и «чем проект **не** является» из паспорта отсекают половину
|
||||
вариантов до того, как их начнут сравнивать. **В explore не пишем код.**
|
||||
|
||||
Развилку грумминга **не записывай вопросом** — она и есть предмет следующего
|
||||
шага. Это отличие от обычной ветки: там развилка уходит в запись, здесь она
|
||||
копится в чекпоинт.
|
||||
|
||||
### Р3. Чекпоинт: варианты
|
||||
|
||||
**Остановись и покажи человеку способы решить.** Это первый из двух плановых
|
||||
стопов, и он существует потому, что после `propose` выбор уже сделан
|
||||
предложением.
|
||||
|
||||
Форма — короткая, экран текста:
|
||||
|
||||
- **вопрос**, на который отвечаем, одной фразой (он уже есть в записи);
|
||||
- **2–4 варианта**, не больше. Больше четырёх — это не выбор, а список: человек
|
||||
не сравнит, а признает свою неспособность сравнить и попросит рекомендацию.
|
||||
У каждого варианта: в чём суть простыми словами, **что даёт**, **чего стоит**,
|
||||
**что становится невозможным** (это ловится хуже всего и стоит дороже всего);
|
||||
- **рекомендация с причиной**. Человек чаще соглашается, чем выбирает заново;
|
||||
- что известно **недостоверно** и как это проверить, если проверять дёшево.
|
||||
|
||||
Что нельзя: приносить варианты, различающиеся только реализацией; прятать
|
||||
отвергнутое (отвергнутый вариант с названной причиной — половина будущего ADR);
|
||||
приносить один вариант и называть это выбором.
|
||||
|
||||
**Выбор оседает по адресу.** У `research` это раздел «Куда ляжет ответ». У
|
||||
остальных — `design.md` change, который родится на шаге 2, разделом
|
||||
«рассмотренные варианты». Не в переписку: разговор, из которого ничего не
|
||||
записано, повторяется через месяц целиком.
|
||||
|
||||
Три исхода чекпоинта:
|
||||
|
||||
- **выбран способ** — идёшь на шаг 1 общей ветки;
|
||||
- **ответ и есть исход** — работа кончается знанием: запиши ответ по адресу,
|
||||
доложи исход «знание вместо изменения» и закрой задачу (шаг 11). Провенанс у
|
||||
каждого числа обязателен: число без источника проход ревью обязан читать как
|
||||
условие, а не как замер;
|
||||
- **разведка родила задачи** — исход «оказалась крупнее задачи». Нарезка не твоя
|
||||
работа: отдай список формулировками и остановись.
|
||||
|
||||
## Шаги
|
||||
|
||||
### 1. Прочитать задачу
|
||||
|
||||
Прочитай запись и связанные спеки и черновики.
|
||||
|
||||
Проект даёт задаче **критерии приёмки** — выпиши их сразу: на шаге 2 они уезжают
|
||||
в `tasks.md` change. Файл задачи может быть удалён до коммита, а критерии обязаны
|
||||
его пережить.
|
||||
|
||||
Здесь же проверка на «крупнее задачи»: видно, что одним заходом это не
|
||||
мерджится, — объявляй исход **до** заведения change.
|
||||
|
||||
### 2. Завести change — `opsx:propose`
|
||||
|
||||
Вызови Skill `opsx:propose`. Получаем `proposal.md`, дизайн, дельта-спеки
|
||||
(`ADDED`/`MODIFIED`/`REMOVED Requirements`), `tasks.md`. Каждое `### Requirement`
|
||||
содержит `SHALL`/`MUST`; структурные заголовки английские, сценарии
|
||||
`GIVEN/WHEN/THEN`. Прогони `openspec validate --strict <id>`.
|
||||
|
||||
Критерии приёмки задачи, если они были, копируются в `tasks.md` отдельным блоком.
|
||||
Варианты, разобранные на чекпоинте Р3, — в `design.md`, с причиной отказа по
|
||||
каждому отвергнутому.
|
||||
|
||||
**`proposal.md` пишется так, чтобы его понял человек, не читавший спек.** Это не
|
||||
стилистическое пожелание: из него собирается чекпоинт шага 5, и переписывать его
|
||||
там заново значит завести второй дом для одного объяснения. Требование стоит в
|
||||
`openspec/config.yaml`, `rules.proposal` — то есть применяется в момент
|
||||
порождения артефакта, а не вспоминается после.
|
||||
|
||||
### 3. Разметка задачи — агент `review-scope`
|
||||
|
||||
**Один запуск на всю задачу, и он обслуживает оба чекпоинта ревью.** Запусти
|
||||
агента `review-scope`, дав ему корень проекта, идентификатор change, базу диффа и
|
||||
запись задачи. Кода на этот момент нет, и это условие его работы, а не помеха.
|
||||
|
||||
Он возвращает **план задачи**:
|
||||
|
||||
- **размер** (малое / среднее / крупное) и **сложность** (знакомое /
|
||||
незнакомое), каждое с обоснованием по факту;
|
||||
- **метку** как максимум по двум осям: `small`, `medium` или `large`;
|
||||
- **состав ревью дизайна** — что звать на шаге 4;
|
||||
- **таблицу тем** «тема → дом → глубина → кто закрывает» — для шага 7;
|
||||
- разнесение документов проекта по трём категориям и строку про директивы.
|
||||
|
||||
**Метку выбираешь не ты.** Раньше состав ревью дизайна называл сам оркестратор —
|
||||
то есть тот, кто только что довёл предложение до `propose`. Разведённости с
|
||||
автором в этой точке не было вовсе; теперь есть.
|
||||
|
||||
**План держи в контексте до конца задачи.** На диск он не пишется: файл-план стал
|
||||
бы четвёртым артефактом рядом с `proposal.md`, `tasks.md` и `design.md`, пережил
|
||||
бы задачу и разошёлся бы с ней молча. Прервался прогон — повтори шаг 3, это самый
|
||||
дешёвый его проход.
|
||||
|
||||
**Разметка повторяется ровно в одном случае** — если правки изменили сами
|
||||
**дельта-спеки**: план выведен из них, и план по отменённым требованиям назовёт
|
||||
не те темы. Во всех прочих случаях, включая переделку формы кода на шаге 7,
|
||||
метка остаётся прежней.
|
||||
|
||||
### 4. Ревью дизайна — ДО кода, состав по метке
|
||||
|
||||
Вызови Skill **`av-dev-code:review`**, дав ссылку на change `<id>`,
|
||||
**план разметки с шага 3** и указание, что это ревью дизайна.
|
||||
|
||||
Состав приходит планом, а не решается здесь:
|
||||
|
||||
| Метка | Проходы на предложении |
|
||||
|---|---|
|
||||
| `small` | `specs` |
|
||||
| `medium` | `specs`, `rubric` |
|
||||
| `large` | `specs`, `rubric`, `architecture` + вопрос автору о трёх формах решения |
|
||||
|
||||
`review-specs` в режиме «дизайн ДО кода» идёт **на каждой задаче**: это самый
|
||||
дешёвый чекпоинт конвейера, и он ловит то, что на готовом коде уже не чинят.
|
||||
Остальные включаются меткой, потому что стадия стоит на каждой задаче и каждый
|
||||
лишний проход здесь умножается на число задач.
|
||||
|
||||
Смысл стадии: архитектурная находка на готовом коде стоит переписывания и потому
|
||||
игнорируется — та же находка здесь стоит абзаца обсуждения. Если `review-rubric`
|
||||
запускался, перенеси его рубрику в `tasks.md` как приёмочные критерии; там же уже
|
||||
лежат критерии от постановки, если они были.
|
||||
|
||||
**Отработка замечаний, и она идёт до чекпоинта, а не после:**
|
||||
|
||||
- мелочь и явные улучшения — правь сам в спеках и дизайне;
|
||||
- развилки (компромисс, scope, инвариант) — **не в запись, а в чекпоинт**: он
|
||||
следующим шагом, и это ровно то, ради чего он поставлен здесь;
|
||||
- после правок перепрогони `openspec validate --strict <id>`.
|
||||
|
||||
### 5. Чекпоинт: объяснение
|
||||
|
||||
**Остановись и объясни человеку, что происходит.** Второй плановый стоп и
|
||||
единственный обязательный для всех задач.
|
||||
|
||||
Он стоит **после** ревью дизайна намеренно. Человек читает объяснение, уже
|
||||
просеянное машиной: то, что поймал бы `review-specs`, до него не доходит, а
|
||||
внимание — самый дорогой ресурс процесса, и тратить его на выловимое машиной
|
||||
нельзя.
|
||||
|
||||
**Объяснение не сочиняется заново — оно собирается из артефактов**, `proposal.md`
|
||||
и `design.md`. Третий пересказ был бы третьим домом одного и того же и разошёлся
|
||||
бы с обоими. Что показываешь:
|
||||
|
||||
- **в чём проблема** — словами домена из паспорта, без имён модулей и функций;
|
||||
- **как решаем** — суть одним абзацем. Не пересказ дельта-спек: спека нормирует,
|
||||
а здесь объясняют;
|
||||
- **что человек увидит иначе**, когда это будет сделано;
|
||||
- **чего мы намеренно не делаем** и почему — граница scope ловится хуже всего;
|
||||
- **чем рискуем и что осталось нерешённым** — сюда съезжаются развилки,
|
||||
накопленные до этого места, и находки ревью с пометкой `развилка`;
|
||||
- **что дальше**, если возражений нет.
|
||||
|
||||
Проверка на «простой язык» одна и механическая: **в тексте нет `SHALL`, нет имён
|
||||
файлов и функций там, где вещь называется по-русски, и нет слов, которых нет в
|
||||
паспорте проекта.** Не проходит — переписывай, а не объясняй, почему иначе
|
||||
нельзя.
|
||||
|
||||
Длина — экран. Длинное объяснение не читают, а пролистывают, и чекпоинт
|
||||
превращается в ритуал одобрения.
|
||||
|
||||
Три исхода:
|
||||
|
||||
- **согласен** — идёшь на шаг 6;
|
||||
- **скорректировать** — правишь спеки и дизайн по сказанному. Изменились
|
||||
**дельта-спеки** — повтори шаг 3 (разметка выведена из них) и ту часть ревью
|
||||
дизайна, которой касается правка; затем чекпоинт **заново**. Правка внутри
|
||||
дизайна без спек — повтори только чекпоинт;
|
||||
- **не одобрено** — исход «не доведена» с причиной. Change остаётся
|
||||
незаархивированным, задача не закрывается, ничего не коммитится наполовину.
|
||||
|
||||
### 6. Написать код — `opsx:apply`
|
||||
|
||||
Вызови Skill `opsx:apply` для реализации `tasks.md`. Код — по конвенциям проекта
|
||||
(каталог `docs/conventions/`). Меняешь схему — обнови её описание в документации
|
||||
тем же change, если проект этого требует: гейт обычно это проверяет.
|
||||
|
||||
Прогони гейт и добейся зелёного — он же гейт следующего шага.
|
||||
|
||||
**Поведенческая верификация.** Если задача меняет реальное поведение (новый
|
||||
эндпоинт, разбор входа, схема, форма ответа) — зелёных юнит-тестов мало. Подними
|
||||
изменение вживую командой из раздела команд `CLAUDE.md` и прогони сценарий.
|
||||
Пропусти только для чисто внутренних правок без наблюдаемого рантайма.
|
||||
|
||||
**Сервис не оставляем лежать.** Если запуск упал — почини или откати до конца
|
||||
шага.
|
||||
|
||||
### 7. Ревью кода — та же метка
|
||||
|
||||
Вызови Skill **`av-dev-code:review`**, дав ссылку на change `<id>`,
|
||||
базу диффа, **план разметки с шага 3** и режим запуска.
|
||||
|
||||
**Метку ты не выбираешь, и это правило, а не упрощение.** Её назвал
|
||||
`review-scope` ещё на шаге 3 — по размеру и сложности, с обоснованием по каждой
|
||||
оси. Причина в разведённости: ты только что написал этот код, и решать, насколько
|
||||
глубоко его проверять, тебе нельзя — под давлением «я почти закончил» решение
|
||||
известно заранее. Правило выбора живёт в скилле конвейера —
|
||||
`av-dev-code:review`, `references/review-levels.md`; проектные
|
||||
триггеры — в `docs/review.*`, подраздел «Триггеры метки».
|
||||
|
||||
**Метка не пересматривается по факту диффа.** Дифф может выйти крупнее, чем
|
||||
ожидалось при разметке, — это не повод её поднимать: пересмотр означал бы второй
|
||||
запуск разметчика, ровно то, ради устранения чего он и переехал на шаг 3.
|
||||
|
||||
**Считаешь метку заниженной — скажи это в докладе строкой, а не переспорь.**
|
||||
Разметчик вправе и поднять, и понизить; твоё несогласие это факт для человека, а
|
||||
не команда конвейеру. Место, где такое несогласие превращается в изменение
|
||||
правил, — журнал дефектов `docs/review.md`, и только постфактум.
|
||||
|
||||
**Плана нет — ревью кода не запускается.** Триаж требует план обязательным
|
||||
входом: без него он не может сверить, все ли размеченные темы вернули отчёт, а
|
||||
эта сверка — единственная защита от молчащего пропуска. Потерял план (прервалась
|
||||
сессия, ушёл контекст) — повтори шаг 3, а не гони прогон без него.
|
||||
|
||||
**Режим по умолчанию — `по графу`, и обосновывать его не надо.** Конвейер сам
|
||||
знает свои рёбра: гейт открывает опиниативные проходы, проходы с пометкой «держит
|
||||
машину» идут цепочкой (иначе замеры портят друг друга и находка выглядит
|
||||
доказанной), триаж — сток. Просить **`линейно`** нужно только по причине, и она
|
||||
называется строкой: так сказал оператор; машина занята чем-то ещё; идёт разбор
|
||||
самого конвейера.
|
||||
|
||||
Скилл сам гоняет гейт, нужные проходы и обязательный триаж. Возвращает отчёт с
|
||||
потолком 7 пунктов, разметкой `Действие: инлайн | развилка` и секцией границ
|
||||
покрытия.
|
||||
|
||||
**Сверь план прогона с исходом, прежде чем коммитить.** Отчёт начинается планом
|
||||
разметчика — таблицей «тема → дом → глубина → кто закрывает», — и против каждой
|
||||
темы обязан стоять исход. Тема без отчёта и тема без дома — разные вещи, и обе
|
||||
должны быть названы. Реестр короткий (шесть тем ядра плюс свои) — сверка стоит
|
||||
одного взгляда.
|
||||
|
||||
#### Отработка, и здесь появляется одно новое правило
|
||||
|
||||
Помеченное `инлайн` чини сам и не логируй. `развилка` — вопросом в запись (он уже
|
||||
сформулирован триажем, его остаётся перенести). После правок — снова гейт.
|
||||
|
||||
**Находка, отменяющая одобренный дизайн, отменяет и одобрение.** Признак
|
||||
проверяемый: **меняются ли дельта-спеки**.
|
||||
|
||||
- не меняются — находка внутри дизайна, дожимай сам, это обычная отработка;
|
||||
- меняются — решение стало другим, а одобрено было прежнее. Повтори шаг 3
|
||||
(разметка выведена из дельта-спек) и **вернись на чекпоинт шага 5** с тем, что
|
||||
изменилось и почему.
|
||||
|
||||
Тихо переделать утверждённое нельзя. Чекпоинт, который можно обойти находкой
|
||||
ревью, не значит ничего, а человек при этом уверен, что одобрил именно то, что
|
||||
уехало в коммит.
|
||||
|
||||
**Урожай — списком, не задачами.** Отложенные находки (реальный `major` не для
|
||||
этого мерджа, развилка, решённая «потом», пачка `nit`) собери в секцию доклада
|
||||
`Урожай`: формулировка, оракул, провенанс. Задачи из него **заводит не этот
|
||||
скилл** — у того, кто ведёт задачи проекта, своя нарезка, свой формат и свои
|
||||
правила дублей. Твоя обязанность — не потерять и передать.
|
||||
|
||||
**Границы покрытия из отчёта не выбрасывай** — они уезжают в финальный доклад
|
||||
сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно»,
|
||||
превращается в ложное ощущение проверенности.
|
||||
|
||||
**Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`; шаг 8
|
||||
унесёт его в архив вместе с change) — это обязательно, а не «если удобно».** По
|
||||
нему потом видно, что было найдено и что из этого осталось в урожае. И это
|
||||
единственный **независимый** артефакт о составе прогона: своей прозе здесь верить
|
||||
нельзя — она написана тем же, кто мог проход и пропустить.
|
||||
|
||||
### 8. Архивировать — `opsx:archive`
|
||||
|
||||
Вызови Skill `opsx:archive`: change уезжает в архив, дельты вливаются в
|
||||
актуальные спеки. Не пропускай `openspec validate --strict` перед этим.
|
||||
|
||||
### 9. Синк документации
|
||||
|
||||
**Вызови Skill `av-dev-docs:docs`**: он владеет содержимым документов канона и
|
||||
ведёт чек-лист синка.
|
||||
|
||||
**Правило одно и оно жёсткое: принуждённое отрицание.** Доклад обязан назвать
|
||||
**каждый** документ канона — либо чем он обновлён, либо «не требуется, потому
|
||||
что…». Нетронутые группируются одной строкой с общей причиной. Список триггеров
|
||||
прозой уже проверен на живом проекте и дал 6 записей ADR на 43 изменения;
|
||||
работает только обязательное отрицание.
|
||||
|
||||
**Список документов и их триггеров здесь не дублируется** — он в чек-листе скилла
|
||||
`av-dev-docs:docs`, и копия уже однажды разошлась с оригиналом, потеряв два
|
||||
триггера.
|
||||
|
||||
**Плагина `av-dev-docs` в проекте нет** — путь в его дерево не разрешится ниоткуда,
|
||||
поэтому за списком иди в **свой** reference:
|
||||
[references/project-facts.md](../review/references/project-facts.md)
|
||||
конвейера ревью перечисляет все документы канона с их предметом. Пройди по этому
|
||||
перечню — каждый документ получает строку, отрицание остаётся обязательным.
|
||||
Триггеры при этом ты знаешь хуже, и это называется в докладе строкой: «синк
|
||||
сделан по перечню документов, без списка триггеров — плагина `av-dev-docs` нет».
|
||||
Канона в проекте тоже нет — назови это исходом и предложи `av-dev-docs:canon`.
|
||||
|
||||
### 10. Коммит
|
||||
|
||||
Коммить **в текущую ветку** (`git rev-parse --abbrev-ref HEAD`), сам ветку не
|
||||
создавай и не переключай, ничего не пушь.
|
||||
|
||||
Сообщение — по-русски, скиллом `av-dev-git:commit`, если он подключён (первая
|
||||
строка «что сделано», тело списком 1–3 пункта, без трейлеров). Одна задача — один
|
||||
осмысленный коммит.
|
||||
|
||||
### 11. Закрыть задачу — **после коммита, не раньше**
|
||||
|
||||
**Вызови Skill `av-dev-tasks:tasks`** и попроси закрыть задачу как реализованную —
|
||||
он владеет форматом и двигает строку индекса сам. Путь к его скрипту не выясняй и
|
||||
индексы руками не правь: мост между плагинами — вызов скилла, а не путь.
|
||||
|
||||
**Порядок обязателен.** Закрытие удаляет файл задачи; сделанное до коммита оно
|
||||
оставило бы задачу закрытой без единого следа работы, если шаг 10 упадёт.
|
||||
|
||||
**Закрытие тоже коммитится — вторым коммитом, тут же.** Удаление файла задачи и
|
||||
правка индексов (их имена знает `av-dev-tasks`, не ты) — это правки в рабочем
|
||||
дереве, и оставить их незакоммиченными нельзя: закрытие, не доехавшее до основной
|
||||
ветки, оставит задачу открытой молча, а опора «индексы под git показывают, что и
|
||||
когда закрыто» без коммита — пустые слова. Сообщение короткое, про учёт, а не про
|
||||
работу: `закрыта задача <slug>`. Это второй коммит осознанно: правило «одна задача
|
||||
— один осмысленный коммит» про работу, а учёт — не работа.
|
||||
|
||||
Разведка, кончившаяся знанием, закрывается так же — но перед этим убедись, что
|
||||
ответ **записан по названному адресу и закоммичен**. Закрытая разведка без
|
||||
записанного ответа не оставляет следа вообще: файл задачи удалён, ответ был в
|
||||
переписке.
|
||||
|
||||
Плагина в проекте нет — вызов не разрешится. Тогда **ничего не выдумывай**: скажи
|
||||
в докладе, что учёт задач остаётся за владельцем, и назови исход.
|
||||
|
||||
## Доклад
|
||||
|
||||
Коротко, и в нём обязательно:
|
||||
|
||||
- **исход** одним из четырёх слов и, если не «сделана», чем ограничен результат;
|
||||
- что сделано, какие вопросы записаны и куда;
|
||||
- **что было одобрено на чекпоинте и совпало ли с тем, что уехало в коммит** —
|
||||
расхождение здесь называется прямо, даже если оно мелкое;
|
||||
- ссылка на архивный change и хеш коммита;
|
||||
- по каждому критерию приёмки, если они были: **оракул и наблюдаемый исход** —
|
||||
это доклад приёмщику, а не отметка «принято»;
|
||||
- **`Урожай`** — отложенные находки списком (формулировка, оракул, провенанс);
|
||||
- **одна строка границ покрытия**: какая метка и режим гонялись, какие проходы не
|
||||
запускались и что проверить было невозможно. Доклад без неё сообщает
|
||||
«проверено», не сообщая, что именно.
|
||||
|
||||
## Тонкости
|
||||
|
||||
- **Не завязывайся на основную ветку и корень репозитория.** Скилл работает в
|
||||
текущем worktree и на текущей ветке: не делай `git checkout`/`switch`, не
|
||||
создавай веток, не пушь.
|
||||
- Обычная задача с очевидным решением проходит **один** чекпоинт, и это норма, а
|
||||
не упрощение. Два чекпоинта — цена незнания, а не признак серьёзности задачи.
|
||||
- Гейт блокирует: пока он красный, опиниативные проходы не запускаются. Чинить и
|
||||
перезапускать, а не «посмотреть заодно».
|
||||
- Держи вызывающего в цикле короткими репликами на переходах фаз, но не проси
|
||||
подтверждать механику: чекпоинты — единственные места, где ждут ответа.
|
||||
- **Занизить метку ревью, пропустить тему или проскочить чекпоинт — самый дешёвый
|
||||
способ «ускориться», и он же самый дорогой по последствиям.** Защита устроена
|
||||
так, что регулятора у тебя нет: метку выбирает разметчик **до того**, как ты
|
||||
написал код, план сверяется по темам, непокрытое называется строкой, а
|
||||
расхождение с одобренным — отдельным пунктом доклада.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
# Калибровка проходов
|
||||
|
||||
Без измерения набор проходов растёт монотонно и вырождается в театр: каждый
|
||||
кажется полезным, потому что иногда что-то говорит. Калибровка отвечает на
|
||||
единственный вопрос — **ловит ли проход дефект своего класса**.
|
||||
|
||||
## Процедура (инъекция дефекта)
|
||||
|
||||
1. Взять **реальный коммит** из истории (`git log --oneline`), лучше
|
||||
архивированный change с непустым диффом.
|
||||
2. Внести в него **один** дефект того класса, который проход обязан ловить по
|
||||
своему charter'у. Дефект должен быть правдоподобным — таким, какой реально
|
||||
пишет модель, а не карикатурой (`panic("TODO")` не считается).
|
||||
3. Прогнать **только этот проход** на подготовленном диффе — **три раза**,
|
||||
каждый в чистом контексте.
|
||||
4. Зафиксировать: нашёл `n/3`, число находок всего, число ложных.
|
||||
5. Вердикт:
|
||||
|
||||
| Результат | Вердикт | Что делаем |
|
||||
|---|---|---|
|
||||
| нашёл 3/3 или 2/3, ложных немного | `keep` | ничего |
|
||||
| нашёл 1/3 или 0/3 | `retune` | правим charter — сужаем вход, убираем чек-лист, добавляем оракул |
|
||||
| `retune` уже был дважды подряд | `drop` | удаляем проход |
|
||||
| находит, но ложных больше трети от всех находок | `retune` | триаж съедает больше, чем экономит проход |
|
||||
|
||||
Вердикты образуют храповик со счётчиком — его-то таблица и не показывает:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
state "проход в составе метки" as live
|
||||
state "retune №1 — правка charter'а" as r1
|
||||
state "retune №2 — последняя попытка" as r2
|
||||
state "проход удалён" as dead
|
||||
|
||||
[*] --> live: заведён и откалиброван ДО включения
|
||||
live --> r1: 1/3, 0/3 или ложных больше трети
|
||||
r1 --> live: замер keep — счётчик сброшен
|
||||
r1 --> r2: снова не ловит
|
||||
r2 --> live: замер keep — счётчик сброшен
|
||||
r2 --> dead: снова не ловит — это театр
|
||||
```
|
||||
|
||||
Схема — **сводка** к таблице вердиктов выше: она добавляет только счётчик, и при
|
||||
расхождении прав таблица.
|
||||
|
||||
**`retune` не более двух раз подряд.** Проход, не находящий дефект своего класса
|
||||
в 2 из 3 прогонов после двух правок промпта, — это театр. Удалять, а не
|
||||
бесконечно править формулировки: каждая итерация правки промпта стоит дороже,
|
||||
чем отсутствие прохода.
|
||||
|
||||
**Существующий проход не удаляется без замера.** Сначала калибровка, потом
|
||||
решение — иначе удаляется то, что работало, а остаётся то, что громче. Обратный
|
||||
пример уже был: проход про идиоматичность стоял в списке на удаление как
|
||||
«вкусовщина», а замер показал, что он зарабатывает **экспериментами против
|
||||
поведения библиотеки и драйвера**, — и находка, воспроизведённая числом, отменила
|
||||
решение, принятое по ощущению.
|
||||
|
||||
## Состав проходов принадлежит плагину, а не проекту
|
||||
|
||||
Проходы общие. Проект не может удалить проход — он может **не звать** его, и
|
||||
тогда это идёт строкой «не запускался» в границы покрытия, как любой другой
|
||||
пропуск. Молча сузить состав нельзя: пропуск прохода не отличим от прохода без
|
||||
находок.
|
||||
|
||||
Отсюда два следствия:
|
||||
|
||||
- **правка charter'а — правка для всех проектов.** Прежде чем сужать
|
||||
формулировку под свою боль, проверь, не место ли ей в документах проекта: предмет проверки
|
||||
живёт там, метод — в charter'е;
|
||||
- **удаление прохода из плагина требует замера на двух проектах**, а не на одном:
|
||||
класс, не всплывший здесь, мог быть единственным работающим там.
|
||||
|
||||
## Пробы дефектов по проходам
|
||||
|
||||
Проба — заготовка инъекции. Список пополняется из журнала проскочивших дефектов
|
||||
(см. [review-journal.md](review-journal.md)): реальный проскочивший дефект —
|
||||
лучшая проба, какая вообще возможна, потому что синтетические смещены в сторону
|
||||
тех, которые уже умеешь придумывать.
|
||||
|
||||
| Проход | Класс дефекта для инъекции | Заготовка пробы |
|
||||
|---|---|---|
|
||||
| `review-scope` | пропущенная тема | положить в `docs/` новый документ и проверить, попал ли он в план темой |
|
||||
| `review-autotests` | отсутствующая верификация | убрать тест на изменённую ветку, оставить код рабочим |
|
||||
| `review-specs` | поведение вне спеки | добавить незаказанный фолбэк-дефолт на пустом входе |
|
||||
| `review-code` | нарушение прозаической конвенции | увести штатный отказ мимо единой точки трансляции ошибки |
|
||||
| `review-code` | технический дефект | не проверить возвращённую ошибку в ветке раннего возврата |
|
||||
| `review-rubric` | нарушенное свойство узла | у клиента внешнего сервиса убрать таймаут и протяжку `context` |
|
||||
| `review-basics` | отказ, видимый чтением | убрать обработку ошибки записи так, чтобы отказ считался успехом |
|
||||
| `review-basics` | своя тема проекта | нарушить правило из документа, у которого нет именного прохода |
|
||||
| `review-architecture` | второй способ | завести вторую точку генерации id мимо единой |
|
||||
| `review-adversary` | построенный путь | принять внешний идентификатор без разбора до запроса в хранилище |
|
||||
| `review-ops` | деградация окружения | убрать обработку недоступности внешней зависимости в фоновом цикле |
|
||||
| `review-triage` | шум | подать 20 находок, из них 15 вкусовщина и 3 дубля — проверить потолок и дедуп |
|
||||
|
||||
Метрик сверх этого не заводим. Precision, корреляция между проходами, стоимость
|
||||
прогона в токенах — всё это красиво звучит и никем не считается вручную; набор
|
||||
показателей, который не собирают, создаёт впечатление измеряемости и тем вреден.
|
||||
Работает ровно один механизм: инъекция дефекта и вердикт. Если корреляция двух
|
||||
проходов действительно бросается в глаза — это видно по полю `Найдено проходом`
|
||||
в триажированных отчётах и без отдельной метрики.
|
||||
|
||||
## Когда калибровать
|
||||
|
||||
- при заведении нового прохода — **до** включения в состав метки по умолчанию;
|
||||
- при правке charter'а существующего — иначе непонятно, правка помогла или нет;
|
||||
- при появлении записи в журнале проскочивших дефектов — калибруем тот проход,
|
||||
который должен был поймать;
|
||||
- планово — нет. Календарная калибровка ради галочки сама превращается в театр.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Контракт находок
|
||||
|
||||
Единый формат для всех проходов конвейера ревью. Проход, нарушивший контракт,
|
||||
считается сломанным — триаж вправе выбросить его вывод целиком.
|
||||
|
||||
## Форма находки
|
||||
|
||||
```
|
||||
### <краткая формулировка ПОСЛЕДСТВИЯ, не симптома>
|
||||
- Файл: internal/<пакет>/<файл>.go:120-134
|
||||
- Severity: critical | major | minor | nit
|
||||
- Confidence: high | medium | low
|
||||
- Оракул: <падающий тест / команда с выводом / положение руководства / нет>
|
||||
- Последствие: <что произойдёт и при каких условиях>
|
||||
- Предложение: <конкретное изменение>
|
||||
- Найдено проходом: <имя агента; у проходов с раздельными потолками — имя и половина, например `code/техника`>
|
||||
```
|
||||
|
||||
## Правила
|
||||
|
||||
- **Заголовок через последствие.** Не «нет проверки токена», а «читатель без
|
||||
токена выгрузит всю историю». Не «слияние перезаписывает запись», а «повторная
|
||||
доставка сотрёт поля у уже сохранённой записи, и восстановить их нечем».
|
||||
Симптом в заголовке — это заявка на то, что читатель сам достроит последствие;
|
||||
он не достроит, он просто починит симптом.
|
||||
- **`critical` без оракула или построенного пути не существует.** Оракул — это
|
||||
падающий тест, вывод выполненной команды или поимённое положение руководства. Не
|
||||
«вероятно, здесь гонка», а прогон детектора гонок с его выводом.
|
||||
- **`confidence: low` — это «так обычно пишут».** Такие находки допустимы, но не
|
||||
поднимаются выше `minor`. Частотность конструкции в публичном коде — не
|
||||
аргумент.
|
||||
- **Находка без поля «Последствие» не выводится вовсе.** Пустое «Последствие:
|
||||
ухудшает читаемость» равносильно отсутствию поля.
|
||||
- **`nit` допустим только при нарушении записанной конвенции** — со ссылкой на
|
||||
файл и раздел конвенций проекта (`docs/conventions/`) либо на
|
||||
правило линтера. Если правило механизируемо, но не механизировано — это не
|
||||
находка ревью, это `Promote candidate` (см. [promote.md](promote.md)).
|
||||
- **`critical` по основанию «нарушен инвариант проекта» требует инвариантов.**
|
||||
Ссылка идёт на пункт раздела инвариантов `CLAUDE.md` дословно. Без них основание
|
||||
недоступно — см. [project-facts.md](project-facts.md), поразрядная деградация.
|
||||
- **Расхождение — не дефект, пока не названо последствие.** Особенно для
|
||||
архитектурного прохода: «я бы сделал иначе» без последствия не выводится.
|
||||
|
||||
## Шкала severity
|
||||
|
||||
| Severity | Что это | Пример |
|
||||
|---|---|---|
|
||||
| `critical` | нарушение инварианта проекта, потеря или порча данных, утечка секрета, построенный путь к отказу | запись потеряна при слиянии; тело пользовательской выгрузки в поле лога |
|
||||
| `major` | сломанное требование дельта-спеки, необрабатываемый отказ штатного сценария, флаки-тест, поведение вне спеки, меняющее исход | приём отвечает 200, не записав тело: доставка считается принятой, а данных нет |
|
||||
| `minor` | отступление от конвенции с реальной ценой, отсутствующая наблюдаемость, дублирование, которое разойдётся | ни одного чекпоинта на пути разбора: молчащая автоматизация неотличима от пустого потока |
|
||||
| `nit` | нарушение записанной конвенции без последствий за пределами чтения | `msg` с интерполяцией вместо константы |
|
||||
|
||||
Шкала привязана к обратимости, а не к громкости: класс «необратимо и молча»
|
||||
всегда весит больше класса «шумно и лечится повтором». Что здесь необратимо,
|
||||
говорит `CLAUDE.md` — что в этом проекте необратимо.
|
||||
|
||||
## Блок границ покрытия
|
||||
|
||||
Каждый проход завершает вывод этим блоком. Он не сокращается и не заменяется
|
||||
фразой «всё проверено».
|
||||
|
||||
```
|
||||
## Coverage of this pass
|
||||
- проверено: <что реально прочитано/запущено, с путями и командами>
|
||||
- не проверялось и почему: <бюджет, недоступный инструмент, вне входа>
|
||||
- принципиально недоступно этому проходу: <из charter'а агента>
|
||||
```
|
||||
|
||||
## Финальный отчёт триажа
|
||||
|
||||
Секции строго в этом порядке, потолок — 7 пунктов в первых двух:
|
||||
|
||||
1. `Блокирует мердж` (≤3, каждая с оракулом);
|
||||
2. `Стоит исправить сейчас` (≤4);
|
||||
3. `Гипотезы без доказательства` — что понижено и почему;
|
||||
4. `Promote candidates` — кандидаты в конвенцию или правило линтера;
|
||||
5. `Границы покрытия` — сводная, обязательная.
|
||||
|
||||
Перед секциями — сводка для человека: размер, сложность, метка и режим
|
||||
прогона, состояние гейта, **план разметки задачи с исходом по каждой теме**,
|
||||
сколько находок пришло на вход и сколько осталось.
|
||||
|
||||
**Реестр сводки — темы, а не проходы, и это не оформление.** Перечень запущенных
|
||||
проходов отвечает «все, кто должен был, отработали» и молчит о том, что именно
|
||||
осталось непроверенным: уехавший в старшую метку проход уносит тему с собой
|
||||
беззвучно. План же называет тему, её дом, глубину и исполнителя — и тема,
|
||||
оставшаяся без отчёта, видна сразу. Перечень проходов из сводки не исчезает, но
|
||||
идёт **внутри** плана, колонкой «кто закрывает».
|
||||
|
||||
Каждая находка в секциях 1–2 несёт дополнительное поле:
|
||||
|
||||
```
|
||||
- Действие: инлайн | развилка
|
||||
```
|
||||
|
||||
`инлайн` — оркестратор чинит сам, не спрашивая и не логируя. `развилка` — цена
|
||||
исправления сопоставима с переработкой, либо выбор меняет scope, либо решение
|
||||
трогает инвариант: уезжает вопросом с вариантами и ценой каждого туда, где
|
||||
проект держит вопросы, а работа продолжается на остатке.
|
||||
|
||||
Потребитель отчёта — оркестратор, который **реализует прочитанное**. Поэтому
|
||||
потолок в 7 пунктов — не забота о внимании читателя, а защита кодовой базы от
|
||||
правок, которых никто не заказывал.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Откуда проход берёт проектную конкретику
|
||||
|
||||
Конвейер общий, находки — проектные. Проход, не знающий, что в этом проекте
|
||||
нельзя нарушать, чем краснеет гейт и сколько данных реально идёт через узел,
|
||||
выдаёт правдоподобные общие места: их дорого опровергать и нечем подтверждать.
|
||||
|
||||
Отдельного файла-брифа **нет**. Проектная конкретика живёт в документах канона
|
||||
`av-dev-docs`, и проход читает их напрямую: пути жёсткие, посредник не нужен, а
|
||||
второй дом для тех же фактов разошёлся бы и выглядел актуальным.
|
||||
|
||||
Определение канона — в плагине `av-dev-docs`,
|
||||
`skills/canon/references/canon.md`. Здесь только карта «тема → её дом → что
|
||||
оттуда берётся».
|
||||
|
||||
## Карта тем
|
||||
|
||||
**Дом бывает файлом или каталогом** — `docs/security.md` и `docs/security/`
|
||||
называют одну и ту же тему. Форму дома называет план разметки задачи; проход её не
|
||||
угадывает.
|
||||
|
||||
| Тема | Дом | Что оттуда берётся |
|
||||
| --- | --- | --- |
|
||||
| `requirements` | `openspec/specs/`, `openspec/changes/<id>/specs/` | нормативное поведение и дельты изменения |
|
||||
| `autotests` | `CLAUDE.md`, семантика гейта | команда гейта, чем краснеет безусловно, чего в нём нет, кто гоняет дорогое |
|
||||
| `conventions` | `docs/conventions.*` | конвенции прозой и **что уже механизировано** правилом |
|
||||
| `architecture` | `docs/architecture.*` | компоненты и capability, единые точки проекта |
|
||||
| | источник `docs/passport.*` | что система делает и **чего не делает**, граница домена |
|
||||
| `security` | `docs/security.*` | периметр, недоверенный вход, из чего строятся пути и ключи, что вне модели |
|
||||
| `operations` | `docs/architecture.*`, раздел эксплуатации | окружение, внешние зависимости поимённо, наблюдатель, характер потока |
|
||||
| | источник `docs/database.*` | чем физически лежит запись, что при чтении и записи, настройки с числовым значением |
|
||||
| *тема проекта* | её **свой** документ в `docs/` | то, что проект счёл нужным записать |
|
||||
|
||||
**`docs/adr.*` и `docs/research.*` в этой карте нет намеренно.** Они процессные
|
||||
документы: прогон ревью их не открывает. Раньше первый питал тему `architecture`,
|
||||
второй — `operations` и `requirements`; обе строки убраны, и цена этого названа в
|
||||
`SKILL.md`, раздел «Честный предел».
|
||||
|
||||
**Дом темы зависит ещё и от метки.** На `small` темы `security`, `operations` и
|
||||
`architecture` смотрятся не против домов из этой таблицы, а против **инвариантов
|
||||
`CLAUDE.md`**, и закрывает их `code`. Таблица описывает полный дом темы; сколько
|
||||
из него открыто на этом прогоне, говорит план разметки задачи.
|
||||
|
||||
Сквозное, не привязанное к теме:
|
||||
|
||||
| Что нужно проходу | Где лежит |
|
||||
| --- | --- |
|
||||
| инварианты **с severity рядом с формулировкой** | `CLAUDE.md` (и `AGENTS.md`, если он рядом), раздел инвариантов |
|
||||
| что запускать запрещено, с путями; `testdata`; куда писать временное; имя основной ветки | `CLAUDE.md` |
|
||||
| типовые узлы, типовые ложноположительные, **вопросы по темам**, триггеры метки, недоступно проверке | `docs/review.*`, раздел настройки |
|
||||
| прецеденты: воспроизведённые дефекты с оракулом | `docs/review.*`, журнал |
|
||||
|
||||
**Вопросы проекта привязаны к теме, а не к имени прохода.** Раньше блок в
|
||||
`docs/review.md` адресовался поимённо (`ops: <вопрос>`), и когда проход уехал в
|
||||
старшую метку, вопрос перестал задаваться молча. Тема переезд прохода
|
||||
переживает.
|
||||
|
||||
## Сшивать обязаны проходы
|
||||
|
||||
Раньше эти факты лежали рядом в одном файле, и соседство работало само. Теперь
|
||||
они разложены по домам, и **проход обязан собрать их сам** — иначе снимет верное
|
||||
число и честно понизит находку до гипотезы, потому что сравнить будет не с чем.
|
||||
|
||||
Два обязательных стыка:
|
||||
|
||||
- **замер + настройка.** «Пик 768 МиБ» — аномалия только рядом со строкой
|
||||
«запись лежит сжатой и распаковывается целиком»; «блокировка удерживалась
|
||||
5.019 с» — гарантированный отказ соседа только рядом с известным таймаутом
|
||||
занятости. **Число проход снимает сам, на этом прогоне**, настройки берёт из
|
||||
`docs/database.md`, и сшивают их `ops` и `adversary`. Раньше числа брались из
|
||||
`docs/research/`; теперь этот документ процессный, и замер неизвестной свежести
|
||||
больше не выдаёт себя за оракул.
|
||||
- **инвариант + обратимость.** severity берётся из `CLAUDE.md`; если её там
|
||||
нет — она **выводится по обратимости последствия** и помечается «выведена по
|
||||
обратимости», а не выдаётся за решение проекта.
|
||||
|
||||
**У `basics` стыков нет, и это не упущение.** Он не меряет, поэтому сшивать число
|
||||
с настройкой ему нечего; единственное его основание для `critical` — инвариант из
|
||||
`CLAUDE.md`, всё остальное он формулирует условиями и оставляет гипотезой. Его
|
||||
вход намеренно узкий: дома тем из плана плюс инварианты и журнал. Широкий вход —
|
||||
это метка `large`, и там он есть у `architecture`. Греп по базе ему разрешён
|
||||
точечный — «есть ли второй вызывающий», — но обход всей базы и инвентарь
|
||||
концепций не его работа.
|
||||
|
||||
**У `scope` стыков нет по другой причине: он не читает содержимого.** Его дело —
|
||||
найти дома и раздать темы, а не пересказать написанное. Пересказ сделал бы его
|
||||
посредником между документом и проходом, а посредник расходится с источником и при
|
||||
этом выглядит актуальным.
|
||||
|
||||
## Деградация — поразрядная
|
||||
|
||||
Документа нет — деградирует то, что из него читалось, и **только оно**. Каждый
|
||||
проход пишет **свою** строку в границы покрытия; триаж собирает их в один
|
||||
список и **не сливает в одну строку**: разные пробелы чинятся разным — периметр
|
||||
пишется руками за десять минут, а числа требуют замера.
|
||||
|
||||
**Кто какой документ читает — из документа не выводится, а назначается планом.**
|
||||
Документ питает тему (это записано на стороне канона, таблица «Роли документов и
|
||||
темы ревью»), а тему на этом прогоне закрывает тот, кого назвала разметка задачи; вся
|
||||
раскладка «тема → проход → глубина» — в `SKILL.md` этого скилла и больше нигде.
|
||||
**Списка читателей не ведёт никто, и это не пробел.** Он жил бы на стороне
|
||||
канона, а документ живёт дольше, чем раскладка проходов: список разошёлся бы с
|
||||
конвейером молча и при этом выглядел актуальным. Однажды уже разошёлся.
|
||||
|
||||
Ниже — только **последствие** отсутствия дома, и оно называет самое дорогое, а не
|
||||
всех пострадавших.
|
||||
|
||||
| Нет дома | Что деградирует |
|
||||
| --- | --- |
|
||||
| `CLAUDE.md` без инвариантов | `critical` по основанию «нарушен инвариант проекта» не присваивается никем |
|
||||
| `docs/security.*` | тема `security` остаётся без дома: вопросы задаются по коду, `critical` не ставится, периметр неизвестен |
|
||||
| `docs/database.*` | замер не с чем сравнить: находка темы `operations` не поднимается выше гипотезы |
|
||||
| `docs/passport.*` | тема `architecture` теряет границу домена и вырождается в общее мнение |
|
||||
| `docs/review.*` | `triage` отсеивает вслепую: типовых ложноположительных нет; вопросы проекта по темам не задаются |
|
||||
| `docs/conventions.*` | вторая половина `code` идёт вхолостую: записанных конвенций нет |
|
||||
| `docs/architecture.*` | «не появился ли второй способ» не проверяется — единых точек не знает никто; тема `operations` теряет перечень внешних зависимостей |
|
||||
|
||||
Строка в границах покрытия обязана называть **причину**: «`docs/security.md` в
|
||||
проекте нет» читается иначе, чем «есть, но периметр не назван». Без причины
|
||||
строка неотличима от «мы просто не стали» и перестаёт читаться на третьей задаче.
|
||||
|
||||
**Документов канона нет вовсе** — проект не приведён к канону. Это не повод
|
||||
работать вслепую: скажи об этом строкой и предложи `av-dev-docs:canon`. Одна
|
||||
операция на проект против деградации на каждой задаче.
|
||||
|
||||
## Правило чтения
|
||||
|
||||
- **Читай в источнике, не по памяти.** Документы правятся по ходу работы, в том
|
||||
числе этой же задачей.
|
||||
- **Число без провенанса — условие, а не утверждение.** Число, чей источник по
|
||||
ссылке не подтвердился, читается как условие и **называется расходящимся**, а
|
||||
не подменяется догадкой.
|
||||
- **Пустое, названное пустым, — это факт.** «Внешних зависимостей нет — смотри
|
||||
на диск и на СУБД» экономит обязательный вопрос. Отсутствие строки — не факт,
|
||||
а пробел, и его надо назвать в границах покрытия.
|
||||
- **Свойство, ставшее правилом линтера, из конвенций удалено** и лежит в
|
||||
перечне механизированного в `docs/conventions/README.md`. Проверять его
|
||||
проходом — тратить внимание на уже проверенное.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Промоут: находка → конвенция → правило → удаление
|
||||
|
||||
Механизм храповика. Без него конвейер выдаёт одни и те же находки бесконечно, а
|
||||
конвенции не растут — то есть внимание тратится повторно на уже решённое.
|
||||
|
||||
Роли уровней:
|
||||
|
||||
- **generative-проходы** — механизм *открытия* неявного (дорого, шумно, но
|
||||
только они достают то, чего нет в списках);
|
||||
- **конвенции** — дешёвая *регрессионная сетка* на уже открытое;
|
||||
- **правила линтера** — то же с детерминированным оракулом и нулевой ценой
|
||||
внимания.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
f["находка ревью"]
|
||||
cond{"принята и не специфична<br/>для одного места?"}
|
||||
no["промоуту не подлежит:<br/>место одно — комментарий в коде;<br/>вкусовщина — вон на триаже;<br/>нужен рантайм — в журнал ревью"]
|
||||
conv["конвенция:<br/>проверяемое свойство + какой проход нашёл"]
|
||||
rule["правило линтера, запретитель,<br/>тест-сканер или анализатор"]
|
||||
clean["шаг 3: формулировка удалена из конвенций,<br/>строка — в conventions/README.md"]
|
||||
|
||||
f --> cond
|
||||
cond -->|нет| no
|
||||
cond -->|да| conv
|
||||
conv --> rule
|
||||
rule --> clean
|
||||
rule -->|"ложных чаще, чем ловит (~треть)"| conv
|
||||
```
|
||||
|
||||
Ребро назад — обратное движение (внизу): правило, дающее ложные срабатывания
|
||||
чаще, чем ловит, снимается в прозу. Ребро `rule → clean` **обязательное**: без
|
||||
него первые два шага не окупаются, а именно его и пропускают.
|
||||
|
||||
Схема — **сводка**: условия каждого шага в его разделе, и при расхождении прав
|
||||
текст.
|
||||
|
||||
## Шаг 1. Находка → конвенция
|
||||
|
||||
Условия: находка **принята** при ревью (не отвергнута, не понижена в гипотезу) и
|
||||
**не специфична для одного места**.
|
||||
|
||||
- Формулируется как **проверяемое свойство**, а не как совет: «уровень доменного
|
||||
отказа выбирает единственный логирующий чекпоинт», а не «внимательнее с
|
||||
уровнями логов».
|
||||
- Записывается источник — какой проход нашёл. Это единственные данные для
|
||||
калибровки: проход, чьи находки регулярно доезжают до конвенции, оправдан;
|
||||
проход, чьи находки не доезжают никогда, — кандидат на `drop` (см.
|
||||
[calibration.md](calibration.md)).
|
||||
- Место записи — конвенции проекта, файл или нужный файл каталога (путь — в
|
||||
каталог `docs/conventions/`). Если
|
||||
тема относится к поведению системы, а не к тому, как мы пишем код, — это не
|
||||
конвенция, а требование: заводится дельта-спека обычным путём.
|
||||
|
||||
Промоут идёт **тем же путём, что change → spec**: правка попадает в тот же
|
||||
коммит, что и исправление кода, с пометкой в сообщении — история промоутов
|
||||
остаётся видна в `git log` по файлу конвенций.
|
||||
|
||||
## Шаг 2. Конвенция → правило
|
||||
|
||||
Как только свойство выражается детерминированно, оно переезжает в инструмент.
|
||||
Порядок предпочтения — от дешёвого к дорогому:
|
||||
|
||||
1. **готовое правило существующего линтера** — включить в конфиг;
|
||||
2. **запрет идентификатора или импорта** правилом-«запретителем» с собственным
|
||||
паттерном;
|
||||
3. **правило с настройкой формы** — когда важно не имя, а конструкция;
|
||||
4. **тест-сканер исходников** — когда правило про структуру проекта или про
|
||||
схему: направление зависимостей, форма миграций, матчинг ошибки по тексту,
|
||||
бизнес-логика в транспорте;
|
||||
5. **собственный анализатор** — последний рубеж, заводим только если 1–4 не
|
||||
выражают правило.
|
||||
|
||||
Правило обязано быть **зелёным на текущем коде в момент включения**: иначе
|
||||
хук блокирует любой коммит, и правило снимут первым же раздражённым движением.
|
||||
Приводить код в соответствие — часть шага 2, отдельным коммитом.
|
||||
|
||||
## Шаг 3. Удаление из конвенций и из промптов
|
||||
|
||||
**Шаг, который пропускают чаще всего, и единственный, ради которого затевались
|
||||
первые два.**
|
||||
|
||||
Как только правило работает:
|
||||
|
||||
- из файла конвенций убирается формулировка правила; остаётся, если нужно, одна
|
||||
строка «проверяется линтером `<имя>`» — но только там, где без неё раздел
|
||||
теряет связность;
|
||||
- правило переезжает в **перечень механизированного в
|
||||
`docs/conventions/README.md`** — со ссылкой на место механизации: конфиг
|
||||
линтера, собственный анализатор, тест-сканер исходников. Не названное место
|
||||
означает, что проход будет добросовестно проверять уже проверенное;
|
||||
- из контекста инструмента спек убирается дубль, если он там был.
|
||||
|
||||
Charter'ы проходов при этом **не правятся**: они общие и живут в плагине, а
|
||||
предмет проверки приходит из документов проекта. Именно поэтому шаг 3 дешевле,
|
||||
чем был:
|
||||
вычеркнуть строку в одном файле проекта, а не в девяти промптах.
|
||||
|
||||
Практический критерий: **в прозаических конвенциях остаётся только то, что
|
||||
принципиально не выражается правилом.** Файл конвенций на несколько сотен строк
|
||||
размазывает внимание модели по тривиальному — она добросовестно проверит
|
||||
именование полей лога и не дойдёт до формы решения. Каждая строка конвенций,
|
||||
которую можно было бы проверить машиной, оплачивается дефектом, который не
|
||||
поймали где-то ещё.
|
||||
|
||||
## Обратное движение
|
||||
|
||||
Правило, которое даёт ложные срабатывания чаще, чем ловит (порядка трети от
|
||||
общего числа), снимается и возвращается в прозу — или удаляется совсем, если
|
||||
свойство перестало быть важным. Снятие фиксируется там же, где включалось, с
|
||||
одной строкой «почему».
|
||||
|
||||
## Что промоуту не подлежит
|
||||
|
||||
- Находка, специфичная для одного места (её лечит комментарий в коде).
|
||||
- Вкусовщина: не меняет поведения, не влияет на стоимость следующего изменения,
|
||||
не нарушает записанного. Такое выбрасывается на триаже и не хранится.
|
||||
- Свойство, требующее знания рантайма (профиль нагрузки, история инцидентов) —
|
||||
его нельзя проверить ни промптом, ни линтером; место такому — в журнале ревью
|
||||
как «признано неавтоматизируемым» (см. [review-journal.md](review-journal.md)).
|
||||
@@ -0,0 +1,106 @@
|
||||
# Журнал дефектов
|
||||
|
||||
Артефакт проекта, а не плагина: файл живёт в репозитории — **`docs/review.md`**,
|
||||
слот канона `av-dev-docs`. Здесь описано, зачем он и какой формы, потому что без
|
||||
него конвейер не учится: находки закрываются, а почему их не поймали — забывается,
|
||||
и один и тот же класс проскакивает второй раз.
|
||||
|
||||
Тот же файл держит **настройку конвейера под проект** — типовые узлы, типовые
|
||||
ложноположительные, вопросы по темам, недоступно проверке. Это не соседство по
|
||||
случаю: все четыре раздела — производные калибровки, а журнал им источник.
|
||||
|
||||
## Что туда попадает
|
||||
|
||||
**Воспроизведённый дефект — с пометкой `проскочил` или `пойман ревью`.**
|
||||
Записывается **сразу**, а не ретроспективно: со временем теряется не сам факт, а то,
|
||||
почему дефект не поймали, — единственное, ради чего журнал существует.
|
||||
|
||||
Пометка делит журнал на две выборки с разным назначением:
|
||||
|
||||
- **проскочил** — проверочный набор для калибровки конвейера. Реальный промах сильнее
|
||||
синтетической пробы: синтетические смещены в сторону тех, которые уже умеешь
|
||||
придумывать;
|
||||
- **пойман ревью** — прецеденты с оракулом. Самая сильная опора, какая у прохода
|
||||
бывает: проектная, воспроизводимая и однажды уже оказавшаяся правдой. Без
|
||||
журнала они остаются только в отчётах триажа в архиве change, где их никто не
|
||||
ищет.
|
||||
|
||||
Реализованные задачи и принятые решения сюда не пишутся: у них есть коммит, спека
|
||||
и `docs/adr/`.
|
||||
|
||||
Отдельно сюда попадают **решения о составе прогонов**: перестали звать проход,
|
||||
понизили метку правилом, сузили класс проверяемого. Не потому, что это промах,
|
||||
а потому, что здесь лежит цена: если что-то теперь проскочит, первый вопрос —
|
||||
«не тот ли это класс, который мы перестали проверять».
|
||||
|
||||
Каждое такое решение обязано получить строку в подразделе **«Перестали проверять
|
||||
сознательно»** раздела «Недоступно проверке» того же файла. Журнал хранит «почему
|
||||
тогда так решили», раздел настройки — то, во что смотрит каждый прогон. Решение,
|
||||
оставшееся только в журнале, в границы покрытия не доедет.
|
||||
|
||||
## Форма записи
|
||||
|
||||
**Это дом формы, и у него есть копия.** Скелет `docs/review.md`, который кладёт
|
||||
в проект `av-dev-docs` (`skills/canon/references/skeletons.md`), повторяет её
|
||||
дословно — он уезжает в репозиторий и обязан там что-то говорить. Правка формы
|
||||
здесь **обязана** тянуть правку скелета и запись в журнал версий канона; иначе
|
||||
проекты продолжат писать по старой форме, а конвейер — ждать поля, которого нет.
|
||||
Дословность сверяет `scripts/copies.py` маркетплейса по маркерам ниже — но
|
||||
запись в журнал версий он не проверит, это остаётся на человеке.
|
||||
|
||||
<!-- дом: журнал-дефектов-форма -->
|
||||
```
|
||||
## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
|
||||
|
||||
- **Где:** путь:строка либо «конвейер, а не код»
|
||||
- **Симптом:** как обнаружилось, кем и когда
|
||||
- **Причина:** что на самом деле было не так
|
||||
- **Чем воспроизведён:** тест, команда, замер — с числами
|
||||
- **Почему не поймали:** только для проскочивших — какой проход обязан был найти
|
||||
и что ему помешало
|
||||
- **Что меняем:** правило прохода, шаг гейта, конвенция, факт в документе
|
||||
проекта — либо «ничего, цена поимки выше цены дефекта»
|
||||
```
|
||||
<!-- /дом: журнал-дефектов-форма -->
|
||||
|
||||
Пункт «чем воспроизведён» отличает запись от байки: без него на неё нельзя
|
||||
сослаться как на оракул. Регрессионный тест, написанный вместе с починкой,
|
||||
годится наравне с независимым экспериментом — он исполняемый и падает на старом
|
||||
коде. Слабее он ровно в одном: сформулирован уже зная ответ, и это отмечается
|
||||
словом.
|
||||
|
||||
Последний пункт важнее остальных. Вывод «ничего не меняем» — законный исход: не
|
||||
всякий дефект стоит того, чтобы усложнять ради него ревью каждой задачи.
|
||||
|
||||
## Куда ведёт запись
|
||||
|
||||
Три адреса, и выбор между ними — половина ценности журнала:
|
||||
|
||||
- **в документ проекта** — если проход не мог знать факта. Адрес зависит от рода
|
||||
факта, и карта их всех — [project-facts.md](project-facts.md):
|
||||
настройка хранилища → `docs/database.md`;
|
||||
что необратимо и какой шаг гейта красит безусловно → `CLAUDE.md`; периметр и
|
||||
недоверенный вход → `docs/security.*`. **Вопрос по теме**, если промах лечится
|
||||
не фактом, а заданным вопросом, → раздел «Вопросы по темам» того же
|
||||
`docs/review.*`; адресуй теме, а не имени прохода — проход уедет между
|
||||
метками, тема останется. Самый частый адрес и самый дешёвый. Прежде чем
|
||||
править charter, проверь, не хватит ли факта или вопроса: charter общий для
|
||||
всех проектов, документ — про этот.
|
||||
- **в конвенции или в правило линтера** — если свойство выражается
|
||||
детерминированно (процедура — [promote.md](promote.md)).
|
||||
- **в charter прохода** — если сломан **метод**, а не знание. Правка charter'а
|
||||
меняет поведение во всех проектах, поэтому она требует калибровки
|
||||
([calibration.md](calibration.md)) и обоснования, почему это не лечится фактом
|
||||
в документе проекта.
|
||||
|
||||
## Что журнал даёт конвейеру
|
||||
|
||||
- **пробы для калибровки** — выборка по пометке `проскочил`;
|
||||
- **готовые оракулы** — выборка по пометке `пойман ревью`: находка того же
|
||||
класса подтверждается ссылкой на запись, а не рассуждением;
|
||||
- **основание для правил конвейера** — требование называть запущенные проходы
|
||||
поимённо, отказ от чисел, производных от размера корпуса, и правило очереди для
|
||||
меряющих проходов выведены из конкретных записей, а не из общих соображений;
|
||||
- **счётчик обратимости решений** — сузили состав проходов и через месяц поймали
|
||||
дефект ровно того класса, который перестали проверять: решение пересматривается
|
||||
фактом, а не спором.
|
||||
@@ -0,0 +1,165 @@
|
||||
# Метки задачи — выбор, цена, доли
|
||||
|
||||
**Дом правила выбора метки.** Состав проходов по каждой метке, схема процесса и
|
||||
раздача тем живут в [SKILL.md](../SKILL.md) — там диспетчер, и на готовой задаче
|
||||
его достаточно. Здесь то, что читают, когда метку **выбирают, оспаривают или
|
||||
калибруют**.
|
||||
|
||||
Применяет правило `review-scope` при разметке задачи — не автор изменения. Его
|
||||
рабочая выжимка лежит в уставе агента; расходиться она с этим файлом не вправе, а
|
||||
при расхождении прав этот.
|
||||
|
||||
## Правило выбора — две оси, а не один вопрос
|
||||
|
||||
**Оси две, они измеряют разное, и метка есть максимум по ним.**
|
||||
|
||||
| | **знакомое** — форму решения можно назвать до начала | **незнакомое** — форму предстоит нащупать по ходу |
|
||||
|---|---|---|
|
||||
| **малое** — один узел | `small` | `large` |
|
||||
| **среднее** — несколько узлов одного слоя | `medium` | `large` |
|
||||
| **крупное** — несколько слоёв, перенос ответственности, большой рефакторинг | `large` | `large` |
|
||||
|
||||
**Метка — не синоним размера.** Совпадают они только в левом верхнем углу: малое
|
||||
**незнакомое** изменение получает `large`, трогая один узел. Поэтому в плане
|
||||
стоят три строки, а не одна: размер, сложность и метка — каждая со своим
|
||||
обоснованием. Проход, выведший объём диффа из метки, ошибётся ровно на этом
|
||||
случае — а он и есть самый опасный: незнакомая форма в одном узле течёт там, где
|
||||
её никто не ждёт.
|
||||
|
||||
**Размер** — про объём: сколько мест трогается. **Сложность** — про
|
||||
неизвестность: знаем ли мы форму решения заранее. Признак незнакомого простой и
|
||||
проверяемый: **перед работой нельзя назвать, какие узлы будут тронуты**.
|
||||
|
||||
Раньше обе оси были склеены в один вопрос «крупное **или** незнакомое?». Ответ
|
||||
получался тот же, но две вещи под одним именем не измеришь по отдельности, и
|
||||
потому разметка не могла сказать «изменение среднее, но совершенно знакомое» —
|
||||
а именно эта пара и есть рабочее умолчание. Теперь обе оси называются в плане
|
||||
поимённо, и обе — с обоснованием.
|
||||
|
||||
**Оси называются и на стадии дизайна, и на стадии кода — но считаются один
|
||||
раз.** Это и есть причина, по которой разметка переехала к `propose`: состав
|
||||
ревью дизайна выводится из той же пары, что и состав ревью кода, а считать её
|
||||
дважды значит один раз посчитать без разведённости с автором.
|
||||
|
||||
**Обратимость — не третья ось, а отрицательный тест.** Она не уточняет размер и
|
||||
не уточняет сложность: она запрещает нижнюю метку независимо от обеих.
|
||||
|
||||
**Отрицательный тест `small`, и он важнее положительного:** изменение, которое
|
||||
после мерджа **не откатывается обратной правкой**, — не `small`, каким бы
|
||||
маленьким ни был дифф. Сюда попадают миграция схемы и данных, формат на диске,
|
||||
публичный контракт, имя, которое разойдётся по кодовой базе. Три строки миграции
|
||||
— это `medium`, а не `small`: размер диффа и цена ошибки здесь расходятся.
|
||||
|
||||
Что здесь считается крупным, что — незнакомым и что — мелким, проект уточняет в
|
||||
`docs/review.md`, подразделе «Триггеры метки»: **тремя списками** — по одному на
|
||||
каждую ось вверх и один вниз, поимённо, узлами или capability. Это **уточнение**,
|
||||
а не отмена: не записано — работает таблица выше.
|
||||
|
||||
## Спорный случай решается вниз, и у этого есть цена
|
||||
|
||||
Правило асимметрично, потому что асимметрична цена ошибки.
|
||||
|
||||
- **Спорно между `medium` и `large` → бери `medium`.** Ошибка в эту сторону
|
||||
стоит находки, которая всплывёт на следующей задаче или в журнале дефектов.
|
||||
Ошибка в обратную стоит трёх тяжёлых проходов, двое из которых держат машину и
|
||||
идут цепочкой, — и платится она **на каждой** задаче, выбранной неверно.
|
||||
- **Спорно между `small` и `medium` → бери `medium`.** Раньше эта строка
|
||||
обосновывалась тем, что состав одинаков и ошибка почти бесплатна. Теперь состав
|
||||
разный, и обоснование стало прямо противоположным: на `small` три темы ядра
|
||||
смотрятся **только против записанных инвариантов**, а спорный случай — ровно тот,
|
||||
где неизвестно, покрыт ли он инвариантом. Сомнение здесь стоит дороже, чем
|
||||
раньше, и потому решается вниз тем более твёрдо.
|
||||
|
||||
**Выбор сделан в пользу пропускной способности, и это записано, а не подразумевается.**
|
||||
Конвейер настроен на поток задач, а не на максимум находок с каждой: поправить в
|
||||
следующей задаче дешевле, чем держать одну два часа. Отсюда три обязанности,
|
||||
без которых сделка превращается в незаметную потерю качества:
|
||||
|
||||
- **границы покрытия называют темы и их глубину**, а не только запущенные
|
||||
проходы — иначе `small` выглядит так же, как `large` без находок;
|
||||
- **журнал дефектов в `docs/review.md` перестаёт быть хорошей практикой и
|
||||
становится единственной обратной связью**: проскочивший дефект — единственный
|
||||
сигнал, что метка выбрана слишком низко;
|
||||
- **возврат в код — повод пересмотреть метку.** Задача, которая приходит в тот
|
||||
же узел третий раз, уже не мелкая, чем бы ни выглядел её дифф.
|
||||
|
||||
## Метка — максимум по поверхности
|
||||
|
||||
**Обе оси меряются по всему диффу разом, и максимум по каждой отвечает за весь
|
||||
дифф.** Метка изменения — не средневзвешенное: одна строка в перечне границ
|
||||
задачи поднимает метку всему остальному, включая ту часть, которая сама по себе
|
||||
была бы `small`.
|
||||
|
||||
Обратное тоже верно и тоже не бесплатно: у каждой задачи есть **несокращаемый
|
||||
костяк — гейт, спеки, код, триаж**. Разрезать задачу, обе половины которой
|
||||
остаются в одной метке, значит заплатить костяк дважды за ту же проверку.
|
||||
Резать стоит там, где разрез **снимает доказательство с большей части диффа**.
|
||||
Шов и правило нарезки живут у того, кто ведёт задачи, — скилл `av-dev-tasks:tasks`,
|
||||
его `references/split.md`. Пути туда конвейер не выносит: за пределы своего
|
||||
плагина он ходит вызовом скилла, а не файлом.
|
||||
|
||||
Разметка в костяк не входит — она платится один раз на задачу, а не один раз на
|
||||
прогон, и потому **разрез задачи её не удваивает**. Это единственное, что стало
|
||||
дешевле от переезда разметки к `propose`, и это же снимает прежний довод против
|
||||
нарезки.
|
||||
|
||||
**Размер, сложность, метка и глубина объявляются в отчёте, и все четыре с
|
||||
обоснованием.** Метка выбирает `review-scope`; он вправе и поднять, и понизить
|
||||
её — но не молча: строка «метка X, потому что размер Y и сложность Z»
|
||||
обязательна на каждом прогоне, а не только когда метка отличается от ожидаемой.
|
||||
|
||||
## Чем `small` дешевле `medium` и что это стоит
|
||||
|
||||
Экономят три рычага — непуск, вход, потолок, — и они общие для всех проходов и
|
||||
всех меток; их дом и точные числа в [SKILL.md](../SKILL.md), раздел «Модель по
|
||||
проходу». Здесь только то, что рычаги делают **с этой меткой**:
|
||||
|
||||
1. **Составом.** `basics` на `small` не запускается — кроме случая, когда у
|
||||
проекта есть свои темы; тогда он идёт **только с ними**, ровно как в `large`.
|
||||
Три темы ядра, которые он держал бы, переходят к `code` сверкой по
|
||||
инвариантам.
|
||||
2. **Входом.** На `small` `specs` читает только дельта-спеку, а `code` — только
|
||||
**индекс** конвенций (перечень родов и что механизировано), не весь их дом. На
|
||||
`medium` оба читают дома целиком.
|
||||
3. **Потолком.** На `small` потолки самые жёсткие из трёх меток, и каждый
|
||||
напечатан в границах покрытия своего прохода.
|
||||
|
||||
**Что `small` за это не проверяет, названо поимённо и обязано идти строкой в
|
||||
границы покрытия:** темы `security`, `operations` и `architecture` смотрятся
|
||||
только против **записанных инвариантов** `CLAUDE.md`. Свойство, которого в
|
||||
инвариантах нет, с этой меткой не спросит никто — ни сценарием, ни чтением
|
||||
дома темы. Это и есть цена метки, и она заметно больше прежней: раньше `small`
|
||||
отличался от `medium` одним проходом на один вопрос, то есть не экономил
|
||||
ничего и назывался отдельной меткой зря.
|
||||
|
||||
**`large` назван по тому, что он добавляет: вход шире диффа.** Он единственный, где
|
||||
живут тяжёлые проходы, и единственный, где что-то **запускается**. `basics` в нём
|
||||
берёт только проектные темы; своих тем у проекта нет — он не запускается вовсе, и
|
||||
план говорит об этом строкой. **На `small` действует то же правило и по той же
|
||||
причине** — приёмник запускается только тогда, когда ему есть что принимать.
|
||||
Совпадение неслучайное: `basics` держит темы ядра ровно при одной метке из трёх,
|
||||
а приёмником проектных тем работает на всех.
|
||||
|
||||
## Доли — не пожелание, а проверка правила, и проверок две
|
||||
|
||||
**Сверху: `large` — 5–10%.** Если туда уходит каждая третья задача, метку
|
||||
выбирают по ощущению важности. Обратный перекос виден по журналу проскочивших
|
||||
дефектов: класс, который ловят только меряющие проходы, начинает всплывать после
|
||||
мерджа.
|
||||
|
||||
**Снизу: `small` не должен обгонять `medium`.** Ориентир — до трети задач, но
|
||||
сравнение важнее числа: **перевес `small` над `medium` значит, что рабочее
|
||||
умолчание сместилось, а решения об этом никто не принимал.** Проверка нужна
|
||||
именно теперь: пока две нижние метки совпадали составом, дрейф между ними не
|
||||
стоил ничего, и проверки не было. Сейчас он стоит трёх тем ядра, которые на
|
||||
`small` смотрятся только против инвариантов, — то есть ровно того, чем `small` и
|
||||
дёшев.
|
||||
|
||||
Считается это по журналу дефектов и по отчётам, а не по ощущению: метка
|
||||
напечатана в каждом отчёте, и посчитать её за спринт — работа на минуту.
|
||||
|
||||
**У дрейфа вниз есть свой стимул, и его стоит назвать.** `small` дешевле по
|
||||
времени и по деньгам, а выбирает метку хоть и не автор, но проход, читающий
|
||||
описание, написанное автором. Занижённое описание даёт занижённую метку без
|
||||
чьего-либо злого умысла — потому корректор и вынесен в `code`, который смотрит
|
||||
уже на код, а не на описание.
|
||||
Reference in New Issue
Block a user