TL;DR
У соло-разработчика CLAUDE.md устаревает быстрее кода. Через пять-шесть итераций фич он начинает уверенно врать, и Claude верит ему, потому что у него больше нечему верить. Ниже - про понятие документационного долга, кейс с пропавшей подсистемой, пять проверок руками на пятнадцать минут и метод двух слоев + двух сессий, который заменяет код-ревью для соло-разработчика. Главный вывод: одноразовая чистка не работает, документация для AI - это процесс (сюрприз!), не артефакт.
1. Симптом
Открываю Claude Code, прошу «продолжить вчерашнюю работу с ХХХ». Он уверенно вызывает функцию validate_post_length, которую я переименовал три недели назад. Замечаю, спорю, переименовываю в промпте обратно. Через час он просит «применить миграцию» для базы, которую я уже перевез с SQLite на Postgres месяц назад. Под вечер генерирует текст на 2400 знаков (есть у меня такой своеобразный сервис), хотя всю дорогу ждут от него 4000.
В каждом случае моя реакция инстинктивная: «модель тупит». Иду в Anthropic Console, проверяю кэш, перезапускаю сессию, добавляю в промпт «учти, что сейчас Postgres, не SQLite». На пятый-шестой эпизод раздражение становится сильнее любопытства, и я смотрю в свой CLAUDE.md. Там - версия проекта трехнедельной давности. И функция, и SQLite, и длина 2400 - все аккуратно описано как актуальное состояние. Это не баг Claude. Это баг моей документации.
В предыдущей статье про правила контроля Claude Code я отдельно выделял документацию как внешнюю память модели - без нее каждая новая сессия начинается с изобретения колеса. Этот текст - продолжение: про то, что происходит, когда внешняя память уже есть, но врет. У этой проблемы есть минимум три формы, и самая гадкая из них ловится плохо даже своим взглядом.
Соло-разработчик в эпоху AI-помощников получает этот сценарий снова и снова, потому что в одиночку он одновременно автор, ревьюер и тимлид - и роль «следить, чтобы доки не отставали от кода» имеет приоритет точно ниже, чем «доделать фичу». Через две-три итерации фич между обновлениями документации CLAUDE.md начинает врать. Через пять-шесть итераций - врет уверенно, и Claude верит ему, потому что ему же больше нечему верить.
Я опишу три типа документационного долга, который я нашел в собственных проектах; разбор самого тяжелого случая; чек-лист (а как же без него) на три-пять минут без всяких инструментов; описание процесса, который я пытаюсь поддерживать; и список из трех вопросов, которые я для себя еще не закрыл. Поехали.
2. Почему так происходит
Главная разница между документацией для команды и документацией для AI-помощника - у Claude нет долгосрочной памяти на ваш проект. У вашего коллеги, который работал с вами полгода, есть «встроенный» контекст: он помнит, что мы переехали на Postgres, помнит шутку про backup-скрипт, помнит, что таблицу users_old нельзя трогать. Claude этого не помнит. Каждое утро, а если сессия закрылась быстро, то и каждые два часа, он открывает CLAUDE.md, прочитывает его за минуту, и эта минута становится единственным источником его картины проекта. Любое расхождение между документом и кодом становится для него ложным сигналом, по которому он будет действовать так же уверенно, как если бы это было правдой. Он не «думает», не обольщайтесь. И это первое важное обстоятельство.
К этому добавляется экономический фактор. Стартовое чтение, это обычно CLAUDE.md плюс какой-то файл с актуальным состоянием, вроде STATE.md, - это килобайты, которые Claude грузит в контекстное окно при каждом старте сессии. У одного из моих проектов это 156 КБ. То есть каждая новая сессия съедает порядка $0.30 только на чтение некоторой, возможно, неактуальной истории. В месяц при моем режиме работы это около пятидесяти долларов. Половина из них - на абзацы, описывающие фичи, которых уже нет, или планы, которые я давно реализовал.
Соло-разработчик платит этот налог из своего кармана, и платит дважды: деньгами за токены и временем на исправление ошибок, которые модель сделала, поверив инструкции. Чем дольше живет проект, тем дороже становится каждая ошибка. И одновременно, тем менее вероятно, что соло-разработчик регулярно ревизирует документацию: «фичи важнее», «потом», «пока работает». Даже если это опытный разработчик.
У меня в работе сейчас девять соло-проектов, я их регулярно прогоняю через одни и те же проверки, и из девяти у одного - 100 из 100 (мета-инструмент, на котором я отрабатываю методику; ему положено быть образцовым). У остальных средний показатель - около 53 из 100 по автоматическим проверкам. Это не «плохие проекты», у каждого из них есть CLAUDE.md, у большинства есть STATE.md, у части есть отдельные ARCHITECTURE.md. Это нормальное состояние соло-проектов с документацией, которую никто не ревизирует. Половина текста описывает уже несуществующее.
3. Три типа долга
Я для себя выделил три типа документационного долга. Они отличаются тем, насколько сложно их поймать и насколько дорого они обходятся.
Первый тип - «документ не успевает за кодом». Самый частый. Я переименовал функцию, а README ссылается на старое имя. Поменял переменные окружения - .env.example не валиден и содержит мусор. Поменял минимальную версию языка в pyproject.toml - в требованиях документа стоит старая. Это ловится grep’ом и небольшой автоматизацией: парсер собирает упоминания из документации, сравнивает с реальным кодом, выдает разницу. У восьми из девяти моих проектов в .env.example сейчас есть мертвые переменные - суммарно около сорока штук, накопившиеся за пару месяцев рефакторингов.
Второй тип - «документ врет сам себе». Один документ говорит «длина X», другой - «длина Y», третий - «длина Z». Каждый внутренне последователен, но они не сходятся друг с другом. Поймать сложнее: ни один из них формально не «устарел» - они просто конфликтуют. У одного из моих проектов в шапке документа стоит «Auto-generated, не редактировать вручную», а сам документ показывает «последние закрытые спринты: 98, 99», тогда как реально закрыты 196, 197, 201. Скрипт-генератор сортирует имена файлов по алфавиту, и sprint_99 оказывается «после» sprint_201 лексикографически. Шапка про авто-генерацию создает ложное доверие, читатель открывает документ и работает с картиной мира из марта.
Третий тип - «документ описывает то, чего больше нет». Самый сложный. За три недели в проекте появилась подсистема - несколько модулей, новый внешний интеграционный канал, конфигурация, кеши. Документация про это не знает. Не упоминает ни единым словом. Поймать автоматически почти невозможно: формально все упомянутые в доке файлы существуют, ссылки не битые, никаких явных противоречий. Просто документация описывает половину системы вместо целой.
Этот третий тип я и хочу разобрать в деталях, потому что именно он самый дорогой в тех смыслах, которые упоминал выше. И именно его я недооценивал, пока не наступил.
Кейс: служба мониторинга, которая «потеряла» половину себя
Я пишу проект, это служба мониторинга. Python-скрипты, которые раз в день читают базы данных в продакшне, выявляют аномалии (просевший RPS, переполненные очереди, медленные запросы) и шлют мне сводку. Соло, без CI, без коллег. Все, как мы любим. Существует три месяца. Не самый сложный проект.
Последний раз я серьезно обновлял документацию 10 дней назад. Тогда было опрятно: один большой документ methodology.md с описанием слоев проверок и семи модулей в lib/, короткий README, документ «как продолжить сессию завтра» с точным STATE.md-аналогом. По цифрам автоматических проверок документации я получал тогда 50 из 100. Невысоко, но рост был стабильный.
За эти 10 дней к проекту прирос целый слой. Двусторонняя интеграция с трекером задач (наш внутренний инструмент): скрипт пушит найденные проблемы как тикеты, читает ответы в комментариях, отслеживает просроченные задачи, генерирует ranking «что чинить первым». Технически это:
Один файл на 38 КБ -
sync_findings_to_tracker.py. Главная точка интеграции, делает несколько API-вызовов на цикл.Один файл на 16 КБ -
tracker_events.py. Watchdog, читающий ответы в комментариях.Еще около 6 КБ во вспомогательных модулях - кеши, ranking, форматтеры.
Всего около 60 КБ нового кода. По объему это больше, чем весь остальной мониторинг до этого. По важности - это центр коммуникации с заказчиком, на который теперь завязан мой рабочий процесс. В документации про эту подсистему ни единого слова. Слово «трекер» и его аналоги не встречаются ни в README, ни в methodology.md. Я просто писал код и не обновлял.
При следующем прогоне моих автоматических проверок проект получил 47 из 100 - даже чуть выше, чем 10 дней назад. Никаких новых критических замечаний, никаких сигналов о скрытой подсистеме. Автоматика первого слоя не отличает «настоящего» от «отсутствующего», она проверяет только то, что упомянуто.
И только когда я попросил Claude (ниже расскажу об этом более подробно) прочитать проект целиком, как если бы он был сторонним аудитором, обнаружились четыре зацепки. Все четыре формально мелкие, но в совокупности они описывают именно состояние «документация устарела до неузнаваемости».
Зацепка первая. README перечисляет содержимое папки lib/ - пять-шесть модулей с именами. Реально там 18 файлов. README не упоминает 12 из них. Половина системы для читателя не существует, он даже не знает, что ее надо искать.
Зацепка вторая. README упоминает «ADR-016» и «ADR-020» по номерам, без расшифровки темы, в разделе «Architecture decisions» - формат «см. ADR-016 / ADR-020». Папки docs/adr/ в репозитории нет вообще, ноль файлов с именем ADR-*. Это документы-призраки: упомянуты, как будто существуют, но их нет. Особенно неуютно от того, что номера 16 и 20 - в самом конце последовательности; именно они, если бы существовали, как раз могли бы описывать новую подсистему, про которую больше нигде ни слова.
Зацепка третья. В README сказано: «запуск по субботам в 09:00 МСК». В methodology.md: «запуск по воскресеньям в 12:00». Это два соседних файла в одной папке, написанные одним человеком (мной) с разницей примерно в неделю. Несовпадение, которое можно заметить, только если читать оба файла сразу.
Зацепка четвертая. В шапке требований: «Python ≥ 3.9». В коде интенсивно используется синтаксис dict | None - PEP 604, оператор | на типах появился только в 3.10. Если кто-то развернет проект на Python 3.9, получит TypeError: unsupported operand type(s) for |: 'type' and 'NoneType' при первом обращении к коду с такой аннотацией в runtime. С from __future__ import annotations ошибка отъезжает дальше - до первой реальной проверки типа через isinstance или вычисления dict | None в коде, не в подписи. В любом случае - проект на 3.9 не работает, документация занижает требования на минор.
Каждая из четырех зацепок по отдельности - мелочь. Каждая отдельно объясняется забывчивостью или копи-пастой. Но вместе они описывают одно: документация не пересекалась с кодом последнее время. Она замкнулась на саму себя и стала независимой реальностью.
Почему этот тип ловится плохо
Третий тип сложно поймать автоматически, потому что отсутствие - не паттерн. Регулярка ищет упоминания. Парсер ищет существующие функции и сравнивает с ссылками в доке. Линтер ищет битые пути. Все эти инструменты исходят из предположения, что в документе что-то есть, и проверяют это что-то. Если в документе чего-то нет - инструмент не знает, чего именно ему не хватает.
Это слепое пятно. И единственный способ его закрыть, который я пока нашел, требует включения второго слоя проверок, о котором речь дальше. Но сначала пару слов о том, что можно сделать руками сегодня, без всякого второго слоя.
4. Что сделать сегодня: пять проверок без инструментов
Если читаете эту статью и уже чешутся руки, то вот пять проверок, которые я делаю руками, когда у меня нет под рукой ничего автоматизированного. На маленьком проекте это несколько минут на все. Каждая ловит свой класс проблем.
Проверка 1. Размер стартового чтения.
В терминале проекта:
wc -c CLAUDE.md STATE.md
Складываете два числа. Ориентир - суммарно не больше 50 КБ. Это эмпирический порог: при 50 КБ и выше Claude в новой сессии тратит ощутимое количество токенов на чтение, и среди этих токенов почти гарантированно есть устаревшие. У одного из моих проектов было 156 КБ - каждая сессия начиналась с минуты на «погрузиться в контекст». Сейчас 41 КБ; разница в скорости старта весьма заметна.
Проверка 2. Расхождение переменных окружения.
Простой способ - два списка, потом comm. Под Python ловим три самых частых паттерна (os.environ.get("X"), os.getenv("X"), os.environ["X"]), плюс параллельно для JS - process.env.X:
# Переменные, упомянутые в коде (Python + JS) { grep -rEho "os\.(environ\.get|getenv|environ\[)\(?['\"][A-Z_]+['\"]" src/ grep -rEho 'process\.env\.[A-Z_]+' src/ } | grep -oE '[A-Z_]{3,}' | sort -u > /tmp/in-code.txt # Переменные в .env.example grep -oE '^[A-Z_]+' .env.example | sort -u > /tmp/in-example.txt # Что есть в коде, но нет в примере comm -23 /tmp/in-code.txt /tmp/in-example.txt
Если на выходе непусто, то кто-то, клонирующий проект завтра, не запустит его, потому что у него .env не будет содержать обязательной переменной. У части проектов проверка возвращает 5-15 строк. У одного аж 30. Тридцать обязательных переменных, не упомянутых в шаблоне .env.example.
Проверка 3. Битые ссылки в .md.
grep -rE '\]\([^)]+\.md\)' --include='*.md' . | head -50
Проверяем: каждая ссылка [X](path) указывает на существующий файл? У .md-документации с историей рефакторингов всегда обнаружится 5-20 ссылок на файлы, которые переехали или удалены. У одного из моих проектов прогон находил пять десятков битых ссылок. Большая часть в архивных промптах спринтов, которые я давно не открывал, но они формально остаются «доступной документацией» в графе ссылок. Для автоматизации той же проверки есть готовые штуки - lychee (Rust, быстрый) и markdown-link-check (npm); сырой grep нужен, только когда хочется ad-hoc, одной строкой, без установки зависимостей.
Проверка 4. Файлы, на которые никто не ссылается (orphans).
# Все .md в проекте (для monorepo фильтруем */node_modules/* через *, # а не ./node_modules/* - иначе пропустятся вложенные node_modules в packages/*) find . -name "*.md" \ -not -path "*/node_modules/*" -not -path "*/.venv/*" \ -not -path "*/target/*" -not -path "*/dist/*" -not -path "*/build/*" \ -not -path "*/vendor/*" -not -path "*/.git/*" \ | sort > /tmp/all.md # Все .md, упомянутые из README/CLAUDE.md/docs/README.md grep -hoE '[a-zA-Z0-9_/-]+\.md' \ README.md CLAUDE.md docs/README.md 2>/dev/null \ | sort -u > /tmp/linked.md # Что осталось comm -23 /tmp/all.md <(sed 's|^|./|' /tmp/linked.md | sort)
Это грубая эвристика, но она быстро показывает картину. Файлы, оставшиеся в списке, - либо устарели и их пора в архив (docs/_archive/), либо актуальны и их надо явно залинковать из точек входа. Третьего обычно нет. У моих проектов в среднем было 20 таких сирот; на самом разросшемся - 148.
Проверка 5. Прочитать собственный CLAUDE.md глазами «новичка».
Откройте CLAUDE.md и прочитайте его как разраба, которого позвали на проект на один день. Где спотыкаетесь? Где аббревиатура без расшифровки? Где «как обычно», без указания, что такое «обычно»? Где ссылка на документ, до которого вы не дойдете, потому что он не залинкован из точек входа? У меня каждый раз набирается 3-5 таких мест. Это нормальное состояние документа, который автор писал из своей сегодняшней головы; для новичка и для Claude в новой сессии оно непрозрачно.
5. Если хотите систематически: метод
Пять проверок выше - это не панацея. Они находят типовые косяки. Третий тип долга, о котором писал выше, - это «документация не знает о существующей подсистеме». Этого они не находят. После того, как я наступил на одни и те же грабли в пяти-шести проектах, я разработал для себя инструмент, который автоматизирует аудит документации. Я расскажу про метод.
Главная идея - два слоя проверок
Первый слой - автоматические правила. Парсеры, регулярки, AST-сканеры. Они быстрые (секунды на проект), стабильные, годятся в pre-commit hook или в недельный cron. Они ловят:
Расхождения между упомянутыми в
README.mdкомандами и реальным CLI (парсерclick/argparseсравнивает заявленные опции с действительно определенными).Расхождения между переменными в
.env.exampleи переменными в коде.Битые ссылки и orphan-файлы.
Версии, заявленные в документации и в
pyproject.toml/package.json.Размер стартового чтения относительно эмпирического порога (50 КБ).
Количество тестов, заявленное в
README(«350+ тестов»), и реальное (pytest --collect-only).
Этот слой отвечает на бинарный вопрос «есть или нет», и он бессилен против третьего типа долга, потому что для него «отсутствие подсистемы в доке» выглядит как «все нормально, ничего не упомянуто, ничего не сломано».
Второй слой - Claude в роли независимого аудитора. Я открываю отдельную сессию, отдельный workspace (не тот, где я пишу код), и прошу: «прочитай проект, посмотри код, посмотри документацию, и скажи, где они не согласованы». Это медленно (минуты, не секунды), стоит токенов и требует ручного запуска. Но именно второй слой ловит:
Концепции, которые в коде есть, а в доке отсутствуют.
Терминологическую несогласованность (одно и то же называется по-разному в разных файлах).
Документы, которые внутренне последовательны, но конфликтуют друг с другом.
Случаи, когда документ декларирует одно (например, «auto-generated»), а ведет себя по-другому.
Разница между результатами двух слоев - отдельный сигнал. У одного из моих проектов первый слой дает 57 из 100, второй - 84 из 100. Это значит: «автоматические проверки слишком строги, реально проект сильно лучше, чем кажется» - обычно так бывает у OSS-проектов, где документация ушла на сайт через mkdocs, а локальные эвристики автоматики этого не понимают. У другого: 47 из 100 первый, 32 из 100 второй. Это значит: «автоматика недосчитывает скрытый долг, реально хуже» - и именно это был мой кейс из третьего раздела с пропавшей подсистемой.
Главный рабочий прием - две сессии Claude Code одновременно
Одна сессия - в мета-инструменте, где живут проверки и история прогонов. Эта сессия диагностирует: запускает первый слой, опционально второй, формирует список рекомендаций. Каждая рекомендация - это конкретная правка с цитатой файл:строка, severity (Critical / Major / Minor), и формулировкой в форме вопроса («согласовать ли замену X на Y?»), а не команды («сделай X»). Это важно: Claude в формате команд начинает «выполнять список», а в формате вопросов ждет согласования каждого пункта.
Вторая сессия - в самом проекте, в его рабочей директории. В нее копируется список рекомендаций. Дальше я иду по списку по одному пункту: «давай» - применяется, «пропусти» - следующий, «иначе» - обсуждаем альтернативу. Каждая правка фиксируется в отдельном коммите. После того, как пройден весь список, в первую сессию возвращается короткий отчет: «8 из 10 применено, 1 пропущено, 1 заменено альтернативой».
Это и есть замена код-ревью для соло-разработчика. Два «голоса», разнесенные по сессиям, при том что автор один. Первая сессия не знает, что делает вторая, и наоборот - это структурно гарантирует независимость диагноза и применения. Если бы я делал это в одной сессии, Claude быстро смешал бы «что нужно сделать» и «как мы это уже делаем», и независимости не было бы.
Замер из моей практики: одна такая ревизия закрыла 8 рекомендаций за один заход, 11 коммитов в main за ~3 часа. Автоматические проверки выросли с 61 до 66, Claude-аудит - с 55 до 82. Большая часть прироста - за счет того, что вторая сессия закрыла реальные дыры (мертвый код в .env.example, дрифт между прод-IP в шести файлах, орфан-документы), а не за счет косметики.
Главное в подходе - не инструмент, а двухслойность и независимость сессий. Это паттерн, который собирается, имея только Claude Code и пару git коммитов; готовый инструмент его только ускоряет.
6. Что надо решить
Метод выше работает. Но это не финальная картина - у меня есть открытый список того, что я знаю, что не закрыл. Перечислю три самых важных.
Первое: как ловить «hidden subsystems» автоматически.
В кейсе из третьего раздела (служба мониторинга) все четыре зацепки - несуществующий ADR, расхождение в расписании, занижение версии языка, потерянные модули в lib/ - ловятся только когда я прошу Claude перечитать проект целиком. Это раз в неделю, в лучшем случае. Я хочу сделать детектор, который смотрит на статистику: «за окно последних 30 дней в кодовой базе появилось X КБ нового кода. За те же 30 дней в документации появилось Y КБ. Если X / Y больше определенного порога - поднять флаг». В простом виде это однодневная задача. В содержательном (учесть, что не любой новый код требует документ, отфильтровать тесты, миграции, авто-генерированное) - это уже исследование, и я его пока не сделал. Возможно, правильное решение лежит в анализе git-истории: смотреть, сколько коммитов в src/ за окно соответствуют коммитам в docs/ или README.md. Эту метрику никто из инструментов, которые я видел, не считает.
Второе: соло-специфика штрафуется как командная.
Большинство классических метрик качества документации построены под команды: наличие CONTRIBUTING.md, шаблон pull request, отдельный RELEASING.md. Если у вас соло-проект, все это формально отсутствует, то и автоматическая оценка опускается на 20-30 пунктов. У 8 из моих 9 проектов эти три пункта - стабильные ложные срабатывания, тянущие общий балл вниз и обесценивающие остальные сигналы. Решение очевидно: понижать строгость для проектов с одним автором (определяется по git log за 30 дней) или с явным маркером project_type: solo в конфигурации. У меня этого пока нет, потому как нужно переделывать веса осей и пороги, а вместе с ними и систему severity. Руки не дошли, лежит в открытых задачах.
Третье: документация как процесс, а не как артефакт.
Каждая ревизия дает прирост. Я ловлю долг, чищу, проект растет в оценке. Через неделю-две все снова разрастается: STATE.md обрастает событиями за каждый день, CLAUDE.md накапливает «прецеденты» («в Sprint 32 мы решили, что…») вместо правил, мертвые env-переменные снова накапливаются. У одного из моих проектов через пять дней после идеальной ревизии STATE.md снова разросся с 30 КБ до 71 - за это время прошло пять спринтов, каждый дописал в файл по 8 КБ.
Без автоматических триггеров - pre-commit hook на размер STATE.md, недельный cron на расхождение .env.example, скрипт-архиватор по дате последней модификации - соло-проект всегда деградирует к следующей ревизии. Одноразовая чистка не работает. Это самое важное нерешенное: не как чинить долг, а как сделать так, чтобы он перестал накапливаться. Хороший аналог из мира кода - линтер в pre-commit: не лечит существующий код, но не дает новому коду уходить от стандарта. Для документации такой структуры у меня пока не выработалось.
Дмитрий Волошин, сооснователь и генеральный директор OTUS, основатель Клуба менторов, основатель Школы бизнеса Ninox. Заметки про управление, найм и фаундерский опыт: t.me/coffee_notes
