← 記事一覧

エージェントがボタンを4つ書いた。どうやって見つけるか

PrimaryButton、SecondaryButton、DangerButton、GhostButton。4つ持とうと決めた人はいません。どれも書かれた日には妥当でした。

約8分

破壊的な操作を頼めば赤いボタンが返ってきます。別の日に控えめなものを頼めばグレーのボタンが返ってきます。どちらの依頼も間違っていません。6週間後、ボタンのコンポーネントが4つあり、どれかに触れた瞬間に互いにずれ始め、テストも共有できません。

これはAI駆動のコードベースで最も溜まりやすいもので、ハードコードされた色よりずっと気づきにくい。生の hex は1行で見えますが、ほぼ同じコンポーネント4つは、4つ同時に開いたときにしか見えません。

素直なやり方は効かない

最初に思いつくのは — 私たちもそうでしたが — 名前で照合することです。PrimaryButtonSecondaryButtonDangerButton は末尾を共有し、修飾語で違う。修飾語を落として残りでまとめればいい。

やってみました。外れて、そのあとも新しい形で外れ続けました。1つのコードベースで6回です:

  1. 接頭辞照合が ButtonButtonGroup をまとめてしまいました。後者は入れ物であって、variant ではありません。
  2. 修飾語判定が NewHarnessForm を捕まえてしまいました。New は修飾語ではなく、役割だったためです。
  3. ファイル名の規則が components/loading.tsx を落としてしまいました。フレームワークの予約ファイルと取り違えたためです。
  4. import の共起 —「一緒に使われるものは関係がある」— は実データで成り立たず、破棄しました。
  5. 「値を持つから primitive トークンだ」という判定は、3層のスケールに当たった時点で壊れました。
  6. WaitlistSection は名前だけ見れば *Section 族に見えます。ですが構造は1つも共有していません。
1つのリポジトリで6回の反例は、運が悪いのではありません。その方法が効かないと言われているのです。

理由は後から見れば単純です。名前は意図の主張であり、ほぼ同じものが並んだコードベースでは、その意図こそが壊れています。壊れている信号を使って、壊れを検出することはできません。

代わりに構造を見る

見た目しか違わない2つが本当に共有しているのは、描いているものの形です。信号は3つ、どれも実行せずに読めます:

  1. 根の要素。<button><div> は、何と呼ばれていようと互いの variant ではありません。
  2. 描いている要素の多重集合。div > spandiv > span > span は別のコンポーネントです。
  3. スタイル指定の語彙 — ただし値は捨てる。ここが直感に反していて、そして要点です。

値を捨てることが効き目の源です。bg-[#3b82f6]bg-[#ef4444]同じ として扱わなければなりません — 探しているものが「見た目しか違わない」なのですから。色を違いとして数えたら、永久に見つかりません。

bg-[#3b82f6]  →  bg
px-[13px]     →  px
hover:bg-red-500 → hover:bg
p-4           →  p

修飾子(hover:)は残します — hover 状態を持つかどうかは本物の構造の違いです。最初の区切り以降は落とします。

そのうえで、根が同じ・要素構成が同じ・語彙の重なりが閾値以上、でまとめます。私たちは8割にしています。

保守的に倒し、そして測る

誤検知する検出器は1週間で切られ、その後は検出器が無くなります。だから基準は「全部見つける」ではなく、「誰かの午後を潰す形で間違えない」です。

構造版を自分たちのコードベース(生きているコンポーネント71件)に当てたら、3組出ました。うち2組が誤りで、その理由が示唆的でした。誤検知はすべて、同じファイルに同居しているコンポーネント同士でした。

当然です。指紋をファイル単位で作っていたので、同居していれば指紋は必ず一致します — 似ているからではなく、文字通り同じ文字列だからです。同一ファイルを除いた結果は 1組・誤検知ゼロ になりました。

残った1組は本物でした。別のディレクトリにある2つのバッジで、色と文言しか違いません。片方は自分の説明文に「もう一方を見よ」と書いていました。

よく知らないコードベースで信用する前に、よく知っているコードベースで走らせてください。真陽性より偽陽性のほうが多くを教えてくれます。

組が見つかったらどうするか

すべての組を統合すべきではありません。有用な問いは「同じか?」ではなく:

  • 役割が同じ・構造が同じ・見た目だけ違う → variant プロパティを持つ1つのコンポーネントにまとめます。
  • 役割が違う、または構造が違う → 分けたままにして、理由を書きます。次の読み手が同じ問いを開き直さないように。

実務的な判定: プロパティ1つで違いを表せるなら variant です。3つ必要なら、それは似た服を着た別のコンポーネントでしょう。

variant を選ぶ理由は整頓ではありません。PrimaryButtonDangerButton は spec もテストもトークンも共有できないので、どちらかに触れた瞬間にずれていきます。variant を2つ持つ1つのコンポーネントは、ずれようがありません。

なぜ人手では捕まらないか

プルリクエストを読んでいてボタン4つに気づくことはありません。どれも単独で、それを足すのが妥当な差分の中で到着しました。重複は集合の性質であり、その集合を誰も見ないからです。

その部分を Harnd が自動化します。既にあるコードからコンポーネントを読み取り、構造で似たものをまとめ、何が共通なのかを添えて出すので、判断はあなたができます。わざと散らかした検証用アプリでは、12コンポーネント中に2組(ボタン4つとカード3つ)を見つけます。早期アクセスを受付中です

自分で作るなら、いちばん高くついた教訓を持ち帰ってください: コンポーネントが何と呼ばれているかではなく、何を描いているかを見てください。

エージェントがUIを書いても、デザインの一貫性を保つ。