0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

機械的作業とエージェンティック作業のコミットを分けよう

0
Posted at

はじめに

3年ぶりくらいの記事です。
エージェント作業に限った話ではないですが、コミットを見やすくしましょうという話です。

エージェント(Claude Code を想定)を使った開発を中期的に(数か月)続けていると、リファクタの介入をすべきタイミングが訪れます。
開発当初の設計が中盤になると通用しなくなるからです。

逆に中盤を見据えた実装を最初からしてしまうと、それはオーバーエンジニアリングになります。
開発の自然な流れとして、コードの規模の拡大に応じた設計の更新、実装の別ファイルへの切り出し、コンポーネント化は不可避でしょう。

そんなわけで今回は、UI の移動・統合を扱ったリファクタを例に、こうするとエージェントの作業をレビューしやすかったよという実録になります。
今回の結論は以下です。

  • 機械的作業とエージェンティック作業のコミットを分離する
    • 機械的作業: カット&ペーストなど
    • エージェンティック作業: 個人的な造語。AIが判断を伴って編集する作業

見やすいコミットログがもっと世に増えますように。

背景

前提として、私は Qt/QML を使った C++ デスクトップアプリを開発しています。
今回は QML (GUI側の言語)を例に書きます。言語はやや特殊かもしれませんが、作業自体は他の言語でも起こると思っています。

設定ウィンドウについて、タブ構成を再編成するリファクタリングを行いました。
概念的にはブロックを移動するだけの単純な作業です。ちなみにファイルは1000行ほどでした。

SettingsWindow.qml

  Hoge タブ {
      |----------|
      | hoge1    | <- Piyo の中、一番上に移動したい
      |----------|
      |----------|
      | hoge2    |
      |----------|
      |----------|
      | hoge3    |
      |----------|
  }

  Fuga タブ { ... }

  Piyo タブ { <- Hoge と Fuga の間に移動したい
      |----------|
      | piyo1    |
      |----------|
  }

  Other タブ {
      |----------|
      | other1   | <- Piyo の中、piyo1 の上に移動したい
      |----------|
  }

しかしこれを一括でやると、git diff 上では大変なことになりました。

一括でやらせて失敗

最初はたかをくくっていたため、コミット的には1つにまとめていい粒度に感じましたし、エージェントに1作業でやらせました。
概念的には以下のようになっただけのはずです。ですが、git 差分は地獄のようでした。

SettingsWindow.qml

  Hoge タブ {
      |----------|
      | hoge2    |
      |----------|
      |----------|
      | hoge3    |
      |----------|
  }

  Piyo タブ {
      |----------|
      | hoge1    |
      |----------|
      |----------|
      | other1   |
      |----------|
      |----------|
      | piyo1    |
      |----------|
  }

  Fuga タブ { ... }

以下は diff の一部です。
変更していないはずの Fuga タブが変更に現れ、Piyo タブの変更は Fuga タブの上下に分かれ、diff の長さは数百行になりました。

--- a/SettingsWindow.qml
+++ b/SettingsWindow.qml

+            // Piyo タブ
+            ScrollView {
+                ...
+                    GroupBox {
+                        title: "hoge1"
+                        ...
+
-            // Fuga タブ(変更していないのに差分に巻き込まれる)
-            ScrollView {
                 Layout.fillWidth: true
                 Layout.fillHeight: true

                     GroupBox {
-                        title: "fuga1"         ← 変えてないのに差分扱い
+                        title: "other1"
                         Layout.fillWidth: true
                     }
-                }
-            }
-
-            // Piyoタブ
-            ScrollView {
-                ...
-                ColumnLayout {
-                    ...

+                    GroupBox {
+                        title: "fuga1"
                         ...

 (... Fuga の追加と Piyo の削除がストライプ状に混在して数百行続く ...)

重要なのはこれが、自分が IDE 上でコードを移動させたのでなく、エージェントが Edit ツールか何かで編集した ということです。
私には、この数百行の diff の中にたった1つのタイポ(ハルシネーション、ノイズ)が含まれてないことは保証できません。
というか diff が見づらすぎて元のコードから内容的な変更が0であるかどうかも判断付きません。
Fuga が Huga になっているかもしれません。

この変更には責任が持てないと判断し、会話履歴を巻き戻し、別の方針で変更することにしました。

「どのような手段で変更されたのか」をコミット単位で区別する

私はエージェントのコーディングは既存の情報に対し破壊的な変更であると考えています。
言い方を変えると、エージェントが編集する前と編集した後で、コードの情報量が維持される保証はありません。どれだけ rules を設定しても。

エージェントの作業は情報量を変化させます。であれば、その変更は確認が容易であるべきです。
情報量が変化する変更と、変化しない変更でコミットを分けることにしました。

  • コミット1:機械的なカット&ペースト(情報量が変化しない)
  • コミット2:動作のための調整(情報量が変化する)

(もちろん、カット&ペースト、構造の移動が変化を生むこともままあります。言語への理解が前提です)

コミット1:機械的なカット&ペースト

コードの移動だけを行い、内容は一切変更しません。この時点では一旦ビルドが通らなくなります。
今回は、ファイルが 1000 行くらいで長くなってきていたので、git 差分も崩れにくいですし、外部ファイルに切り出しました。

(残念ながら)この作業は手作業でやりました。空ファイル作成とかはエージェントにやってもらいましたが…。

Piyo1.qml

|----------|
| hoge1    |
|----------|

|----------|
| other1   |
|----------|
Piyo2.qml

|----------|
| piyo1    |
|----------|
SettingsWindow.qml

  Hoge タブ {
      |----------|
      | hoge2    |
      |----------|
      |----------|
      | hoge3    |
      |----------|
  }

  Fuga タブ { ... }

  Piyo タブ {
  }

  Other タブ {
  }

コミット2:動作のための調整

移動したコードを実際に動作させるための修正です。import の追加や、切り出したクラスの使用、cmake へのファイル追加。

Gihub Desktop において、「Hide whitespace changes」をオンにして見ると、インデント調整は全部消えて、意味のある変更だけ が残ります。

Piyo1.qml:

--- a/Piyo1.qml
+++ b/Piyo1.qml
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import AppSettings
+
+ColumnLayout {
+    spacing: 8
+
                     GroupBox {
                         title: "入力デバイス"
                         ...
                     }
+}

Piyo2.qml も同様(import 追加 + ColumnLayout ラッパー追加のみ)。

SettingsWindow.qml:

--- a/SettingsWindow.qml
+++ b/SettingsWindow.qml

+            // Piyo タブ
+            ScrollView {
+                ...
+                    Piyo1 {
+                        Layout.fillWidth: true
+                    }
+                    Piyo2 {
+                        Layout.fillWidth: true
+                    }
+                }
+            }

             // Fuga タブ(変更なし)

-            // Piyo タブ
-            ScrollView {
-                ...
-            }
-
-            // Other タブ
-            ScrollView {
-                ...
-            }

diff が小さく、すべてが意味のある変更です。「移動したコードがちゃんと動くか」だけに集中してレビューできます。

おわりに

「機械的作業」と「エージェンティック作業」を分けようね、という話をしました。
より本質的には、情報量を変える作業と変えない作業は履歴上で分離しようね、という話です。

別にこれは今に始まった話ではなく、アトミックコミットのような考え方として昔からあります。
また今回はファイルを切り分けましたが、そもそも最初の図の <- xxx に移動したい という矢印ごとにコミットを分ければ、おそらくは git 差分もそこまで酷いことにはならなかったでしょう。
私はこういうやり方で今回上手くいったよ、という報告であり、ベストプラクティスとも思いません。

ですが少なくとも、コミットを見やすくすることは、人間にもエージェントにも優しいはずです。
git 差分を小さくすることで、未来のエージェントが過去の diff を参照したときのコンテキストへの悪影響を抑えられます。
レビュワーには「今回はリファクタで、全体差分でなくコミットごとの変更を見たほうがレビューしやすいです」と伝えられます。

Claude Code はとても楽しいので、また何か書きます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?