# Канон документов проекта **Версия 7.** Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs` читают его, а не пересказывают: три описания одной раскладки разъедутся, и работать будет то, которое прочитали последним. Меняется канон — меняется этот файл и появляется запись в [changelog.md](changelog.md). ## Зачем канон жёсткий Пути фиксированы, и проект под них подгоняется, а не наоборот. Причина не техническая: проектов много, все малого и среднего размера, и ориентироваться в слегка похожих, но разных раскладках дороже, чем один раз привести их к общей. Рядом лежит OpenSpec, у которого структура тоже строгая. Цена принята сознательно: плагин не переносится на чужой репозиторий как есть — чужой репозиторий **приводится** к канону скиллом `canon`. Раскладка отвечает, **где** текст лежит и на какой вопрос отвечает. Каким он должен быть **словами** — общий для всех документов канона файл [language.md](language.md): информационный стиль, англицизмы, жаргон. Он относится и к задачам, и к решениям ADR, и к запискам разведки. ## Сопровождение и эксплуатация — целое и часть Одна тема живёт в трёх местах канона, и путать их слова нельзя. **Сопровождение** — всё, чем держат проект: инструмент и сборка, процесс, выкладка, метрики и логи, инфраструктура, дежурство. **Эксплуатация** — его часть: работа системы на проде. Целое и часть, и никогда наоборот. | Место | Уровень | Что там | | --- | --- | --- | | `ROADMAP.md`, секция `Сопровождение` | план | **работы**, которые собираемся делать: цели и их задачи | | `architecture.md`, раздел «Эксплуатация» | состояние | **как устроено сейчас**: где работает, что рядом, кто перезапускает | | тема ревью `operations` | оптика | **чем проверяем**: «это упало через неделю на проде» | Слово **«поддержка» не употребляется вовсе** — в нём слышится помощь пользователю, а это другая работа. **Граница с возможностями проходит по тому, кто наблюдает.** «Приложение сообщает о своём состоянии» — возможность приложения, её место среди прочих целей: наблюдает пользователь сервиса. «Дежурный видит состояние на одном экране» — сопровождение: наблюдаем мы. Одни и те же метрики попадают в разные секции роадмапа, и это верно — секции отвечают на разные вопросы. ## Раскладка **Документ канона живёт файлом или каталогом.** `docs/security.md` и `docs/security/` — одно и то же; форму выбирает проект по объёму написанного, и переход между формами не меняет ни канон, ни версию. Обе формы сразу — ошибка: два дома для одного факта расходятся молча. ``` CLAUDE.md памятка агенту: что это, стек, инварианты с severity, команды, семантика гейта, запреты AGENTS.md необязателен, лежит рядом; читается теми же docs/ .pm.json версия канона и пути, нужные проверкам passport.md | passport/ зачем и для кого; чем НЕ является; сценарии architecture.md | architecture/ как сложено — обзор; окружение и эксплуатация database.md | database/ схема хранилища; представление данных и настройки security.md | security/ периметр; недоверенный вход; что вне модели conventions.md | conventions/ как пишем код; что механизировано research.md | research/ наблюдения и числа с провенансом adr.md | adr/ почему решено так; статусы, правило замены review.md | review/ настройка конвейера под проект + журнал дефектов <своя тема>.md | <своя тема>/ всё, что проект счёл нужным проверять tasks/ скилл tasks: items/, ROADMAP.md, BACKLOG.md, SPRINT.md, REJECTED.md openspec/ config.yaml только нужды генерации артефактов + ссылки specs//spec.md что система делает — нормативно changes/archive/ архив изменений с design.md — сырьё для ADR ``` **У документа-каталога обязателен `README.md`** — вход, по которому его читают агенты. `adr/` в форме каталога держит ещё и `template.md`, а записи именуются `ADR-ГГГГ-ММ-ДД-slug.md`. ## Три категории документов Раньше здесь стояло плоское правило «каждый документ `docs/` — тема ревью». Оно неверно ровно наполовину: паспорт и схема хранилища ревью нужны, но темами не являются, а журнал решений и журнал наблюдений ревью изменения не нужны вовсе. Плоское правило заставляло разметчика либо плодить фантомные темы, либо терять документы молча — а молчащая потеря и есть то, против чего канон написан. **Разрез один и проверяемый: можно ли по документу сказать «в этом изменении сделано не так»?** | Категория | Ответ на разрез | Что с ней делает ревью | | --- | --- | --- | | **тема** | да, прямо | заводит направление проверки и требует исполнителя | | **источник темы** | нет, но он задаёт границу, по которой судит чужая тема | читается как материал, своей темы не порождает | | **процессный документ** | нет: он про то, как мы работаем, а не про изменение | не судит по нему изменение | | Документ | Категория | Куда питает | | --- | --- | --- | | `conventions.*` | тема | `conventions` | | `security.*` | тема | `security` | | `architecture.*` | тема | `architecture`; раздел эксплуатации — `operations` | | *свой документ проекта* | тема | своя тема, именем документа | | `passport.*` | источник | `architecture` — граница домена, «чем **не** является» | | `database.*` | источник | `operations` — схема и настройки с числами | | `CLAUDE.md`, `AGENTS.md` | источник | `autotests` (семантика гейта); инварианты — сквозные | | `openspec/specs/` | источник | `requirements` | | `openspec/config.yaml` | процессный | — (настройка порождения артефактов, слой **до** тем) | | `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 и задач — английские, kebab-case.** Причина не эстетическая: имя файла стоит в ссылках из других документов, в коммитах и в путях, которые набирают руками, — а кириллица в пути ломается по-разному в разных местах и не набирается на английской раскладке. **Транслита не заводим.** Слаг именуется английским словом **по сути**, а не записью русского латиницей: `queue-as-table`, а не `ochered-tablicej`. Транслит нечитаем тому, кто ищет по смыслу, и не сокращается. У ADR имя вдобавок несёт форму — `ADR-ГГГГ-ММ-ДД-slug.md`: по ней записи сортируются, и по ней же ищется дата решения. `docs.py check` проверяет кириллицу и kebab-case **жёстко**, форму имени ADR — тоже, а транслит **эвристикой**, то есть замечанием: английское слово от транслита машина не отличает. Слаги каталога задач ведёт `tasks.py` — там та же проверка и тот же разрез. **Переименование — не правка, а перенос ссылок**: делается одним проходом по всем местам, где имя упомянуто, иначе останутся битые ссылки. Для задач это умеет `tasks.py adopt`; для документов канона правит человек, а `docs.py` потом показывает, что ссылки целы. ## Роли документов и темы ревью Одна строка на каждый — на какой вопрос он отвечает, в какой он категории и какую тему питает. **Кто именно закрывает тему, здесь не указано намеренно**: это зависит от метки прогона и меняется вместе с конвейером, а документ живёт дольше. Раскладку «тема → проход → глубина» держит скилл `av-dev-pipeline:review-pipeline`. **Общего словаря у канона с конвейером ровно три вида имён: имена категорий, имена тем и имена меток.** Категорий три — `тема`, `источник`, `процессный`; **меток тоже три, и они закрыты: `small`, `medium`, `large`.** Метка это итог классификации задачи, и по ней конвейер выбирает исполнителей на обеих стадиях ревью; проект её не выдумывает, а только уточняет триггеры. Ими проект и настраивает ревью — вопросами по темам и триггерами метки. **Имён проходов канон не называет нигде**, включая вывод `docs.py`: проход переименовывается и переезжает между метками, и канон, назвавший его, в этот день соврёт молча. Обратное направление законно — конвейер называет документы канона поимённо, потому что он их читатель. | Документ | Вопрос | Категория и тема | | --- | --- | --- | | `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` Цель; закрытый список потребителей и что каждому нужно; **чем целью не является** — это граница домена, по которой архитектурный проход судит о переносе понятия; типовые сценарии; мера, по которой проект считается удавшимся; референсы, у кого подсматривать. ### `architecture.md` — **обзор, не поведение** Принципы; компоненты **со ссылками на capability**, а не с пересказом их требований; **единые точки проекта** — где генерируются идентификаторы и время, где единственный парсер входного формата, где маппинг доменной ошибки в код ответа, где общий путь приёма (это материал для вопроса «не появился ли второй способ»); внешние границы и форматы чужих систем; окружение — где работает, что рядом, кто перезапускает; **внешние зависимости поимённо** и чем каждая отказывает (не только «падает», но и «отвечает медленно», «молчит», «отдаёт мусор»); кто заметит отказ и когда; характер потока — непрерывный, по запросу, по расписанию; деплой; открытые вопросы. **Обратимости здесь нет** — её единственный дом `CLAUDE.md`: туда ходят пять проходов, и раздвоение адреса означало бы, что проект написал ответ, а ревью его не прочитало. **Поведение системы сюда не пишется.** Его нормативный дом — `openspec/specs/`, куда `opsx:archive` вливает дельты; второй дом синхронизировать руками невозможно, и он разойдётся. Раздел, ещё не разнесённый при переезде, помечается маркером долга: ``` ``` `docs.py` считает маркеры и печатает остаток числом. Гейт от них **не краснеет**: это долг, а не отказ, иначе постепенный переезд стал бы невозможен. ### `database.md` Схема: таблицы, ключи, связи, правило времени и идентификаторов. Плюс то, чего нет в схеме, но без чего замер не превращается в находку: **чем физически лежит запись** (сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи (распаковка целиком, read-modify-write), и **настройки с числовым значением** — таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен. Конвенции идентификаторов и именования — не схема, они в `conventions/`. ### `security.md` **Периметр первой строкой.** «Сервис открыт наружу» и «контур доверенный, публичного интернета здесь нет, не выдумывай его» — противоположные постановки под одним заголовком, и разбор темы `security` между ними сам не выберет. Контур ещё не развёрнут — назови **оба** периметра, целевой и сегодняшний, и скажи прямо, против какого строятся находки. Дальше: что недоверенное и каким каналом приходит; **из чего строятся пути и ключи** (раскладка файлов, состав координатного ключа, имя каталога) — отсюда строится выход за пределы песочницы; что разграничивает доступ; что чувствительнее чего; **что вне модели** — перечислить явно. ### `conventions/` Прозой остаётся **только то, что не выражается правилом**. `README.md` держит индекс, правило промоута и **перечень уже механизированного** со ссылкой на место механизации — конфиг линтера, собственный анализатор, тест-сканер исходников. Не названное место механизации означает, что проход добросовестно проверит уже проверенное. ### `research/` Наблюдения за внешним миром: что реально шлёт источник, чем документация формата расходится с практикой, какие числа сняты с живого потока. **Числа — с провенансом**, то есть с командой или условиями, которыми получены. `README.md` — как снималось и индекс тем. Число без источника проход обязан читать как условие, а не как замер. Число, чей источник по ссылке не подтвердился, не выбрасывается и не переписывается по догадке — остаётся с пометкой «расходится с источником: там <что нашли>». ### `adr/` **ADR — промоут поверх архивных `design.md`, а не второе сочинение.** Запись цитирует решение и ссылается на `openspec/changes/archive//design.md`. Заводится, когда верно одно из трёх: - **дорогой откат** — переделка стоит дороже переписывания одного файла; - **намеренный отказ** от очевидного подхода; - **пересмотр прежнего решения** — тогда у старой записи обязателен статус «заменено на». Не заводится для рутины и для того, что видно из кода и `git log`. Записи неизменяемы: передумали — заводится новая, старая получает статус. Активная запись статуса не имеет. **Статус живёт полем меты записи**, там же, где дата и источник: `- **Статус:** заменено на ADR-…` либо `- **Статус:** устарело`. Места ему в шаблоне не отводилось, и каждая запись изобретала своё — то абзацем, то заголовком; в таблице `adr/README.md` статус при этом обязан быть, а брать его оттуда, где он у каждого свой, нельзя. ### `review.md` Два раздела с разными сроками жизни. **Настройка конвейера под проект**, пять подразделов с точными именами — по ним проходы находят свой кусок: - **Типовые узлы** — рода узлов проекта и 3–5 проверяемых свойств к каждому; - **Типовые ложноположительные** — находки, которые здесь выглядят убедительно и всегда неверны, каждая со строкой «почему здесь это не дефект»; - **Вопросы по темам** — в форме `<тема>: <вопрос> (<провенанс>)`. **Не по именам проходов**: проход уезжает между метками, а тема остаётся, и вопрос, адресованный проходу, перестал бы задаваться молча в тот день, когда тот уехал в старшую метку. Задаёт вопрос тот, кто закрывает тему на этом прогоне. Адресовать можно только теме: `passport`, `database`, `adr`, `research` и `review` — не темы, и вопрос, адресованный им, не задаст никто; - **Триггеры метки** — проектная конкретизация правила выбора метки ревью, **тремя списками**. Два поднимают, по одному на ось: что в этом проекте считается **крупным** (объём: сколько узлов и слоёв трогает) и что считается **незнакомым** (форма решения: известна до начала или нащупывается по ходу). Любая из двух осей поднимает прогон до `large`, старшей метки, — а она рассчитана на 5–10% задач. Третий список — что считается **мелким** (опускает до `small`); он один, потому что вниз метку опускает только совпадение обеих осей сразу. Перечнем мест, узлами или capability, а не вторым определением класса. Уточняет умолчания, а не отменяет их. Рабочее умолчание — `medium`: миграция схемы и публичный контракт метку **не** поднимают, их проверяют проходы, которые в `medium` и так есть; - **Недоступно проверке** — два подраздела, оба **по темам**: «не проверит ни один проход» (принципиальная граница, по факту промаха не пересматривается) и «перестали проверять сознательно» (пересматривается первым). Тема, у которой в проекте нет дома, сюда не пишется: её и так называет план каждого прогона. **Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой **проскочил / пойман ревью**. Проскочившие — проверочный набор для калибровки конвейера, выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные, воспроизводимые, однажды оказавшиеся правдой. ### `tasks/` Раскладку, форму записи и команды держит скилл `tasks` — канон фиксирует имена файлов (`items/`, `ROADMAP.md`, `BACKLOG.md`, `SPRINT.md`, `REJECTED.md`) и то, от чего зависит, читается ли проект как продукт. **`ROADMAP.md` отвечает на «что приложение уже умеет и чего ещё не умеет».** Это не очередь работ: цель — **возможность приложения**, задача — шаг к ней. Достигнутая цель из роадмапа **не исчезает** — строка с датой переезжает в секцию достигнутого, потому что «что умеет» и есть половина вопроса, ради которого документ открывают. Вторым домом поведения роадмап при этом не становится: нормативное поведение живёт в `openspec/specs/`, роадмап отвечает, **когда и в каком порядке** оно появилось. **У каждой записи есть тип, и тип решает, что с ней можно делать.** Дом типа — поле меты `Тип` первой строкой; эмодзи в заголовке от него производна. Словарь закрыт: | Тип | Что это | | --- | --- | | 🎯 `goal` | возможность приложения | | ✨ `feature` | снаружи появляется то, чего не было | | 🐞 `fix` | поведение расходится с заявленным | | 🧹 `chore` | обслуживание, поведение не меняется | | 🔬 `research` | исход — знание, а не изменение | **Схемы записи здесь нет намеренно.** Какие разделы тип требует, нужна ли ему цель и берётся ли он в спринт — скилл `tasks`: сводка в его [SKILL.md](../../tasks/SKILL.md), раздел «Тип записи», подробно — по файлу на тип в `tasks/references/task-<тип>.md`. Канон фиксирует **словарь**, потому что от него зависит, читается ли проект как продукт; схема — механика ведения задач, и второй её экземпляр разошёлся бы с первым (он и разошёлся: канон успел объявить цель у `fix` запрещённой, хотя она там необязательна). Схема требуется **к взятию в спринт**, а не к заведению: беклог пополняется чаще, чем разбирается, и требование на входе выгоняло бы в заметки то, что должно лежать задачей. Запись, не собравшая разделы своего типа, — законное состояние беклога; невзятой её делает `sprint take`. Отдельного типа для незаполненной записи нет: «ещё не описано» — состояние, а не род работы, и называется оно **`research` без раздела «Вопрос»**. Такая запись в спринт не берётся и лежит в конце своей категории. Раскладку, форму записи и алгоритм работы над каждым типом держит скилл `tasks`. ### `CLAUDE.md` Что это и стек; **инварианты с severity рядом с формулировкой** — по ним проходы присваивают `critical`, поэтому severity стоит здесь, а не выводится каждым проходом заново; команды; **семантика гейта** — чем краснеет безусловно и почему, где логи, что означает исход, чего в гейте намеренно нет, **кто и когда обязан гонять дорогое вне гейта**. Плюс то, что нужно git-операциям и проходам и не выводится ниоткуда: - **имя основной ветки** — от неё считается база диффа (`git merge-base HEAD <ветка>`), в неё вливает батч, от неё ветвятся задачи. Угадывание между `master` и `main` ломает интеграцию целиком; - **что запускать запрещено, с путями** — рабочая БД, боевой каталог данных, внешние сервисы. Запретом с путями, а не «будь осторожен»; - **где `testdata`** и что в них лежит; **куда писать временное**; - **что считается необратимым** — единственный дом: от обратимости зависит вся шкала ранжирования триажа и право проходов на `critical`; - **общий станок**, врывающийся в замороженный спринт; **ориентир по размеру спринта**. ### `openspec/config.yaml` **Только нужды генерации артефактов** — язык, правила именования capability, придирки валидатора RFC 2119 — плюс **адреса** документов канона. Правило ревью, пересказ конвенций и инварианты сюда не пишутся: у них есть свои дома, и второй дом разойдётся на первой же правке. **Каталог `openspec/` — часть канона, а не соседняя технология.** В нём дом темы `requirements`, и заводится он командой: `openspec init --tools claude`. Её выполняет `init` на новом проекте и `adopt` на переводимом; из канона она названа поимённо потому, что её печатает отказ `docs.py`, а отказ без команды заставляет искать её в другом месте. **Файл из коробки настройкой не является.** `openspec init` кладёт `config.yaml`, где и `context`, и `rules` лежат закомментированным примером. Такой файл читается как настроенный — он есть, он валиден, у него правильное имя, — а работает как пустой: предложение пишется без языка, без правил именования capability и без знания, где лежит граница домена. Это ровно тот класс, против которого написан весь канон, и потому здесь он проверяется машиной, а не чтением. Проверяется пять вещей, и каждая — про молчащий пробел, а не про вкус: 1. **`openspec/` есть.** Нет — нет и дома темы `requirements`. 2. **Имя файла `config.yaml`.** `config.yml` OpenSpec не читает и об этом не сообщает: настройка, написанная в файл с таким именем, пропадает целиком. 3. **`context` и `rules.specs` не остались примером.** Правила для `specs` обязаны называть `SHALL`: требование без этого литерала валидатор отвергает. 4. **`context` называет `passport` и `CLAUDE.md`.** Предложение пишется **до** того, как кто-либо откроет `docs/`; без этих двух адресов его пишут, не зная ни границы домена, ни инвариантов. 5. **Ключи под `rules:` — имена артефактов схемы** (`proposal`, `specs`, `design`, `tasks`). Правило, адресованное несуществующему артефакту, не применяется и об этом молчит: `rules.spec` вместо `rules.specs` — конфиг, выглядящий написанным и не работающий. **Схема и перечень артефактов — слепок чужого инструмента, и он стареет.** OpenSpec переименует артефакт или сменит схему — правила под прежним именем перестанут действовать молча, а канон будет продолжать требовать прежнее. Поэтому за свежестью слепка следит машина: `check` сравнивает `major.minor` установленного OpenSpec с версией, на которой форма сверялась, и при расхождении даёт **замечание** (не отказ: патч-версии формы не меняют, а нагоняй на каждый багфикс приучает пролистывать блок). Перепроверяет `docs.py openspec-form` — он спрашивает сам инструмент и печатает, что разошлось. **Чинится это в плагине, а не в проекте:** константы скрипта, скелет и запись в журнал версий канона. Шестого — «нет ли здесь пересказа» — машина не проверяет: отличить ссылку от пересказа она не умеет. Это работа `doc-consistency`, и раздел «Что проверяет машина, а что человек» называет её строкой. Форма — [skeletons.md](skeletons.md). ## Правило единственного дома Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора: | Факт | Дом | | --- | --- | | поведение системы | `openspec/specs//spec.md` | | почему решено так | `adr/`, источник — архивный `design.md` | | граница домена, «чем не является» | `passport.md` | | инвариант и его severity | `CLAUDE.md` | | что приложение умеет и чего не умеет; порядок работ | `docs/tasks/ROADMAP.md` | | измеренное число | `research/` | | настройка с числовым значением | `database.md` | | периметр и модель угроз | `security.md` | | что необратимо | `CLAUDE.md` — **не** `architecture.md` | | единые точки проекта | `architecture.md` | | имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` | | что уже механизировано правилом | `conventions/README.md` | ## Пустое называется пустым Скелет канона заводится **целиком** с первого дня. Незаполненный документ держит **одну честную информативную строку**, а не заглушку: - «внешних зависимостей нет — смотри на диск и на СУБД»; - «наблюдений на живых данных нет: внешний источник один, формат документирован»; - «прецедентов не накоплено»; - «сознательно ничего не отключали»; - «архитектуры пока нет: кода нет, заводится первой задачей». Проход читает такую строку **как факт** и не тратит на неё обязательный вопрос. Отсутствие файла он не может прочитать никак, а «TBD» читает как пробел — поэтому `docs.py check` отличает честную строку от нетронутого плейсхолдера шаблона и напоминает о втором. ## Слотов нет Файлы и каталоги, которых в каноне **нет**, и куда уезжает их содержимое: | Было | Куда | | --- | --- | | `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` | | `docs/specs/` | `openspec/specs/` (поведение) и `architecture.md` (обзор) | | `docs/drafts/` | идея → запись `research`; отказ → ADR; порядок → `ROADMAP.md`; размышление → `opsx:explore` | | `docs/plan.md` | `docs/tasks/ROADMAP.md` | | `BRIEF.md` | `passport.md` | | `docs/backlog/` | `docs/tasks/` | | `docs/review-journal.md`, `docs/review/journal.md` | `docs/review.md` | ## Что проверяет машина, а что человек Граница объявляется вслух в каждом отчёте: `check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего. | Проверяет `docs.py` | Судит агент | Какой | | --- | --- | --- | | отсутствующие пути канона | смысловой дубль документа и capability | `doc-consistency` | | файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` | `doc-consistency` | | имя файла не kebab-case латиницей; форма имени ADR | транслит в имени — сверх эвристики | `doc-wording` | | битые относительные ссылки | прямое противоречие между документами | `doc-consistency` | | версия канона и её отставание | достаточность честной строки в пустом слоте | `doc-consistency` | | нетронутый плейсхолдер шаблона | ADR без ссылки на `design.md`, замена без парного статуса | `doc-consistency` | | маркеры долга — числом | **протухший факт, разошедшийся с кодом** | `doc-code-drift` | | миграция изменена, а `database.md` нет | зависимость в манифесте, не названная в обзоре | `doc-code-drift` | | capability без упоминания в `architecture.md` | второй способ там, где обзор обещал единственный | `doc-code-drift` | | `openspec/config.yaml`: имя, `schema`, незаменённый пример, адреса паспорта и `CLAUDE.md`, ключи `rules` против артефактов схемы | **пересказ документа канона в `context` вместо ссылки** | `doc-consistency` | | версия OpenSpec разошлась с той, на которой сверена форма `config.yaml` | придирки валидатора: сменились ли они | никакой — проявляются отказом `openspec validate --strict` | | | связность и читаемость | `doc-wording` | **Агентов двое, и разведены они по глубине, а не по охвату.** `doc-consistency` читает только `docs/` и `openspec/`, `doc-code-drift` — весь репозиторий и гоняет читающие команды. Слитый агент делал бы одну половину поверхностной; тот же разрез, что между `task-form` и `doc-wording`. **Зовутся оба одинаково — раз в спринт на сессии, а также после `adopt` и после `upgrade`, на весь канон разом.** Не на синке документации: `doc-consistency` на `opus` по каждой сделанной задаче не окупается, а расхождение между двумя документами по определению требует двух, и на большинстве задач синк правит один. Пачка, отбираемая работой, вдобавок не видит того, чего работа не касалась, — а именно там расхождение и живёт: правка отменяет решение в одном документе, парный статус нужен в другом. **Перечень фактов, которые `doc-code-drift` сверяет с кодом, закрыт** — имя основной ветки, команды, пути, зависимости поимённо, настройки с числовым значением, единые точки проекта, capability, проверяемые инварианты. «Сверить архитектуру с кодом» задача без дна, и агент, которому её поставили, выдаёт правдоподобную труху вместо находок. ## `docs/.pm.json` ```json { "canon": 7, "migrations": "internal/store/migrations", "tasks": { "backlog": "INDEX.md" } } ``` `canon` — версия канона, под которую проект приведён, целым числом: обратной совместимости у канона нет, есть «приведён» и «не приведён». `migrations` — путь каталога миграций, если БД есть; по нему `docs.py` делает сверку с `database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего `/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**. Внутри `tasks` — **только имена файлов и заголовков** (`items`, `backlog`, `roadmap`, `sprint`, `rejected`, `sprint_section`, `oracle_word` и заголовки разделов тела: `criteria_heading`, `surface_heading`, `questions_heading`, `completion_heading`, `repro_heading`, `question_heading`, `answer_heading`, `scope_heading`), и ключ пишется, лишь когда имя отличается от умолчания. **Словаря типов здесь нет** — он закрыт каноном, а не настраивается проектом: настраиваемый словарь типов разъехался бы на синонимах ровно так же, как открытый. **Категорий беклога здесь тоже нет:** их дом — заголовки `##` самого индекса, и второй список сразу разошёлся бы с первым. Неизвестный ключ `tasks.py` отвергает кодом 3, поэтому лишнее слово в этом объекте останавливает работу с задачами целиком. Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py` игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом строкой, а не молчит.