Handbooks
Руководства

Применение и откат

Механический исполнитель с четырьмя правилами безопасности, резервная копия, способная доказать, что она восстанавливает, и парсер, отвергающий всё неоднозначное.

handbook apply --source <repo> --plan plan.md --dry-run   # verify only
handbook apply --source <repo> --plan plan.md             # for real
handbook rollback --backup <dir>                          # undo

LLM не участвует. apply подставляет точный текст вместо точного текста. Всё интересное в нём — то, что он отказывается делать.

Сначала всегда пробный прогон

handbook apply --source $REPO --plan plan.md --dry-run
{
  "ok": true,
  "dryRun": true,
  "outcomes": [
    { "index": 1, "file": "src/upload.py", "where": "Uploader.send (~88)", "status": "applied", "line": 88 },
    { "index": 2, "file": "src/upload.py", "where": "Uploader", "status": "applied", "line": 71 }
  ],
  "changedFiles": [],
  "problems": []
}

ok: true означает, что каждый якорь разрешился. changedFiles пуст, потому что ничего не записывалось. --dry-run никогда не трогает файловую систему.

Четыре правила безопасности

1. Сначала проверить всё, затем записать в две фазы

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

Не существует состояния, в котором приземлилась половина плана.

2. old должен совпадать байт-точно и единственным образом

СовпаденийРезультат
0no-match — код ушёл вперёд с момента написания плана
1применено
2+ambiguous — якорь не указывает на единственное место

Оба сбоя — отказ. Ни один не выбирает за вас. «Взять первое вхождение» — ровно тот способ, которым патч приземляется не в ту функцию.

3. Каждый затронутый файл сохраняется в резервную копию с хешем до патча

<source>/.handbook-patches/
  .gitignore                     written automatically — backups never enter git
  2026-08-08T14-05-11-204Z/
    manifest.json                source root, timestamp, per-file pre/post hashes
    files/…                      the original bytes

Именно хеш позволяет откату доказать, что он восстанавливает байты, которые заменил этот патч, а не доверять имени файла.

4. Ни один путь не выходит за корень исходников

.., абсолютные пути, абсолютные пути Windows с буквой диска — и побеги через симлинк на родительский каталог, когда сам файл ещё не существует. Последний случай — самый тонкий: realpath берётся от самого глубокого существующего предка, поэтому отсутствующий лист не может проскочить проверку. Цели-симлинки никогда не заменяются.

Статусы исходов

СтатусЗначение
appliedЗаменено, с номером строки (нумерация с 1), где найден old
createdold был пуст; файл создан
no-matchold в файле отсутствует
ambiguousold встречается более одного раза
file-missingНепустой old, но такого файла нет
not-a-fileПуть — каталог или симлинк
unsafe-pathПуть выходит за корень исходников
undecodableФайл — не валидный UTF-8
skippedБолее ранний сбой прервал запуск

apply выходит с кодом 2, когда ok равен false.

Откат

handbook rollback --backup $REPO/.handbook-patches/2026-08-08T14-05-11-204Z \
                  --source $REPO
  • Отказывается от любого файла, изменённого после патча. Его текущий хеш больше не совпадает с пост-патчевым хешем в манифесте — значит, кто-то с тех пор его редактировал, и восстановление молча уничтожило бы эту работу. --force переопределяет — намеренно явно.
  • --source страхует в другую сторону: направить откат на резервную копию, снятую с другого дерева, — это ошибка, а не возможность.
  • Права файлов, окончания строк и завершающий перевод строки сохраняются на всём пути. Патчер не нормализует ничего, что его не просили менять.
  • Пустые каталоги, созданные самим откатом, вычищаются.
ls -1t $REPO/.handbook-patches/     # newest first

Почему парсер враждебен к неоднозначности

Отслеживание ограждений следует CommonMark и для backtick-, и для tilde-ограждений: блок, открытый серией из N маркеров, закрывается только строкой, чья серия ≥ N и не несёт info string. Поэтому ### EDIT n внутри огороженной области — содержимое, а не заголовок: план, цитирующий пример правки, не может протащить в запуск фантомную правку.

ОтклоняетсяСообщение подскажет
Содержимое между огороженными блоками правкиВнутреннее ограждение, вероятно, закрыло old/new раньше времени — откройте их более длинным ограждением
Блок ``` без меткиТа же причина; отклоняется, где бы он ни стоял, чтобы обрезанный якорь не проскочил как «эпилог»
Не ровно один old и один newСколько каждого он нашёл
new раньше oldСначала пишется якорь, затем замена
old идентичен newНечего делать
Отсутствующая или продублированная строка - file:Требуется ровно одна
Номера правок не по порядку или продублированыОни должны возрастать
Путь с пробелами, обратными кавычками, управляющими символами, обратными слэшами, ~ или ведущим /Какое именно правило нарушено
Заголовок-почти (## EDIT 1)Он похож на заголовок, но это не ### EDIT <n>

Завершающая проза и блок деклараций после последней пары old/new — ожидаемый вывод; они игнорируются, а не отклоняются.

Написание плана вручную

Ничто не требует, чтобы план приходил из handbook plan. Формат достаточно мал, чтобы писать его напрямую, — а это делает apply полезным механическим патчером и сам по себе:

### EDIT 1

- file: `src/config.py`
- where: `DEFAULTS` — bump the timeout

```old
TIMEOUT_SECONDS = 30
```

```new
TIMEOUT_SECONDS = 60
```

Проверьте план без применения:

import { parsePlan } from '@handbooks/patcher';
console.log(parsePlan(planText).problems);

После приземления

Handbooks теперь отстаёт от кода. Прокатите его вперёд:

handbook resync --case cases/upload-retry --work work/api

См. Поддержание актуальности.

На этой странице