適用とロールバック
4 つの安全ルールを持つ機械的な実行器、何を復元するかを証明できるバックアップ、そして曖昧なものを一切拒否するパーサー。
handbook apply --source <repo> --plan plan.md --dry-run # verify only
handbook apply --source <repo> --plan plan.md # for real
handbook rollback --backup <dir> # undoLLM は関与しません。 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 はファイルシステムに決して触れません。
4 つの安全ルール
1. すべてを検証してから、2 フェーズで書き込む
プランはまず現在のファイル内容に対して解決されます。1 つでも失敗すれば、1 バイトも 書き込まれる前に適用全体が中止されます。 書き込みはすべてのファイルを一時ファイルとして ステージし、すべての ステージングが成功したときにだけリネームします — そして途中で リネームが失敗した場合、リネーム済みのファイルは直前に取得されたバックアップから復元されます。
プランの半分だけが着地した状態は存在しません。
2. old はバイト単位で正確かつ一意にマッチしなければならない
| マッチ数 | 結果 |
|---|---|
| 0 | no-match — プランが書かれた後にコードが先へ進んでしまった |
| 1 | 適用 |
| 2+ | ambiguous — アンカーが単一の箇所を特定していない |
どちらの失敗も拒否されます。どちらの場合も 1 つを選んだりはしません。 「最初の出現を 取る」は、パッチが間違った関数に着地する典型的な経路です。
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 | 置換された。old が見つかった 1 始まりの行番号付き |
created | old が空だったため、ファイルが作成された |
no-match | old がファイル内に存在しない |
ambiguous | old が複数回出現する |
file-missing | old は空でないが、該当ファイルが存在しない |
not-a-file | パスがディレクトリまたはシンボリックリンクである |
unsafe-path | パスがソースルートから脱出している |
undecodable | ファイルが有効な UTF-8 ではない |
skipped | 先行する失敗により実行が中止された |
apply は ok が false のとき 終了コード 2 で終了します。
ロールバック
handbook rollback --backup $REPO/.handbook-patches/2026-08-08T14-05-11-204Z \
--source $REPO- パッチ後に変更されたファイルはすべて拒否します。 現在のハッシュがマニフェスト内の
パッチ後ハッシュと一致しないということは、その後に誰かが編集したということです —
それを復元すれば、その作業を黙って破壊してしまいます。
--forceで上書きできますが、 これは意図的に明示的な操作になっています。 --sourceは逆方向を守ります: 別のツリーから取得したバックアップにロールバックを 向けるのは間違いであって、機能ではありません。- ファイルモード、改行コード、末尾の改行は終始保持されます。 パッチャーは、変更を 求められていないものを一切正規化しません。
- ロールバック自身が作成した空ディレクトリはクリーンアップされます。
ls -1t $REPO/.handbook-patches/ # newest firstなぜパーサーは曖昧さに対して敵対的なのか
フェンスの追跡は、バッククォートとチルダの両方のフェンスについて CommonMark に従います:
N 個のマーカーの並びで開かれたブロックは、マーカーの並びが N 個以上 かつ info string を
持たない行でのみ閉じられます。したがって、フェンス領域内の ### EDIT n は 常にコンテンツ
であり、決して見出しではありません — 編集例を引用するプランが、幻の編集を実行に紛れ込ませる
ことはできません。
| 拒否されるもの | メッセージが伝えること |
|---|---|
| 編集のフェンスブロックの間にあるコンテンツ | 内側のフェンスが old/new を早く閉じてしまった可能性が高い — より長いフェンスで開いてください |
| タグのない ``` ブロック | 同じ原因。どこにあっても拒否されるため、切り詰められたアンカーが「エピローグ」として紛れ込むことはありません |
old と new がちょうど 1 つずつでない | それぞれ何個見つかったか |
new が old より先にある | 先にアンカー、その後に置換テキストを書いてください |
old が new と同一 | やることがありません |
- file: 行の欠落または重複 | ちょうど 1 つ必要です |
| 編集番号の順序違反または重複 | 昇順でなければなりません |
空白、バッククォート、制御文字、バックスラッシュ、~、先頭の / を含むパス | どのルールに違反したか |
見出しに似て非なるもの(## 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
```適用せずに lint する:
import { parsePlan } from '@handbooks/patcher';
console.log(parsePlan(planText).problems);着地した後で
ハンドブックはもうコードに遅れています。前へ進めましょう:
handbook resync --case cases/upload-retry --work work/api最新に保つを参照してください。