Handbooks
ガイド

適用とロールバック

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>                          # 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 はファイルシステムに決して触れません。

4 つの安全ルール

1. すべてを検証してから、2 フェーズで書き込む

プランはまず現在のファイル内容に対して解決されます。1 つでも失敗すれば、1 バイトも 書き込まれる前に適用全体が中止されます。 書き込みはすべてのファイルを一時ファイルとして ステージし、すべての ステージングが成功したときにだけリネームします — そして途中で リネームが失敗した場合、リネーム済みのファイルは直前に取得されたバックアップから復元されます。

プランの半分だけが着地した状態は存在しません。

2. old はバイト単位で正確かつ一意にマッチしなければならない

マッチ数結果
0no-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 始まりの行番号付き
createdold が空だったため、ファイルが作成された
no-matchold がファイル内に存在しない
ambiguousold が複数回出現する
file-missingold は空でないが、該当ファイルが存在しない
not-a-fileパスがディレクトリまたはシンボリックリンクである
unsafe-pathパスがソースルートから脱出している
undecodableファイルが有効な UTF-8 ではない
skipped先行する失敗により実行が中止された

applyok が 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 を早く閉じてしまった可能性が高い — より長いフェンスで開いてください
タグのない ``` ブロック同じ原因。どこにあっても拒否されるため、切り詰められたアンカーが「エピローグ」として紛れ込むことはありません
oldnew がちょうど 1 つずつでないそれぞれ何個見つかったか
newold より先にある先にアンカー、その後に置換テキストを書いてください
oldnew と同一やることがありません
- 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

最新に保つを参照してください。

このページの内容