これまでの4回で、仕様書の書き方・報告の検証術・完了確認の設計を書きました。それでも壊れることはあります。 第5回は、壊れたときにどこまで戻れるようにしておくかという話です。
結論を先に書きます。壊れたときの復元手段として、gitを使わせていません。 一見遠回りですが、理由があります。
1. バックアップが要る操作は、決まっている
TAKTにファイル全体を書き換えうる操作をさせるときは、必ず事前バックアップを指示しています。対象は次の4種類です。
- インプレース一括置換(
perl -i・sed -iなど、-iを伴うもの) - 変異(ミューテーション)テストでの実装ファイルの改変
- 版数の更新など、巨大な行を含む設計文書への文字列置換
- リダイレクトによる上書き(
... > 対象ファイル)
指示文には、次の形を定型にしています。
対象ファイルを編集する前に
/tmpへコピーしてバックアップを取り、md5sumとwc -lを控えてください。編集後に問題が起きた場合の復元は、このバックアップからのコピーのみで行ってください。git checkout・git restore・git stashは使用しないでください。 復元後はmd5sumとwc -lの一致を確認し、バックアップを取った時刻・復元を行ったかどうかを報告に含めてください。
md5sum だけでなく wc -l(行数)も控えさせているのには理由があります。md5の不一致だけでは、切り詰められたのか、単に差分があるのかを区別できないからです。
そして、復元を行ったかどうかを必ず報告させます。 復元が成功していれば実害は無いのですが、報告が無ければ、事故が起きたこと自体に人が気づけません。これから書く2つの事故は、いずれもTAKTの自己申告によって発覚しています。
2. なぜ gitからの復元に頼らせないか
一見、git checkout -- <file> で戻せば済むように思えます。しかし、これを禁止しているのには明確な理由があります。
git checkout -- <file> はHEADの内容へ戻します。 未コミットの成果がそのファイルに同居していれば、今回の事故だけでなく、そのラウンドで積み上げた成果ごと巻き戻ってしまいます。 実際に、あるtest-stepの作業でこれが起きたことがあります。
もう一つ、git show HEAD:<file> からの復元は、直前にコミットがある場合しか成立しません。 前のラウンドを未コミットのまま次のラウンドへ進んでいれば、そのラウンドの成果は復元できません。
この2つの弱点が、実際に同時に牙をむいたことがあります。設計文書への反映作業の第4ラウンドで、版数更新に使った perl -i -pe の中に last という命令がありました。これは -p の暗黙ループそのものを終了させる命令であり、5,879行あった設計文書が、5行に切り詰められました。
TAKTは git show HEAD:docs/detail-design.md を一時ファイルへ出力し、md5がHEADと一致することを確認したうえで、cp で復元しました。git checkout・restore・stash はいずれも使っていません。この対応そのものは正しく、実害は生じませんでした。
しかし、復元が成立したのは、直前の第3ラウンドをコミット済みだったからにすぎません。 もし第4ラウンドが、未コミットのまま第3ラウンドの続きとして進んでいたら、git show HEAD: はコミット済みの版にしか戻せないため、第3ラウンドの成果ごと失っていたことになります。
gitからの復元は、直前の状態を正確に再現できる保証がありません。 バックアップは、必ず作業の外側(/tmp 配下のファイルコピー)に置きます。
3. 「先に確かめ、確かめてから書く」を型にする
事後の復元だけでなく、事前の予防も型にしています。ファイルを一括で書き換えるときは、次の4段階を踏み、いきなり書き込ませません。
- バックアップを取る
- 変更を加えない試行:対象の全件が同じ形かどうかを、書き込む前に確認する。同じ形でない行があれば個別に確認する
- 検証が成立した場合のみ書き込む:変更行数・行数の不変・文字数の増減・破損検査(連載第4回の完了確認項目)をすべて満たしたときだけ書き込む。1つでも満たさなければ、無変更のまま止まる
- 独立した確認:md5の変化・行数・破損検査・
git diff --statが、想定どおりであること
ある文書で強調記号の対応が19行にわたって崩れていたときは、この型で是正しました。19行すべてが同じ形であることを、置換の前にまず確かめ、4項目の検証がすべて揃ったときだけ書き込みました。このとき git diff --stat に想定外の変更が現れなかったことが、意図しない箇所に手が入っていないことの、独立した証拠になっています。
4. あわせて守っていること
事故を未然に防ぐための運用も、2つ定型化しています。
- 事故の起きやすい対象へ進む前にコミットする。 ラウンドごとにコミットを挟みます。前節のとおり、gitからの復元が成立するのは直前のコミットがある場合に限られるためです
- 最初のラウンドは最小の対象に置く。 指示文が実際に効くかどうかを、最小の被害範囲で確かめます。実際、196行の小さな文書から始め、5,879行の文書は最後に回しています
リスクの低い対象で型を検証してから、リスクの高い対象へ進みます。 順序そのものが安全装置になっています。
5. git worktree を、比較のために使う
最後に、gitを禁止しているわけではないという話を書きます。復元の手段としては使いませんが、比較の手段としては積極的に使っています。
「このテストの失敗は、自分の変更が原因か、それとも変更前から失敗していたのか」を切り分けたいとき、git worktree を使うと現在の作業ツリーに一切触れずに実測できます。
git worktree add /tmp/<名前> <比較したいコミット>
ln -s <本体の絶対パス>/frontend/node_modules /tmp/<名前>/frontend/node_modules
npm --prefix /tmp/<名前>/frontend test 2>&1 | tail -6
git stash・git checkout・git restoreを使わずに済みます。未コミットの成果は、現在の作業ツリーにそのまま残りますnode_modulesはシンボリックリンクで共有します。これにより、依存関係の再インストールが不要になります。シンボリックリンクはあくまで参照であり実体ではないため、撤去してもリンク先が消えることはありません
撤去の手順には注意が要ります。 シンボリックリンクを先に外さずに git worktree remove を実行すると、node_modules が未追跡ファイルとして検出され、削除がエラーで拒否されます。ここで --force は使いません。中身を見ずに強制削除するコマンドであり、シンボリックリンク以外の何かが紛れ込んでいても気づけないためです。git status --porcelain で未追跡がシンボリックリンク1件のみであることを確認してから、rm(-rf ではない)で外します。
そして、撤去を忘れません。 残したまま作業を終えると、次のセッションの git worktree list に見慣れないものが現れます。以降、セッション冒頭の確認コマンドに git worktree list を含めています。
実際にこの手法で、フロントエンドのテストが41件失敗したときの原因切り分けを行ったことがあります。過去のあるコミットを取り出して実行したところ、38件は変更前から失敗していることが実測できました。 増分の3件だけが自分の変更に起因すると確定でき、残る38件は別の課題として切り出しました。
6. まとめ——復元は「戻す」ためではなく「戻せる」ために設計する
この回で書いたことは、要するにgitの機能を信用していないという話ではありません。 git checkout が「直前のコミットへ戻す」という、それ自体は正しい仕様を持っているからこそ、未コミットの成果を巻き込むという副作用も正確に起きてしまいます。 禁止しているのは、gitではなく、gitの挙動を正確に把握しないまま復元に使うことです。
一方で git worktree は、現在の作業ツリーを一切変更せずに別のコミットを実行できるという性質を、復元ではなく比較のために使っています。 同じgitでも、目的が違えば使い方も変わります。
バックアップを取る理由は、壊れたものを元に戻すためだけではありません。「本当に自分の変更が原因なのか」を、あとから正確に問い直せる状態を残しておくためでもあります。
次回(第6回):AIは誤る前提で工程を組む。あるセッションで、AIは7回誤りました。そのすべてが、ファイルへ書き込まれる前に検知されています——なぜ検知できたのかを、これまでの5回で書いた仕組みを振り返りながらまとめます。
動画作成には自信があります!

