классификация задачи: три категории документов и метка вместо ступени

Канон 5 объявил «каждый документ docs/ — тема ревью». Правило верно ровно
наполовину и потому вредно целиком. Паспорт и схему хранилища ревью читает, но
темами они не являются: по ним нельзя сказать «в этом изменении сделано не так»,
они задают границу, по которой судит чужая тема. Журнал решений и журнал
наблюдений ревью изменения не нужны вовсе — ADR объясняет прошлое, а не
предъявляет требование. Разметчик, применявший правило буквально, обязан был
либо завести фантомные темы passport, adr, database, research и продублировать
ими работу architecture и operations, либо потерять четыре документа молча;
случались обе ветки, и в собственном образце плана docs/passport.md не попадал
ни строкой, а обязательная арифметика покрытия при этом не сходилась.

Категорий теперь три, разрез проверяемый. Тема — да, прямо: conventions,
security, architecture и любой свой документ проекта. Источник темы — нет, но он
задаёт границу для чужой: passport, database, CLAUDE.md, openspec/specs.
Процессный — нет, он про то, как мы работаем: tasks, review, adr, research,
.pm.json. Открыта одна категория из трёх, две другие перечислены поимённо, так
что документ вне раскладки — однозначно своя тема. adr и research прогон больше
не открывает ни одним проходом; docs/review остаётся читаемым, но как настройка
конвейера, а не критерий. Цена записана и стала обязательной строкой границ
покрытия: расхождение с записанным решением ловит теперь только сверка
документации, а число под находкой обязано быть снято на этом прогоне, с
приложенной командой.

Классификация выдаёт задаче метку — small, medium, large. Прежние quick,
standard и wide назывались ступенью и описывали ревью: как глубоко смотрим.
Классифицируется же задача, и пока величина называлась свойством прогона, её
естественно было пересчитывать на каждом прогоне — что конвейер и делал. Слово
«ступень» удалено, а не оставлено синонимом: два имени одной вещи расходятся.
Выводится метка из двух разведённых осей — размер (малое, среднее, крупное) и
сложность (знакомое, незнакомое), — и равна максимуму по ним. Метка не синоним
размера: малое незнакомое изменение получает large, трогая один узел, поэтому
план печатает три строки с обоснованием каждая и выводить одну из другой
запрещено. Оси остались русскими словами — это суждение прозой; метка
английская — это идентификатор, который проходы сравнивают.

Разметка переехала из ревью кода в шаг 4 пайплайна, сразу после propose. Она
шла первым проходом каждого ревью кода, а перед ревью дизайна ту же величину
называл сам пайплайн — то есть оркестратор, который только что довёл
предложение до propose. Одно и то же измерялось дважды, и один из двух раз без
разведённости с автором, ровно в той точке, ради которой разметчик заведён.
Теперь запуск один на задачу, диффа он не видит, план обслуживает обе стадии, и
метка после кода не пересматривается: расхождение факта с разметкой ловит журнал
дефектов постфактум, как и всякую другую ошибку выбора. На диск план не пишется —
четвёртый артефакт рядом с proposal, tasks и design пережил бы задачу и разошёлся
бы с ней молча.

Ревью дизайна тоже растёт меткой: small — specs, medium — плюс rubric, large —
плюс architecture и вопрос автору о трёх формах решения. Раньше rubric и
architecture включались одним условием, и medium получал ровно один проход, то
есть не отличался от quick ничем. Разведены они потому, что зарабатывают на
разном: рубрика порождает свойства узла и окупается уже на среднем изменении,
её выход уезжает приёмочными критериями в tasks.md; архитектура отвечает на
вопрос про второй способ, а он на среднем знакомом изменении отвечается «нет»
ещё до запуска.

small подешевел тремя способами сразу. Составом: приёмник тем не запускается,
три темы ядра переходят к code сверкой по записанным инвариантам CLAUDE.md с
потолком в одну находку, и это не «глубина ниже», а другой дом темы. Входом:
specs читает только дельта-спеку, code — только индекс конвенций. Потолком: он
появился у каждого опиниативного прохода, а не у одного basics, и у половин code
он раздельный, потому что конвенционных находок больше по построению и в общем
списке они вытеснили бы техническую половину. Сработавший потолок обязан быть
объявлен строкой — молчащий срез неотличим от «больше не нашлось». Отрицательный
тест small от этого стал жёстче, а не мягче: вопросы про обратимость миграции
задавал приёмник тем, и на этой метке их не задаст никто.

Пайплайн задачи вырос до двенадцати шагов. Тривиальность перестала решать состав
ревью — она влияет только на explore; глубину обеих стадий называет метка.

Проверено прогоном ревьюверов по готовому результату: девять расхождений найдено
и починено — контракт находок печатал старый перечень проходов вместо плана по
темам, три ссылки в task-batch указывали на шаг коммита вместо закрытия, запись
changelog не переводила вопросы, адресованные passport и database, ops и
adversary утверждали, что на нижних метках их вопросы задаёт basics, шаблон
покрытия в review-code зашивал потолки small намертво, триггеры метки рассыпались
на два списка против трёх, тема из директивы CLAUDE.md могла остаться без запуска
исполнителя. Гейт зелёный: фронтматтеры, копии, одиннадцать диаграмм, ruff,
pyrefly; docs.py прогнан на живом фикстуре и печатает категорию в отказе.

Канон повышен до версии 6 с записью, выполнимой upgrade. Решения — 40–44.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-08-07 10:57:15 +03:00
co-authored by Claude Opus 5
parent 61cd9fcd37
commit d5bee11a6b
31 changed files with 1809 additions and 689 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ color: green
| Термин | Что называет |
| --- | --- |
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
| триаж | ступень конвейера, сводящая находки в решение |
| триаж | стадия конвейера, сводящая находки в решение |
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
| дедуп, дедупликация | сверка нового против уже лежащего |
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
+1 -1
View File
@@ -100,7 +100,7 @@ color: green
одной партии»), — находка: слово стоит, проверки нет. Число критериев считает
`tasks.py check`, тебе оно неинтересно.
6. **Предписания процесса в теле нет.** «Делать профилем standard», «взять
6. **Предписания процесса в теле нет.** «Делать с меткой medium», «взять
такой-то агент» — это выбор, который делают, увидев изменение, а не при
постановке. Он же путь понизить требования решением, принятым до
проектирования.
+1 -1
View File
@@ -199,7 +199,7 @@ capability), `openspec/config.yaml`.
**Шаг 6 обязателен, и вот почему.** `check` сверяет **число** в `.pm.json` с
версией скрипта — и только его. Применена ли запись журнала **по существу**, он
не знает: проект несёт `"canon": 4` и может не иметь того, чего требовала любая
не знает: проект несёт `"canon": 6` и может не иметь того, чего требовала любая
из пройденных версий. Записи применяются руками (переименовать секцию, проставить
типы, дописать раздел каждому `fix`), а ручной проход по нескольким записям
подряд — ровно то место, где половина шага делается и забывается. Судьи и есть
+107 -41
View File
@@ -1,6 +1,6 @@
# Канон документов проекта
**Версия 5.**
**Версия 6.**
Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs`
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
@@ -47,10 +47,10 @@
## Раскладка
**Документ канона — это тема ревью, а тема живёт файлом или каталогом.**
`docs/security.md` и `docs/security/` — одно и то же; форму выбирает проект по
объёму написанного, и переход между формами не меняет ни канон, ни версию. Обе
формы сразу — ошибка: два дома для одного факта расходятся молча.
**Документ канона живёт файлом или каталогом.** `docs/security.md` и
`docs/security/` — одно и то же; форму выбирает проект по объёму написанного, и
переход между формами не меняет ни канон, ни версию. Обе формы сразу — ошибка:
два дома для одного факта расходятся молча.
```
CLAUDE.md памятка агенту: что это, стек, инварианты с
@@ -75,21 +75,76 @@ openspec/
changes/archive/ архив изменений с design.md — сырьё для ADR
```
**У темы-каталога обязателен `README.md`** — вход, по которому её читают агенты.
**У документа-каталога обязателен `README.md`** — вход, по которому его читают агенты.
`adr/` в форме каталога держит ещё и `template.md`, а записи именуются
`ADR-ГГГГ-ММ-ДД-slug.md`.
**Список тем открытый, и это не послабление, а механизм.** Всё, что проект
кладёт в `docs/`, становится темой ревью: у конвейера есть приёмник для темы, к
которой нет именной оптики, и заведён он ровно за этим. Завёл
`docs/accessibility.md` — появилась тема `accessibility`, и она попадает в план
каждого прогона. Не темы ровно две: `docs/tasks/` (его ведёт скилл `tasks`)
и `docs/review.*` — это настройка самого конвейера, слой над темами.
## Три категории документов
Раньше здесь стояло плоское правило «каждый документ `docs/` — тема ревью». Оно
неверно ровно наполовину: паспорт и схема хранилища ревью нужны, но темами не
являются, а журнал решений и журнал наблюдений ревью изменения не нужны вовсе.
Плоское правило заставляло разметчика либо плодить фантомные темы, либо терять
документы молча — а молчащая потеря и есть то, против чего канон написан.
**Разрез один и проверяемый: можно ли по документу сказать «в этом изменении
сделано не так»?**
| Категория | Ответ на разрез | Что с ней делает ревью |
| --- | --- | --- |
| **тема** | да, прямо | заводит направление проверки и требует исполнителя |
| **источник темы** | нет, но он задаёт границу, по которой судит чужая тема | читается как материал, своей темы не порождает |
| **процессный документ** | нет: он про то, как мы работаем, а не про изменение | не судит по нему изменение |
| Документ | Категория | Куда питает |
| --- | --- | --- |
| `conventions.*` | тема | `conventions` |
| `security.*` | тема | `security` |
| `architecture.*` | тема | `architecture`; раздел эксплуатации — `operations` |
| *свой документ проекта* | тема | своя тема, именем документа |
| `passport.*` | источник | `architecture` — граница домена, «чем **не** является» |
| `database.*` | источник | `operations` — схема и настройки с числами |
| `CLAUDE.md`, `AGENTS.md` | источник | `autotests` (семантика гейта); инварианты — сквозные |
| `openspec/specs/` | источник | `requirements` |
| `tasks/` | процессный | — |
| `review.*` | процессный | — (настройка самого конвейера, слой **над** темами) |
| `adr.*` | процессный | — |
| `research.*` | процессный | — |
| `.pm.json` | процессный | — (служебный файл, не документ) |
**Список тем открытый, и это не послабление, а механизм.** Категории
`источник` и `процессный` **закрыты** — они перечислены здесь поимённо и
проектом не пополняются. Всё остальное, что проект кладёт в `docs/`, — тема: у
конвейера есть приёмник для темы, к которой нет именной оптики, и заведён он
ровно за этим. Завёл `docs/accessibility.md` — появилась тема `accessibility`, и
она попадает в план каждого прогона.
Отсюда следствие, ради которого правило и заведено: **`docs/` — это конфигурация
ревью.** Проект настраивает проверку тем, что пишет о себе, а не отдельным файлом
настроек, который разошёлся бы с документами.
**«Не судит по нему» и «не открывает» — не одно и то же, и разница существенна.**
`docs/review.*` проходы читают на каждом прогоне: там лежат вопросы по темам,
журнал дефектов, типовые узлы и типовые ложноположительные. Это чтение конвейером
**собственной настройки**, а не суждение об изменении, и потому оно законно.
`adr/`, `research/` и `tasks/` не открывает никто: по ним изменение не судят, и
настройкой конвейера они не являются.
**Процессный документ — не документ второго сорта.** `adr/` и `research/`
проверяются наравне с остальными, но **сверкой документации**, а не прогоном
ревью: ADR без ссылки на архивный `design.md`, замена без парного статуса, число
без провенанса — это работа агентов `doc-consistency` и `doc-code-drift`, и она
осталась там же, где была. Изменилось одно: прогон ревью не открывает их как
критерий и не судит по ним изменение.
Цена этого решения записана, а не подразумевается: **расхождение изменения с
записанным решением прогоном больше не ловится.** Раньше архитектурный проход
читал `adr/` и мог сказать «здесь отменено решение ADR-2026-03-11, а парного
статуса нет»; теперь это скажет только `doc-consistency` на сессии между
спринтами. Сделка сознательная — ADR объясняет прошлое, а не предъявляет
требование к изменению, и чтение всего каталога решений на каждой задаче
оплачивалось на каждой, а срабатывало на единицах.
### Имена файлов английские, текст русский
**Текст документов русский; имена файлов, capability и задач — английские,
@@ -116,30 +171,35 @@ kebab-case.** Причина не эстетическая: имя файла с
## Роли документов и темы ревью
Одна строка на каждый — на какой вопрос он отвечает и какую тему ревью питает.
**Кто именно закрывает тему, здесь не указано намеренно**: это зависит от ступени
прогона и меняется вместе с конвейером, а документ живёт дольше. Раскладку
«тема → проход → глубина» держит скилл `av-dev-pipeline:review-pipeline`.
Одна строка на каждый — на какой вопрос он отвечает, в какой он категории и
какую тему питает. **Кто именно закрывает тему, здесь не указано намеренно**: это
зависит от метки прогона и меняется вместе с конвейером, а документ живёт
дольше. Раскладку «тема → проход → глубина» держит скилл
`av-dev-pipeline:review-pipeline`.
**Общего словаря у канона с конвейером ровно два вида имён: имена тем и имена
ступеней.** Ими проект и настраивает ревью — вопросами по темам и триггерами
профиля. **Имён проходов канон не называет нигде**, включая вывод `docs.py`:
проход переименовывается и переезжает между ступенями, и канон, назвавший его, в
**Общего словаря у канона с конвейером ровно три вида имён: имена категорий,
имена тем и имена меток.** Категорий три — `тема`, `источник`, `процессный`;
**меток тоже три, и они закрыты: `small`, `medium`, `large`.** Метка это итог
классификации задачи, и по ней конвейер выбирает исполнителей на обеих стадиях
ревью; проект её не выдумывает, а только уточняет триггеры. Ими проект и
настраивает ревью — вопросами по темам и триггерами метки. **Имён проходов канон не называет нигде**, включая вывод `docs.py`:
проход переименовывается и переезжает между метками, и канон, назвавший его, в
этот день соврёт молча. Обратное направление законно — конвейер называет
документы канона поимённо, потому что он их читатель.
| Документ | Вопрос | Тема ревью |
| Документ | Вопрос | Категория и тема |
| --- | --- | --- |
| `CLAUDE.md`, `AGENTS.md` | что нельзя нарушать, чем краснеет гейт | `autotests`; инварианты — сквозные, во все темы |
| `passport.*` | зачем и для кого, чем это **не** является | `architecture` |
| `architecture.*` | как сложено и где что работает | `architecture`; раздел эксплуатации — `operations` |
| `database.*` | что лежит в хранилище и какими настройками | `operations` |
| `security.*` | против кого защищаемся и что вне модели | `security` |
| `conventions.*` | как мы пишем код | `conventions` |
| `research/` | что показала реальность, а не документация | `operations`, `requirements` |
| `adr.*` | почему решено именно так | `architecture` |
| `openspec/specs/` | что система делает — нормативно | `requirements` |
| `review.*` | как настроен конвейер и что уже проскакивало | **не тема**: слой над всеми |
| `CLAUDE.md`, `AGENTS.md` | что нельзя нарушать, чем краснеет гейт | источник: `autotests`; инварианты — сквозные, во все темы |
| `passport.*` | зачем и для кого, чем это **не** является | источник: `architecture` |
| `architecture.*` | как сложено и где что работает | тема `architecture`; раздел эксплуатации — `operations` |
| `database.*` | что лежит в хранилище и какими настройками | источник: `operations` |
| `security.*` | против кого защищаемся и что вне модели | тема `security` |
| `conventions.*` | как мы пишем код | тема `conventions` |
| `openspec/specs/` | что система делает — нормативно | источник: `requirements` |
| `research.*` | что показала реальность, а не документация | процессный |
| `adr.*` | почему решено именно так | процессный |
| `review.*` | как настроен конвейер и что уже проскакивало | процессный: слой **над** темами |
| `tasks/` | что делаем и в каком порядке | процессный |
| *свой документ проекта* | что проект счёл нужным проверять | **своя тема**, именем документа |
### `passport.md`
@@ -256,16 +316,22 @@ kebab-case.** Причина не эстетическая: имя файла с
- **Типовые ложноположительные** — находки, которые здесь выглядят убедительно и
всегда неверны, каждая со строкой «почему здесь это не дефект»;
- **Вопросы по темам** — в форме `<тема>: <вопрос> (<провенанс>)`. **Не по именам
проходов**: проход уезжает между ступенями, а тема остаётся, и вопрос,
проходов**: проход уезжает между метками, а тема остаётся, и вопрос,
адресованный проходу, перестал бы задаваться молча в тот день, когда тот уехал
в верхнюю ступень. Задаёт вопрос тот, кто закрывает тему на этом прогоне;
- **Триггеры профиля** — проектная конкретизация правила выбора профиля ревью:
что в этом проекте считается **крупным или незнакомым** изменением (поднимает
прогон до `wide`, верхней ступени, — и она рассчитана на 5–10% задач) и что
считается **мелким** (опускает до `quick`). Перечнем мест, узлами или
capability, а не вторым определением класса. Уточняет умолчания, а не отменяет
их. Рабочее умолчание — `standard`: миграция схемы и публичный контракт ступень
**не** поднимают, их проверяют проходы, которые в `standard` и так есть;
в старшую метку. Задаёт вопрос тот, кто закрывает тему на этом прогоне.
Адресовать можно только теме: `passport`, `database`, `adr`, `research` и
`review` — не темы, и вопрос, адресованный им, не задаст никто;
- **Триггеры метки** — проектная конкретизация правила выбора метки ревью,
**тремя списками**. Два поднимают, по одному на ось: что в этом проекте считается
**крупным** (объём: сколько узлов и слоёв трогает) и что считается
**незнакомым** (форма решения: известна до начала или нащупывается по ходу).
Любая из двух осей поднимает прогон до `large`, старшей метки, — а она
рассчитана на 5–10% задач. Третий список — что считается **мелким** (опускает
до `small`); он один, потому что вниз метку опускает только совпадение обеих
осей сразу. Перечнем мест, узлами или capability, а не вторым определением
класса. Уточняет умолчания, а не отменяет их. Рабочее умолчание — `medium`:
миграция схемы и публичный контракт метку **не** поднимают, их проверяют
проходы, которые в `medium` и так есть;
- **Недоступно проверке** — два подраздела, оба **по темам**: «не проверит ни
один проход» (принципиальная граница, по факту промаха не пересматривается) и
«перестали проверять сознательно» (пересматривается первым). Тема, у которой в
@@ -441,7 +507,7 @@ kebab-case.** Причина не эстетическая: имя файла с
```json
{
"canon": 4,
"canon": 6,
"migrations": "internal/store/migrations",
"tasks": {
"backlog": "INDEX.md"
@@ -13,6 +13,92 @@ upgrade` идёт по записям снизу вверх от версии п
---
## Версия 6 — 2026-08-07
Версия 5 объявила: **каждый документ `docs/` — тема ревью**. Правило оказалось
верным ровно наполовину и потому вредным целиком. Паспорт и схему хранилища
ревью читает, но темами они не являются — они задают границу, по которой судит
чужая тема. Журнал решений и журнал наблюдений ревью изменения не нужны вовсе:
ADR объясняет прошлое решение, а не предъявляет требование к изменению.
Разметчик, применявший плоское правило буквально, обязан был либо завести
фантомные темы `passport`, `adr`, `database`, `research` и продублировать ими
работу тем `architecture` и `operations`, либо потерять четыре документа молча —
а молчащая потеря и есть то, против чего канон написан.
**Что изменилось:**
1. **Три категории документов вместо одной.** Разрез проверяемый: можно ли по
документу сказать «в этом изменении сделано не так»? **Тема** — да, прямо
(`conventions`, `security`, `architecture`, свои документы проекта).
**Источник темы** — нет, но он задаёт границу для чужой темы (`passport.*`
`architecture`, `database.*``operations`, `CLAUDE.md``autotests`,
`openspec/specs/``requirements`). **Процессный документ** — нет, он про то,
как мы работаем (`tasks/`, `review.*`, `adr.*`, `research.*`, `.pm.json`).
2. **Категории `источник` и `процессный` закрыты, категория `тема` открыта.**
Прежде открытым был весь список, и «не темы ровно две» противоречило
собственной раскладке канона. Теперь пополняется только одно множество, и
документ, которого нет в раскладке, — однозначно своя тема проекта.
3. **`adr/` и `research/` уходят из входа ревью изменения.** Прогон их больше не
открывает. Проверяться они не перестали: ADR без ссылки на архивный
`design.md`, замена без парного статуса, число без провенанса — это по-прежнему
работа `doc-consistency` и `doc-code-drift`, на сессии между спринтами.
4. **`docs.py` печатает категорию в отказе.** «Нет источника passport» читается
иначе, чем «нет темы security». Обязательность при этом не изменилась:
заводятся все документы одинаково и с первого дня.
5. **У задачи появилась метка — `small`, `medium` или `large`.** Это итог
классификации и **единственный вход, по которому конвейер выбирает
исполнителей** на обеих стадиях ревью. Прежние имена `quick`, `standard` и
`wide` описывали глубину прогона, то есть свойство ревью; метка описывает
**задачу** — а выбирают по ней одно и то же. Слово «ступень» уходит:
у одной вещи одно имя.
6. **Метка выводится из двух осей и не равна ни одной из них.** Размер (малое,
среднее, крупное) и сложность (знакомое, незнакомое); метка — максимум по
ним. Малое **незнакомое** изменение получает `large`, трогая один узел, —
поэтому размер и метка пишутся отдельными строками, и выводить одно из
другого нельзя.
**Цена, записанная явно:** расхождение изменения с записанным решением прогоном
больше не ловится. Раньше архитектурный проход мог сказать «здесь отменено
решение ADR-2026-03-11, парного статуса нет»; теперь это скажет только сверка
документации. Сделка сознательная: чтение всего каталога решений оплачивалось на
каждой задаче, а срабатывало на единицах.
**Что переехало:** ничего в раскладке. Ни один файл не переименовывается и не
перемещается.
**Что сделать проекту:**
1. `docs/review.*`, подраздел «Вопросы по темам»: убрать вопросы, адресованные
`passport`, `database`, `adr`, `research` и `review` — **ни одно из этих имён
больше не тема**. Под каноном 5 темой был каждый документ `docs/`, поэтому
такие вопросы там законны и почти наверняка есть. Переадресовать:
про границу домена и про решение → `architecture`; про хранилище, настройку и
измеренное число → `operations`. Вопрос, который никуда не переадресовывается,
удалить, а не оставить висеть: адресованный несуществующей теме, он не
задаётся никем и молча.
2. Там же, «Недоступно проверке»: те же пять имён убрать из разнесения по темам,
переразнеся содержимое по оставшимся.
3. Там же: подраздел **«Триггеры профиля» → «Триггеры метки»**, и разнести его
на **три** списка вместо двух — «крупное здесь» (про объём), «незнакомое
здесь» (про форму решения) и «мелкое здесь» (опускает до `small`). Раньше
первые две оси были склеены в один список, и потому объём в правило по факту
не входил.
4. **Переименовать метки прогона везде, где проект их называет** — в «Триггерах
метки», в «Недоступно проверке», в журнале дефектов: `quick`**`small`**,
`standard`**`medium`**, `wide`**`large`**. Метка это итог классификации
задачи, и три её значения — часть общего словаря канона и конвейера. Слово
«ступень» из документов уходит: у одной вещи одно имя.
5. Проверить, что свои темы проекта не совпадают именем с закрытыми категориями:
`docs/passport/`, `docs/adr/`, `docs/research/`, `docs/database/`,
`docs/review/` — это слоты канона, а не свои темы, и своим смыслом их
наполнять нельзя.
6. Ничего не заводить и не удалять: раскладка канона 6 совпадает с раскладкой
канона 5 файл в файл.
7. `docs/.pm.json`: `"canon": 6`.
---
## Версия 5 — 2026-08-06
Канон перестал быть списком файлов и стал **списком тем ревью**. Раскладка та же,
@@ -134,7 +134,7 @@
| Термин | Что называет |
| --- | --- |
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
| триаж | ступень конвейера, сводящая находки в решение |
| триаж | стадия конвейера, сводящая находки в решение |
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
| дедуп, дедупликация | сверка нового против уже лежащего |
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
+25 -15
View File
@@ -282,30 +282,40 @@
задаёт тот проход, который закрывает эту тему на текущем прогоне, дополнительно
к обязательным.
**Адресуй теме, а не имени прохода.** Проходы переезжают между ступенями и
**Адресуй теме, а не имени прохода.** Проходы переезжают между метками и
упраздняются; вопрос, адресованный проходу, перестанет задаваться в тот день,
когда тот уедет в верхнюю ступень, — и заметить это будет нечем. Тема переезд
когда тот уедет в старшую метку, — и заметить это будет нечем. Тема переезд
переживает.
Темы ядра: `requirements`, `autotests`, `conventions`, `architecture`,
`security`, `operations`. Плюс любая своя — та, под которую в `docs/` лежит
документ.
`security`, `operations`. Плюс любая своя — та, под которую проект завёл в
`docs/` **свой** документ. Документы категорий `источник` и `процессный` тем не
порождают, и адресовать вопрос `passport`, `database`, `adr`, `research` или
`review` нельзя — таких тем нет. Вопрос про границу домена адресуй
`architecture`, вопрос про хранилище и числа — `operations`.
### Триггеры профиля
### Триггеры метки
Проектная конкретизация правила выбора профиля, двумя списками и поимённо —
узлами или capability.
Проектная конкретизация правила выбора метки. **Списка три: по одному на
каждую ось вверх и один вниз** — поимённо, узлами или capability.
**Крупное или незнакомое здесь** — поднимает прогон до `wide`, верхней ступени:
там `security`, `operations` и `architecture` проверяют запуском, и там же
единственные замеры. Ступень рассчитана на **510% задач**; если сюда попадает
каждая третья, список написан слишком широко.
**Крупное здесь** — про объём: что трогает несколько узлов или слоёв, переносит
ответственность между ними, перекладывает существующий код в новую форму.
**Мелкое здесь**опускает до `quick`. Помни отрицательный тест конвейера: что
**Незнакомое здесь**про форму решения: то, чего в проекте ещё не было и чью
форму предстоит нащупать по ходу. Признак простой: перед работой нельзя назвать,
какие узлы будут тронуты.
Любая из двух осей поднимает прогон до `large`, старшей метки: там `security`,
`operations` и `architecture` проверяют запуском, и там же единственные замеры.
Метка рассчитана на **510% задач**; если сюда попадает каждая третья, списки
написаны слишком широко.
**Мелкое здесь** — опускает до `small`. Помни отрицательный тест конвейера: что
после мерджа не откатывается обратной правкой (миграция, формат на диске,
публичный контракт, имя), — не `quick`, каким бы маленьким ни был дифф.
публичный контракт, имя), — не `small`, каким бы маленьким ни был дифф.
Уточняет умолчания конвейера, не отменяет их; рабочее умолчание — `standard`.
Уточняет умолчания конвейера, не отменяет их; рабочее умолчание — `medium`.
### Недоступно проверке
@@ -409,7 +419,7 @@ severity стоит здесь, а не выводится каждым прох
```json
{
"canon": 4
"canon": 6
}
```
+76 -56
View File
@@ -25,55 +25,69 @@ from dataclasses import dataclass, field
from pathlib import Path
from typing import NoReturn
CANON_VERSION = 5
CANON_VERSION = 6
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
# --- Раскладка канона -------------------------------------------------------
# Тема канона: имя → на какой вопрос отвечает (для внятного отказа).
# Документ канона: имя → (категория, на какой вопрос отвечает).
#
# **Тема живёт файлом `docs/<имя>.md` либо каталогом `docs/<имя>/` с README.md
# внутри.** Форму выбирает проект: тема разрослась — стала каталогом, и это не
# смена канона и не повод править скрипт. Обе формы сразу — ошибка: это два дома
# для одного факта, ровно то, от чего канон и защищает.
THEMES = {
"passport": "зачем и для кого, чем НЕ является",
"architecture": "как сложено — обзор, окружение, эксплуатация",
"security": "периметр, недоверенный вход, что вне модели",
"conventions": "как мы пишем код; индекс, промоут, что механизировано",
"research": "что показала реальность: наблюдения и числа с провенансом",
"adr": "почему решено так; индекс, статусы, правило замены",
"review": "настройка конвейера + журнал дефектов",
# Категории — из canon.md, раздел «Три категории документов». Разрез один: можно
# ли по документу сказать «в этом изменении сделано не так»?
# тема — да, прямо: документ заводит направление проверки изменения;
# источник — нет, но он задаёт границу, по которой судит чужая тема;
# процессный — нет: он про то, как мы работаем, а не про изменение.
#
# **Категория не меняет обязательности документа** — заводятся все три
# одинаково и с первого дня. Она меняет только то, что с документом делает
# конвейер ревью, и потому печатается в отказе: «нет источника passport»
# читается иначе, чем «нет темы security», и чинится теми же руками, но с
# другим приоритетом.
#
# **Документ живёт файлом `docs/<имя>.md` либо каталогом `docs/<имя>/` с
# README.md внутри.** Форму выбирает проект: документ разросся — стал каталогом,
# и это не смена канона и не повод править скрипт. Обе формы сразу — ошибка: это
# два дома для одного факта, ровно то, от чего канон и защищает.
DOCS = {
"passport": ("источник", "зачем и для кого, чем НЕ является"),
"architecture": ("тема", "как сложено — обзор, окружение, эксплуатация"),
"security": ("тема", "периметр, недоверенный вход, что вне модели"),
"conventions": ("тема", "как мы пишем код; индекс, промоут, что механизировано"),
"research": ("процессный", "что показала реальность: наблюдения и числа"),
"adr": ("процессный", "почему решено так; индекс, статусы, правило замены"),
"review": ("процессный", "настройка конвейера + журнал дефектов"),
}
# Тема, обязательная только при условии: имя → (ключ .pm.json, пояснение).
CONDITIONAL_THEMES = {
"database": ("migrations", "схема хранилища и настройки"),
# Документ, обязательный только при условии: имя → (ключ .pm.json, категория,
# пояснение).
CONDITIONAL_DOCS = {
"database": ("migrations", "источник", "схема хранилища и настройки"),
}
# Обязательные файлы вне тем.
# Обязательные файлы вне раскладки docs/.
REQUIRED = {
"CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта",
"docs/.pm.json": "версия канона и пути, нужные проверкам",
}
# Файлы, которые тема-каталог обязана держать сверх README.md.
THEME_EXTRA = {
# Файлы, которые документ-каталог обязан держать сверх README.md.
DOC_EXTRA = {
"adr": {"template.md": "шаблон записи ADR"},
}
# Служебное в docs/ и каталог, который ведёт tasks.py.
NOT_THEMES = {".pm.json", "tasks"}
# Служебное в docs/ и каталог, который ведёт tasks.py. Оба процессные, но
# проверок формы у них нет: .pm.json не markdown, tasks/ ведёт другой скрипт.
NOT_DOCS = {".pm.json", "tasks"}
# Слоты, которых в каноне нет, — с адресом, куда уезжает содержимое. Имена,
# совпадающие с темой, отсюда убраны намеренно: `docs/conventions.md` и
# `docs/review/` теперь законные формы своих тем.
RETIRED = {
"review-brief.md": "документы канона и есть бриф; остаток — в review",
"review-journal.md": "→ тема review",
"review-journal.md": "документ review",
"plan.md": "→ docs/tasks/ROADMAP.md",
"local-research.md": "→ тема research",
"local-research.md": "документ research",
"specs": "поведение → openspec/specs/, обзор → тема architecture",
"drafts": "идея → запись research, отказ → ADR, порядок → ROADMAP.md",
"backlog": "→ docs/tasks/",
@@ -122,11 +136,11 @@ def check_slugs(root: Path, rep: Report) -> None:
if not docs.is_dir():
return
# Имена, выбранные каноном, а не проектом: их форма задана здесь же.
fixed = {"README.md", "template.md"} | {f"{name}.md" for name in THEMES}
# Все темы-каталоги, включая свои темы проекта: правило имён общее, а
fixed = {"README.md", "template.md"} | {f"{name}.md" for name in DOCS}
# Все документы-каталоги, включая свои темы проекта: правило имён общее, а
# перечислять их поимённо значило бы закрыть открытый список.
for folder in sorted(docs.iterdir()):
if not folder.is_dir() or folder.name in NOT_THEMES:
if not folder.is_dir() or folder.name in NOT_DOCS:
continue
sub = folder.name
for path in sorted(folder.rglob("*.md")):
@@ -267,8 +281,8 @@ def check_version(root: Path, cfg: dict, rep: Report) -> None:
)
def theme_home(root: Path, name: str) -> tuple[Path | None, str | None]:
"""Дом темы: файл `docs/<имя>.md` или каталог `docs/<имя>/`.
def doc_home(root: Path, name: str) -> tuple[Path | None, str | None]:
"""Дом документа: файл `docs/<имя>.md` или каталог `docs/<имя>/`.
Возвращает путь и жалобу. Обе формы сразу — это два дома для одного факта, и
расходятся они молча: правят одну, читают другую.
@@ -278,7 +292,7 @@ def theme_home(root: Path, name: str) -> tuple[Path | None, str | None]:
as_dir = docs / name
if as_file.is_file() and as_dir.is_dir():
return as_file, (
f"тема {name} живёт сразу двумя домами — docs/{name}.md и docs/{name}/:"
f"{name} живёт сразу двумя домами — docs/{name}.md и docs/{name}/:"
f" оставить один, иначе правят один, а читают другой"
)
if as_file.is_file():
@@ -286,8 +300,8 @@ def theme_home(root: Path, name: str) -> tuple[Path | None, str | None]:
if as_dir.is_dir():
if not (as_dir / "README.md").is_file():
return as_dir, (
f"docs/{name}/ без README.md — у темы-каталога вход обязателен:"
f" по нему её читают агенты"
f"docs/{name}/ без README.md — у документа-каталога вход"
f" обязателен: по нему его читают агенты"
)
return as_dir, None
return None, None
@@ -298,54 +312,60 @@ def check_required(root: Path, cfg: dict, rep: Report) -> None:
if not (root / rel).exists():
rep.error(f"нет {rel}{what}")
for name, what in THEMES.items():
home, complaint = theme_home(root, name)
for name, (kind, what) in DOCS.items():
home, complaint = doc_home(root, name)
if home is None:
rep.error(f"нет темы {name} (docs/{name}.md или docs/{name}/) — {what}")
rep.error(
f"нет документа {name} (docs/{name}.md или docs/{name}/),"
f" категория «{kind}» — {what}"
)
continue
if complaint:
rep.error(complaint)
if home.is_dir():
for extra, why in THEME_EXTRA.get(name, {}).items():
for extra, why in DOC_EXTRA.get(name, {}).items():
if not (home / extra).is_file():
rep.error(f"нет docs/{name}/{extra}{why}")
for name, (key, what) in CONDITIONAL_THEMES.items():
home, complaint = theme_home(root, name)
for name, (key, kind, what) in CONDITIONAL_DOCS.items():
home, complaint = doc_home(root, name)
if complaint:
rep.error(complaint)
if key in cfg and home is None:
rep.error(
f"нет темы {name} (docs/{name}.md или docs/{name}/){what}"
f" (обязательна: в .pm.json объявлен {key})"
f"нет документа {name} (docs/{name}.md или docs/{name}/),"
f" категория «{kind}» — {what}"
f" (обязателен: в .pm.json объявлен {key})"
)
elif key not in cfg and home is None:
rep.skip(f"тема {name} — в .pm.json нет ключа {key}, проверка неприменима")
rep.skip(f"{name} — в .pm.json нет ключа {key}, проверка неприменима")
def check_stray(root: Path, rep: Report) -> None:
"""Лишнего в docs/ больше нет — есть темы проекта.
"""Лишнего в docs/ больше нет — есть свои темы проекта.
Список тем **открытый**: каждый документ в docs/ и есть заявка на тему
ревью, и запретить проекту завести свою нельзя. Проверяются только слоты,
у которых дом в другом месте, — иначе переехавшее содержимое вернулось бы
темой и выглядело законным.
Категории `источник` и `процессный` **закрыты**: они перечислены в каноне
поимённо и проектом не пополняются. Открыта только категория `тема` —
поэтому любой документ в docs/, которого нет в раскладке, и есть заявка на
свою тему, и запретить её нельзя. Проверяются только слоты, у которых дом в
другом месте, — иначе переехавшее содержимое вернулось бы темой и выглядело
законным.
"""
docs = root / "docs"
if not docs.is_dir():
rep.error("нет каталога docs/")
return
known = set(THEMES) | set(CONDITIONAL_THEMES)
known = set(DOCS) | set(CONDITIONAL_DOCS)
own: list[str] = []
for entry in sorted(docs.iterdir()):
name = entry.name
if name in RETIRED:
rep.error(f"docs/{name} — слота нет в каноне: {RETIRED[name]}")
continue
if name in NOT_THEMES:
if name in NOT_DOCS:
continue
theme = name[:-3] if entry.is_file() and name.endswith(".md") else name
if theme in known:
topic = name[:-3] if entry.is_file() and name.endswith(".md") else name
if topic in known:
continue
if entry.is_file() and not name.endswith(".md"):
rep.error(f"docs/{name} — не markdown: тема ревью читается как текст")
@@ -356,7 +376,7 @@ def check_stray(root: Path, rep: Report) -> None:
f" по нему её читают агенты"
)
continue
own.append(theme)
own.append(topic)
if own:
rep.note(
f"свои темы проекта: {', '.join(own)} — именной оптики у них нет,"
@@ -418,13 +438,13 @@ def check_placeholders_and_debt(root: Path, rep: Report) -> None:
rep.debt(f"{rel}: {what}")
def theme_text(root: Path, name: str) -> str | None:
"""Текст темы целиком: файл или все markdown каталога, склеенные.
def doc_text(root: Path, name: str) -> str | None:
"""Текст документа целиком: файл или все markdown каталога, склеенные.
Проверке всё равно, одним файлом написана тема или десятью: она ищет
Проверке всё равно, одним файлом написан документ или десятью: она ищет
упоминание, а упоминание живёт в любом из них.
"""
home, _ = theme_home(root, name)
home, _ = doc_home(root, name)
if home is None:
return None
if home.is_file():
@@ -437,7 +457,7 @@ def theme_text(root: Path, name: str) -> str | None:
def check_capabilities(root: Path, rep: Report) -> None:
specs = root / "openspec" / "specs"
text = theme_text(root, "architecture")
text = doc_text(root, "architecture")
if not specs.is_dir():
rep.skip("openspec/specs/ нет — сверка capability с архитектурой неприменима")
return
+1 -1
View File
@@ -130,7 +130,7 @@ description: Вести содержимое документов канона
Твоя часть на синке: **дефект пишется сразу**, а не «потом, когда починим».
Со временем теряется не факт, а то, почему дефект не поймали, — единственное,
ради чего журнал есть. И решение сузить проверки (перестали звать проход, понизили
профиль) обязано попасть в раздел настройки, а не остаться в отчёте ревью.
метка) обязано попасть в раздел настройки, а не остаться в отчёте ревью.
## Промоут в конвенции
@@ -250,7 +250,7 @@
Здесь же последний дешёвый момент заметить **разнородную задачу**: раздел
«Затрагивает» показывает границы до того, как заведено предложение об
изменении. Строка, которая одна тянет задачу на ступень выше остального
изменении. Строка, которая одна тянет задачу на метку выше остального
перечня, — кандидат на разрез (шов — в `tasks`, `references/split.md`).
Замеченная здесь, она стоит одного `edit`; замеченная на ревью — выброшенного
предложения.
+4 -4
View File
@@ -279,7 +279,7 @@ stateDiagram-v2
**напоминает** — беклог, заведённый до появления типа, законен, и переоформлять
его «заодно» здесь не просят.
**Тип не выбирает профиль ревью и вообще ничего не предписывает пайплайну.**
**Тип не выбирает метку ревью и вообще ничего не предписывает пайплайну.**
Профиль выбирается по факту изменения, а не по типу задачи: `chore` бывает
миграцией схемы, `fix` — правкой публичного контракта. Правило «предписание
процесса в теле задачи снимается» типом не отменяется, а подтверждается: он
@@ -523,8 +523,8 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
**каждая давать видимую пользу**, а у штурма исход «выкинуть» — полноправный.
Там же **шов**: где резать, когда допустимых мест несколько. Коротко — по
границе, которая одна поднимает ступень ревью выше остальных; и не резать, когда
обе половины остаются в одной ступени, потому что несокращаемый костяк проверок
границе, которая одна поднимает метку ревью выше остальных; и не резать, когда
обе половины остаются в одной метке, потому что несокращаемый костяк проверок
платится за каждую задачу отдельно.
### Вычитка: два прохода, а не один
@@ -589,7 +589,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
- **свойство репозитория в рамках** — номер миграции, хеш, версия зависимости:
в лежалой задаче протухает молча и становится ложной рамкой. Снимается;
снимок берётся при постановке, а не при заведении;
- **предписание процесса в теле** — «делать таким-то профилем ревью», «взять
- **предписание процесса в теле** — «делать с такой-то меткой ревью», «взять
такой-то агент»: это второй дом для правила выбора и путь понизить требования
решением, принятым до проектирования. Снимается;
- **тип, разошедшийся с задачей** — задача заводилась починкой, а после разбора
+9 -9
View File
@@ -25,25 +25,25 @@
Тест выше говорит, **допустим** ли разрез. Где его провести из нескольких
допустимых мест — отвечает шов.
**Шов — там, где падает ступень ревью.** Раздел «Затрагивает» перечисляет
границы; если одна строка перечня поднимает ступень выше остальных, эта часть и
**Шов — там, где падает метка ревью.** Раздел «Затрагивает» перечисляет
границы; если одна строка перечня поднимает метку выше остальных, эта часть и
режется отдельно. Пример: задача перекладывает несколько узлов разом и заодно
добавляет два поля в существующий ответ. Целиком это `wide` — семь проходов по
добавляет два поля в существующий ответ. Целиком это `large` — семь проходов по
всему диффу, включая два, что держат машину и идут цепочкой. Разрезанная по шву,
она даёт `wide` на маленькой переложенной части и `standard` на остатке.
она даёт `large` на маленькой переложенной части и `medium` на остатке.
**Считай костяк, а не файлы.** У каждой задачи есть несокращаемые четыре прохода
(гейт, спеки, код, триаж), и они платятся за каждую. Разрез, после которого обе
половины остаются в одной ступени, делает ревью **дороже**: тот же объём
половины остаются в одной метке, делает ревью **дороже**: тот же объём
проверяется тем же составом, но костяк оплачен дважды. Отсюда правило: **резать,
когда разрез снимает дорогой проход с большей части диффа**, и не резать, когда
он просто делает файлы мельче.
**Это планирование, а не предписание процесса.** Ступень ревью выбирается по
**Это планирование, а не предписание процесса.** Метка ревью выбирается по
факту изменения — тем, кто его видит, — и в тело задачи не пишется: строка
«делать профилем standard» это ровно тот второй дом правила выбора, который
гигиена полей снимает. Шов пользуется ступенью как **признаком**, что в задаче
две разнородные работы; решение о профиле остаётся за конвейером.
«делать с меткой medium» это ровно тот второй дом правила выбора, который
гигиена полей снимает. Шов пользуется меткой как **признаком**, что в задаче
две разнородные работы; решение о метке остаётся за конвейером.
**Цель наследуется.** Все части несут `goal:` родителя: декомпозиция не меняет
того, чему работа служит. Если у части цель другая — это признак, что дробили не