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,
включая то, что делать, когда он жалуется несправедливо. Механизм, который никто не
понимает, — это механизм, который кто-то отключит.