エージェントがボタンを4つ書いた。どうやって見つけるか
PrimaryButton、SecondaryButton、DangerButton、GhostButton。4つ持とうと決めた人はいません。どれも書かれた日には妥当でした。
破壊的な操作を頼めば赤いボタンが返ってきます。別の日に控えめなものを頼めばグレーのボタンが返ってきます。どちらの依頼も間違っていません。6週間後、ボタンのコンポーネントが4つあり、どれかに触れた瞬間に互いにずれ始め、テストも共有できません。
これはAI駆動のコードベースで最も溜まりやすいもので、ハードコードされた色よりずっと気づきにくい。生の hex は1行で見えますが、ほぼ同じコンポーネント4つは、4つ同時に開いたときにしか見えません。
素直なやり方は効かない
最初に思いつくのは — 私たちもそうでしたが — 名前で照合することです。PrimaryButton、SecondaryButton、DangerButton は末尾を共有し、修飾語で違う。修飾語を落として残りでまとめればいい。
やってみました。外れて、そのあとも新しい形で外れ続けました。1つのコードベースで6回です:
- 接頭辞照合が
ButtonとButtonGroupをまとめてしまいました。後者は入れ物であって、variant ではありません。 - 修飾語判定が
NewHarnessFormを捕まえてしまいました。Newは修飾語ではなく、役割だったためです。 - ファイル名の規則が
components/loading.tsxを落としてしまいました。フレームワークの予約ファイルと取り違えたためです。 - import の共起 —「一緒に使われるものは関係がある」— は実データで成り立たず、破棄しました。
- 「値を持つから primitive トークンだ」という判定は、3層のスケールに当たった時点で壊れました。
WaitlistSectionは名前だけ見れば*Section族に見えます。ですが構造は1つも共有していません。
1つのリポジトリで6回の反例は、運が悪いのではありません。その方法が効かないと言われているのです。
理由は後から見れば単純です。名前は意図の主張であり、ほぼ同じものが並んだコードベースでは、その意図こそが壊れています。壊れている信号を使って、壊れを検出することはできません。
代わりに構造を見る
見た目しか違わない2つが本当に共有しているのは、描いているものの形です。信号は3つ、どれも実行せずに読めます:
- 根の要素。
<button>と<div>は、何と呼ばれていようと互いの variant ではありません。 - 描いている要素の多重集合。
div > spanとdiv > span > spanは別のコンポーネントです。 - スタイル指定の語彙 — ただし値は捨てる。ここが直感に反していて、そして要点です。
値を捨てることが効き目の源です。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 を選ぶ理由は整頓ではありません。PrimaryButton と DangerButton は spec もテストもトークンも共有できないので、どちらかに触れた瞬間にずれていきます。variant を2つ持つ1つのコンポーネントは、ずれようがありません。
なぜ人手では捕まらないか
プルリクエストを読んでいてボタン4つに気づくことはありません。どれも単独で、それを足すのが妥当な差分の中で到着しました。重複は集合の性質であり、その集合を誰も見ないからです。
その部分を Harnd が自動化します。既にあるコードからコンポーネントを読み取り、構造で似たものをまとめ、何が共通なのかを添えて出すので、判断はあなたができます。わざと散らかした検証用アプリでは、12コンポーネント中に2組(ボタン4つとカード3つ)を見つけます。早期アクセスを受付中です。
自分で作るなら、いちばん高くついた教訓を持ち帰ってください: コンポーネントが何と呼ばれているかではなく、何を描いているかを見てください。