# Канон документов проекта **Версия 3.** Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs` читают его, а не пересказывают: три описания одной раскладки разъедутся, и работать будет то, которое прочитали последним. Меняется канон — меняется этот файл и появляется запись в [changelog.md](changelog.md). ## Зачем канон жёсткий Пути фиксированы, и проект под них подгоняется, а не наоборот. Причина не техническая: проектов много, все малого и среднего размера, и ориентироваться в слегка похожих, но разных раскладках дороже, чем один раз привести их к общей. Рядом лежит OpenSpec, у которого структура тоже строгая. Цена принята сознательно: плагин не переносится на чужой репозиторий как есть — чужой репозиторий **приводится** к канону скиллом `canon`. ## Раскладка ``` CLAUDE.md памятка агенту: что это, стек, инварианты с severity, команды, семантика гейта, запреты docs/ .pm.json версия канона и пути, нужные проверкам passport.md зачем и для кого; чем НЕ является; сценарии architecture.md как сложено — обзор; окружение и эксплуатация database.md схема хранилища; представление данных и настройки security.md периметр; недоверенный вход; что вне модели conventions/ README.md индекс, правило промоута, что механизировано <тема>.md research/ README.md как снималось, индекс <тема>.md наблюдения и числа с провенансом adr/ README.md индекс записей, статусы, правило замены template.md ADR-ГГГГ-ММ-ДД-slug.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 ``` Текст документов — русский; слаги файлов, capability и задач — английские, kebab-case. ## Роли документов Одна строка на каждый — на какой вопрос он отвечает и кто его читает. | Документ | Вопрос | Кто читает, кроме человека | | --- | --- | --- | | `CLAUDE.md` | что нельзя нарушать, чем краснеет гейт | все агенты, всегда | | `passport.md` | зачем и для кого, чем это **не** является | `architecture`, `rubric`, `reimpl`, `specs` | | `architecture.md` | как сложено и где что работает | все проходы ревью | | `database.md` | что лежит в хранилище и какими настройками | `ops`, `adversary`, `reimpl` | | `security.md` | против кого защищаемся и что вне модели | `adversary` | | `conventions/` | как мы пишем код | `code` | | `research/` | что показала реальность, а не документация | `specs`, `reimpl`, `ops`, `adversary` | | `adr/` | почему решено именно так | `architecture` | | `review.md` | как настроен конвейер и что уже проскакивало | `triage`, каждый проход — свою часть | | `openspec/specs/` | что система делает — нормативно | `specs` | ### `passport.md` Цель; закрытый список потребителей и что каждому нужно; **чем целью не является** — это граница домена, по которой архитектурный проход судит о переносе понятия; типовые сценарии; мера, по которой проект считается удавшимся; референсы, у кого подсматривать. ### `architecture.md` — **обзор, не поведение** Принципы; компоненты **со ссылками на capability**, а не с пересказом их требований; **единые точки проекта** — где генерируются идентификаторы и время, где единственный парсер входного формата, где маппинг доменной ошибки в код ответа, где общий путь приёма (это материал для вопроса «не появился ли второй способ»); внешние границы и форматы чужих систем; окружение — где работает, что рядом, кто перезапускает; **внешние зависимости поимённо** и чем каждая отказывает (не только «падает», но и «отвечает медленно», «молчит», «отдаёт мусор»); кто заметит отказ и когда; характер потока — непрерывный, по запросу, по расписанию; деплой; открытые вопросы. **Обратимости здесь нет** — её единственный дом `CLAUDE.md`: туда ходят пять проходов, и раздвоение адреса означало бы, что проект написал ответ, а ревью его не прочитало. **Поведение системы сюда не пишется.** Его нормативный дом — `openspec/specs/`, куда `opsx:archive` вливает дельты; второй дом синхронизировать руками невозможно, и он разойдётся. Раздел, ещё не разнесённый при переезде, помечается маркером долга: ``` ``` `docs.py` считает маркеры и печатает остаток числом. Гейт от них **не краснеет**: это долг, а не отказ, иначе постепенный переезд стал бы невозможен. ### `database.md` Схема: таблицы, ключи, связи, правило времени и идентификаторов. Плюс то, чего нет в схеме, но без чего замер не превращается в находку: **чем физически лежит запись** (сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи (распаковка целиком, read-modify-write), и **настройки с числовым значением** — таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен. Конвенции идентификаторов и именования — не схема, они в `conventions/`. ### `security.md` **Периметр первой строкой.** «Сервис открыт наружу» и «контур доверенный, публичного интернета здесь нет, не выдумывай его» — противоположные постановки под одним заголовком, и враждебный проход между ними сам не выберет. Контур ещё не развёрнут — назови **оба** периметра, целевой и сегодняшний, и скажи прямо, против какого строятся находки. Дальше: что недоверенное и каким каналом приходит; **из чего строятся пути и ключи** (раскладка файлов, состав координатного ключа, имя каталога) — отсюда строится выход за пределы песочницы; что разграничивает доступ; что чувствительнее чего; **что вне модели** — перечислить явно. ### `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 проверяемых свойств к каждому; - **Типовые ложноположительные** — находки, которые здесь выглядят убедительно и всегда неверны, каждая со строкой «почему здесь это не дефект»; - **Вопросы к проходам** — поимённо, в форме `<имя прохода>: <вопрос> (<провенанс>)`; - **Триггеры профиля** — проектная конкретизация правила выбора профиля ревью: что в этом проекте считается **новым понятием или структурной единицей** (это поднимает прогон до `wide`) и **где живут правила идентичности, слияния и разбора** (до `deep`) — перечнем мест, производным от теста конвейера, а не вторым определением класса. Уточняет умолчания, а не отменяет их. Рабочее умолчание — `standard`: миграция схемы и публичный контракт ступень **не** поднимают, их проверяют проходы, которые в `standard` и так есть; - **Недоступно проверке** — два подраздела: «не проверит ни один проход» (принципиальная граница, по факту промаха не пересматривается) и «перестали проверять сознательно» (пересматривается первым). **Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой **проскочил / пойман ревью**. Проскочившие — эвал-сет для калибровки конвейера, выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные, воспроизводимые, однажды оказавшиеся правдой. ### `tasks/` Раскладку, форму записи и команды держит скилл `tasks` — канон фиксирует имена файлов (`items/`, `ROADMAP.md`, `BACKLOG.md`, `SPRINT.md`, `REJECTED.md`) и то, от чего зависит, читается ли проект как продукт. **`ROADMAP.md` отвечает на «что приложение уже умеет и чего ещё не умеет».** Это не очередь работ: цель — **возможность приложения**, задача — шаг к ней. Достигнутая цель из роадмапа **не исчезает** — строка с датой переезжает в секцию достигнутого, потому что «что умеет» и есть половина вопроса, ради которого документ открывают. Вторым домом поведения роадмап при этом не становится: нормативное поведение живёт в `openspec/specs/`, роадмап отвечает, **когда и в каком порядке** оно появилось. Плюс два требования к записи задачи, потому что от них зависит, можно ли её оценить: - **род работы** тегом `kind:<род>` из закрытого словаря `feature` | `fix` | `chore` | `research` — у задачи обязателен, у цели запрещён. Он же решает, нужна ли цель: у `feature` обязательна, у остальных нет; - **раздел «Затрагивает»** в теле задачи — границы, которых изменение касается (эндпоинт, таблица и миграция, формат на диске, публичный тип пакета). Оба требуются **к взятию в спринт**, а не к заведению: беклог пополняется чаще, чем разбирается, и требование на входе выгоняло бы в заметки то, что должно лежать задачей. ### `CLAUDE.md` Что это и стек; **инварианты с severity рядом с формулировкой** — по ним проходы присваивают `critical`, поэтому severity стоит здесь, а не выводится каждым проходом заново; команды; **семантика гейта** — чем краснеет безусловно и почему, где логи, что означает исход, чего в гейте намеренно нет, **кто и когда обязан гонять дорогое вне гейта**. Плюс то, что нужно git-операциям и проходам и не выводится ниоткуда: - **имя основной ветки** — от неё считается база диффа (`git merge-base HEAD <ветка>`), в неё вливает батч, от неё ветвятся задачи. Угадывание между `master` и `main` ломает интеграцию целиком; - **что запускать запрещено, с путями** — рабочая БД, боевой каталог данных, внешние сервисы. Запретом с путями, а не «будь осторожен»; - **где `testdata`** и что в них лежит; **куда писать временное**; - **что считается необратимым** — единственный дом: от обратимости зависит вся шкала ранжирования триажа и право проходов на `critical`; - **общий станок**, врывающийся в замороженный спринт; **ориентир по размеру спринта**. ### `openspec/config.yaml` **Только нужды генерации артефактов** — язык, правила именования capability, придирки валидатора RFC 2119 — плюс ссылки на документы канона. Правило ревью, пересказ конвенций и инварианты сюда не пишутся: у них есть свои дома, и второй дом разойдётся на первой же правке. ## Правило единственного дома Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора: | Факт | Дом | | --- | --- | | поведение системы | `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/` | идея → задача `[idea]`; отказ → 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 | | файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` | | битые относительные ссылки | протухший факт, разошедшийся с кодом | | версия канона и её отставание | достаточность честной строки в пустом слоте | | нетронутый плейсхолдер шаблона | связность и читаемость | | маркеры долга — числом | | | миграция изменена, а `database.md` нет | | | capability без упоминания в `architecture.md` | | ## `docs/.pm.json` ```json { "canon": 2, "migrations": "internal/store/migrations", "tasks": { "backlog": "INDEX.md" } } ``` `canon` — версия канона, под которую проект приведён, целым числом: обратной совместимости у канона нет, есть «приведён» и «не приведён». `migrations` — путь каталога миграций, если БД есть; по нему `docs.py` делает сверку с `database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего `/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**. Внутри `tasks` — **только имена файлов и заголовков** (`items`, `backlog`, `plan`, `sprint`, `rejected`, `sprint_section`, `questions_heading`, `criteria_heading`, `oracle_word`), и ключ пишется, лишь когда имя отличается от умолчания. **Секций беклога здесь нет:** их дом — заголовки `##` самого индекса, и второй список сразу разошёлся бы с первым. Неизвестный ключ `tasks.py` отвергает кодом 3, поэтому лишнее слово в этом объекте останавливает работу с задачами целиком. Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py` игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом строкой, а не молчит.