Перейти к основному содержимому

ADR 0029 — Непрерывная синхронизация документации

Контекст​

Репозиторий дошёл до v0.1.0 с 28 ADR, обширным CLAUDE.md и шестью runbooks — и без единого документа, объясняющего систему целиком. Дело не в недостатке написанного: документация общего вида не рождается из точечного решения, а ADR не заменяет карту.

Миссия документирования создала этот набор. Следующая проблема труднее первой: документация умирает не от недостатка начального написания, она умирает от дрейфа. Код меняется, документация остаётся, и наступает день, когда она описывает систему, которой больше нет. С этого момента никто не доверяет ни одной странице, включая правильные.

Начальная разведка измерила размер риска в этом репозитории:

  • 96 коммитов за один месяц, при этом apps/api/src/db/schema.ts изменялся 23 раза — самый изменчивый файл проекта;
  • 89 переменных окружения, 64 идентификатора событий, 110 HTTP-маршрутов и 13 типов действий, все как свободные строки в коде, без объединения типов, которое заставило бы кого-то обновить список;
  • уже существующий .docmap.yml, у которого восемь glob указывали на несуществующие пути (apps/api/drizzle/**, k8s/**, helm/**), унаследованные от прежней структуры каталогов.

Последний пункт самый поучительный. Glob, который ничему не соответствует, не падает — он просто никогда не срабатывает. Правило существует в файле, создаёт впечатление покрытия и ничего не защищает. Карта ответственности без валидации деградирует молча ровно так же, как документация, которую она должна защищать.

Решение​

Установить механизм, а не добрые намерения, на трёх уровнях убывающей надёжности: генерировать > проверять > напоминать.

1. Генерировать, где список и есть содержимое​

docs/reference/scripts.md целиком выходит из pnpm docs:generate, извлекаемый из package.json и аннотированных целей Makefile. Прозы для сохранения нет, поэтому нет причины кому-то поддерживать список вручную.

2. Проверять, где проза ценнее списка​

configuration.md и events.md пишутся вручную — колонка "когда что-то идёт не так" и "когда происходит это событие" — реальная ценность, и никакой скрипт этого не напишет. Но список должен быть полным.

Решение — блок между <!-- BEGIN:GENERATED:<id> --> и <!-- END:GENERATED:<id> --> внутри написанного вручную файла. Блок — это механическая инвентаризация; текст вокруг — объяснение. Инвентаризация отмечает то, что существует в коде и не имеет описания в прозе.

Это нашло два реальных типа событий при первом запуске — agent.response и tool.result, последний выдаётся hook Engine.Harness.Hooks.EventLog, — которые ручное извлечение упустило.

3. Напоминать, где нужно человеческое суждение​

docs/.docmap.yml связывает пути кода с документами, которые от них зависят, с серьёзностью block или warn. Скрипт сопоставляет diff PR с картой и требует то, что не было обновлено.

Четыре решения внутри этого:

Карта валидируется в CI. Мёртвый glob проваливает до любой другой проверки. Сломанная карта заставляет лгать всё остальное.

Аварийный выход обязателен. Метка docs-not-needed или строка docs-not-needed: <motivo> в теле PR снимают проверку. Без легитимного выхода формируется привычка обходить — декоративный коммит в документацию только чтобы gate прошёл, — и тогда механизм начинает лгать, что хуже, чем его отсутствие.

Ложноположительное срабатывание — дефект, а не допустимый шум. Первая версия проверки переменных указала на семь пунктов, которые были задокументированы под сокращением POSTGRES_HOST / _USER / _PASSWORD. Проверку научили раскрывать сокращение. Проверка, которая ошибается, приучает читателя её игнорировать.

Сборка сайта — самый дешёвый gate. onBrokenLinks, onBrokenAnchors и onBrokenMarkdownLinks в throw: перенос файла без исправления тех, кто на него указывает, роняет CI вместо того, чтобы стать 404 в продакшене.

4. Периодический аудит, для того, что PR не ловит​

Drift check ловит документацию, ставшую неверной в PR. Документация, ставшая старой, без того чтобы кто-то её трогал, ничего не запускает. Ежемесячный аудит сообщает о застоявшейся странице, чей соответствующий код изменился позже, о незакрытых TODO(humano), о ссылках arquivo:linha, которые не разрешаются, и об ADR в proposed дольше 60 дней — всегда в одной и той же issue, обновляемой. Новая issue каждый месяц становится спамом, а спам отключают.

5. Единый источник, сайт, который только читает​

Markdown живёт в docs/ в корне. Сайт Docusaurus в website/ читает оттуда через path: '../docs', и website/docs/ никогда не существует. Дублированное содержимое — это содержимое, которое расходится.

Последствия​

PR становится дороже. Изменение apps/api/src/domain/** без обновления business-rules.md проваливается. Это намеренная цена: альтернатива — гниение документации, цена которого проявляется месяцами позже, рассеянная и большая.

Карта требует сопровождения. Переименованный каталог оставляет мёртвый glob — теперь CI на это указывает, но кто-то должен исправить. Это новая работа, малая и видимая, в обмен на старую работу, большую и невидимую.

Предупреждения иногда будут ошибаться. Карта работает по пути файла, а не по семантике: рефакторинг, переименовывающий внутренние переменные, запускает dominio-e-regras, не меняя ни одного правила. Для этого существует аварийный выход. Если одно и то же правило отклоняется три раза за одну неделю, проблема в правиле: сузьте glob или понизьте серьёзность.

Механизм не проверяет, корректен ли текст. Он проверяет, был ли текст пересмотрен при изменении кода и полны ли списки. Фактически неверная фраза, которую никто не трогал, проходит все проверки. Для этого существует человеческое чтение и slash command /sync-docs.

GitHub Pages требует публичного репозитория или плана Enterprise. Deploy сайта будет падать, пока репозиторий приватный. Job сборки в PR — который несёт ценность gate, — работает в обоих случаях.

Две новые зависимости в корне, yaml и picomatch, обе для разработки и покрытые предварительно одобренным исключением для инструментов документации.

Весь механизм объяснён в docs/explanation/documentation-workflow.md, включая то, что делать, когда он жалуется несправедливо. Механизм, который никто не понимает, — это механизм, который кто-то отключит.