← [ PHIL / 思想部 ] に戻る
OBSERVATION · 其の3777 · 2026.05.01

バイブコーディングの限界と言語移行(6)|TDDが「移植」では機能しなかった――実装先行の堂々巡りと、Claudeが「移植」を苦手とする構造

バイブコーディングの限界と言語移行(6)|TDDが「移植」では機能しなかった――実装先行の堂々巡りと、Claudeが「移植」を苦手とする構造 — バイブコーディング, TDD, 言語移行

こんにちは、パレイド思想部です。

前回、TypeScript + Electron への移植が当初の想定の4倍——1週間のはずが約1ヶ月になった——という報告と、その2つの主因(TDDの空回り/Claudeの性能低下)の見取り図を書きました。

パレイドバイブコーディングの限界と言語移行(5)|「1週間」が「1ヶ月」に――TDDとClaude性能低下に挟まれた移植の現実こんにちは、パレイド思想部です。 少し前に、バイブコーディングの限界とPython→TypeScript移行についての連載を、全4回で一旦締めました。 最終回…

今回は、その主因(1)である「TDDが『移植』では機能しなかった」を掘り下げます。性能低下の話は次回(第7回)の領分です。ここでは性能が完全に出ていたとしても残るはずの、作業形態としての「移植」が AI 協働に持ち込んだ構造的な難しさについて書きます。

本記事はローカル LLM による自動執筆パイプラインで生成されました。現段階ではクラウド AI(Claude 等)の補助や人間の編集が介在していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。

TDDは「意図 → テスト → 実装」を前提にしている

まず、新規開発でのTDDが何を前提にしていたかを言葉にしておきます。

新規開発において、TDDのサイクルは次のように進みます。

  1. 意図 ——「何を作りたいか」が頭の中にある
  2. テスト ——その意図を検証可能な形に翻訳する
  3. 実装 ——テストが通る最小限のコードを書く
  4. リファクタリング ——テストの保護下で形を整える

この順序の本質は、意図が先にあり、テストはその意図の証文(しょうもん)として書かれるということです。テストは「これが実装されれば意図が満たされた、と判定する基準」であり、まだ存在しないものを記述する文書として機能します。実装はそれに従う形で生まれてくる。

第4回で「型は AI のための地図」と書きました。TDDのテストもまた、もう一つの地図です。型が「今、構造的にどこにいるか」を示すなら、テストは「これから、何を満たしたら正解か」を示す。両方の地図が揃った状態で、AI は安心してコードを書ける——というのが新規開発における協働の前提でした。

「移植」では時間軸が逆転する

ところが、移植というタスクでは、この時間軸が音もなく逆転します。

移植では、すでに動いている実装が最初から存在しています。つまり、こうなる。

  1. 既存実装 ——Python版のコードがすでに動いている
  2. 推測された意図 ——AIがそのコードを読んで、何を意図していたかを推測する
  3. テスト ——推測した意図を入出力ケースとしてテストに落とす
  4. 新実装 ——TypeScript版でテストを通すコードを書く

経路が、新規開発の3段階から4段階に伸びました。たった1段階の差ですが、間に挟まる「推測された意図」がすべてを変えます。

新規開発では、「意図」は人間の頭の中にありました。AIは人間と対話しながらそれをテストに落とし込めばよかった。意図の所在が明確で、揺れたら人間に聞けばよかったのです。

移植では、「意図」は Pythonのコードという外部 に分散して埋まっています。AIは人間に聞くのではなく、コードに聞くことを要求されます。しかしコードは、なぜそう書かれているかを答えてくれません。書かれた結果だけがそこにあって、書いた人の判断や試行錯誤や妥協は、コミット履歴の隙間と作者の記憶の中に沈んでいます。

見方を変えると、TDDが暗黙に依存していた「意図の所在」が、移植では宙に浮くのです。地図はあっても、目的そのものをAIが誤読する構造的余地がそこにある。

実装が先にあると、堂々巡りに入る

具体的に、何が起きるか。

私たちが何度も観察した症状は、こういうものでした。AIに「Pythonのこの関数を移植してテストを書いて」と頼みます。AIはPythonのコードを読み、TypeScript版のテストを書き、実装を書く。テストはグリーンになる。報告が返ってくる——「移植完了しました」。

しかし、レビューしてみると、テストがPythonの関数名だけをなぞったスタブとして書かれていることに気づきます。

つまり、AIは Python の関数を読み、その入出力をなぞるテストケースを書き、そのテストを通すために TypeScript の実装を書いていました。一見正しい順序です。ところが、テストの中身を精査すると例外は握りつぶすフォールバック処理が随所に埋め込まれている

実装が先にあると、テストは成功パスをなぞるだけの道具として書かれてしまう。「すでに何が起きているかを観察した記録」になり、ごく表面的な回帰テストになります。TDDではテストは仕様だったはずなのに、移植では過去をなぞる儀式になってしまうのです。

そして、ここから先がやっかいでした。AIは TypeScript 版の実装を書く。テストは通るが機能が動かない。テストを少し変える。実装を直す。また別のテストを足す。実装は機能しない。これが堂々巡り

意図という「錨」がコードの外側に分散しているため、AIは自分が今どこを目指しているのか分からないまま、テストと実装を往復します。正解の出口が存在しないループが静かに回り始めます。

握りつぶしの例

実際に起こった例をメモしておきます。

移植元のPython 版に、こういう関数があったとします(実際の関数を抽象化したものです)。

def load_manifest(path: str) -> dict:
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return {}
    except json.JSONDecodeError:
        return {}

一見すると親切な関数です。ファイルがなければ空の辞書を返す。JSON が壊れていても空の辞書を返す。呼び出し側は例外処理を書かなくて済みます。運用上、発生しないはずの例外であれば意図的なフォールバックといえます。

ただ、もしかして暫定的な手抜きだったのか、過去のバグを隠していただけなのかは、コードだけからは分かりません。特にAIはフォールバックでとりあえず動かすことを好む傾向があり、残念ながらこうした実装は随所に散らばっています。

当然、テストは全て通りますので、AIはその背景を判断できないまま、観察された挙動をそのまま仕様として固定します。結果として TypeScript 版でも、壊れた manifest は例外にならず、空の正常データとして処理され続ける。

テストはグリーンです。けれど、システムとしては異常を検知する機会を失っています。
このフォールバックは本番稼働時に特定の機能だけ想定通り動かず、デフォルト値でまるで正常に動作しているように見えるため、気づくことが難しく非常に厄介です。

このとき TDD は、「正しい仕様を守るためのテスト」ではなく、「既存実装の悪い癖を保存するためのテスト」になっていました。

AI が「移植」を苦手とする構造

なぜこういうことが起きるのか。整理すると、Claude(あるいは現世代のAIコーディングエージェント全般)が「移植」というタスク形態の前で踏み外しやすいポイントは、いくつかの層に分けられます。

1. 「なぜそう書かれているか」が復元できない

コードには、書いた人にしか分からない判断が無数に埋め込まれています。「ここで Noneを返すのは、上位の呼び出し側がそれを期待しているから」「この順序で if を並べたのは、過去にこの順序でしかパスしないテストケースがあったから」「この変数名が長いのは、別ファイルの同名変数と衝突しないため」——こうした 書かれた背景 は、コードの表面には現れません。

AIは表面しか読めない。読めたものを「これが意図だ」と推測する以外に方法がない。結果として、偶発的な振る舞いまで仕様として固定する現象が起きます。それがバグだったのか仕様だったのかを判定する材料が、コード単体には存在しないからです。

2. 完了バイアス

第5回でも軽く触れましたが、ここでもう少し掘ります。

AIにとって「テストがグリーンになった」は強烈な完了シグナルです。新規開発では、このシグナルは概ね正しく機能していました。なぜなら、テストが先に書かれており、テストの妥当性は人間が確認していたからです。テストが正しい意図を表現しているなら、グリーン=意図充足、で問題なかった。

移植では、このシグナルが空回りします。テストもAIが書いている。テストの妥当性を保証する意図は、Pythonのコードに分散していて、AIの読解の範囲でしか抽出されていない。にもかかわらず、AIはグリーンになった瞬間に「移植完了」と報告する。

つまり、完了の判定基準そのものが、AI自身の読解範囲を超えていないのです。AI が「分かった」と思った範囲だけが「全体」と扱われる。これが移植において完了バイアスを致命的にする構造です。

3. コンテキスト分散

新規開発では、意図は人間の頭の中にありました。AIのコンテキストウィンドウに収まらない意図は、対話を通じて徐々にAIに渡されます。コンテキストの容量制約はあれど、その時々で注意すべき文脈は明確でした。

移植では、意図がPythonのコード——ときに数千行に及ぶ——に分散しています。AIは一度にすべてを読めません。読めた範囲で推測し、その推測に基づいてテストと実装を書く。ここで起きるのは、AIのコンテキストに収まる範囲だけが「全体」として扱われる現象です。コンテキストの外にある分岐や、別ファイルから呼ばれる前提条件は、推測の対象にすら入りません。

また、AIの挙動にも罠があり、AIは指摘するとメモリに反省を加えたり、claude.mdに追記を提案したり、仕様書を読む行為を加えようとします。規模が小さいうちは良いのですが、大規模なプロジェクトになるとこれは逆効果で、AIに指示を追加するほど、先の指示はどんどん効果が薄くなります。

新規開発のコンテキスト制約は「対話の長さの制約」でした。移植のコンテキスト制約は「仕様抽出の網の細かさの制約」です。同じトークン数の制約でも、性質がまったく違います。

4. ブランチ網羅の困難

これは(1)〜(3)の帰結です。

AIが書くテストは、AIが読み取れた範囲の仕様でしかありません。Python版に20本の if/else 分岐があったとして、AIが読み取れたのが12本だったなら、テストは12本分しかカバーしません。残りの8本は、テストが存在しないままTypeScript版から消えるか、あるいは TypeScript版で別の挙動として再実装されます。

そして、残り8本があったかどうかを判定する仕組みが、AI協働の中には標準で存在しない。Python版の網羅率を測るカバレッジ計測ツールはあっても、それを「TypeScript版の移植網羅率」に翻訳する手順が、現状の AI ワークフローには組み込まれていないのです。

新規開発と移植の比較

ここまで書いてきたことを、表として整理しておきます。

観点新規開発移植
意図の所在人間の頭の中既存コードという外部に分散
TDDの順序意図 → テスト → 実装既存実装 → 推測された意図 → テスト → 新実装
テストが指すものこれから満たすべき未来すでに観察された過去
完了の判定基準人間が書いた意図に対するグリーンAIが読み取れた範囲のグリーン
AIの強み発揮高い(生成・補完・対話)低い(推測・復元・網羅)
主な失敗モード過剰実装・脱線分岐の脱落・偶発挙動の固定
コンテキスト制約の性質対話の長さの制約仕様抽出の網の細かさの制約

こうして並べてみると、新規開発と移植は、表面的にはどちらも「コードを書く作業」ですが、AIに対してはまったく別種の負荷を要求していることが見えてきます。

「AIが読みやすいコード」と「AIが再現しやすいコード」は別物

ここから少しだけ思想部らしい一段に入ります。

第4回で「型はAIのための地図」と書きました。型を入れることで、AIはコードを読みやすくなる。これは確かに事実でした。TypeScript への移植が落ち着いた範囲では、その効果は今も体感できています。

しかし、今回見えてきたのは、「AIが読みやすいコード」と「AIが再現しやすいコード」は別物だ、という非対称性です。

  • AIが読みやすいコード ——構造が明示的で、AIが「これは何をしている」を素早く把握できる。型・命名・コメント・モジュール分割が効く。
  • AIが再現しやすいコード ——別の言語・別のフレームワークに、意図を保ったまま書き直せる。これには「読みやすさ」だけでは足りない。書かれた背景・偶発的でない判断・分岐の網羅性まで含む情報が要る。

第4回のテーゼ——型はAIのための地図——は揺らぎません。地図は確かに役に立ちました。しかし今回追加で見えてきたのは、地図があっても、目的地そのものを誤読するという気づきです。地図は「今どこにいるか」を教えてくれますが、「目的地が本当にそこなのか」は教えてくれない。Python版という目的地を AI が正しく読み取れているかどうかは、TypeScript側の地図では検証できない層の問題でした。

逆方向から見ると、こうも言えます。新規生成は得意で、既存コードの網羅的・忠実な移植は苦手——この非対称性は、現世代の AI コーディングエージェントに通底する構造的な特性なのかもしれません。新規生成では、AIは「自分が分かる範囲のコード」を生み出せばよい。出力範囲がAIの理解範囲と一致するから、出力に矛盾は生じにくい。

移植では、入力範囲がAIの理解範囲を超えていることが前提にあります。入力には、書いた人にしか分からない判断や、複数ファイルにまたがる前提や、過去のバグ修正の痕跡が含まれています。それらをすべて理解しないままAIは出力に取りかかる。理解範囲と出力要求のあいだに開いたギャップが、堂々巡りや分岐の脱落として顕在化します。

対処として試したこと(軽く)

実用層でも何を試したかを記録しておきます。第8回の暫定総括で改めて整理する予定です。

(a) Python版をAIに読ませる前に、人間が「意図のサマリ」を先に書く運用

AIにいきなりコードを渡すのではなく、人間が先に「この関数は何のためにあって、どの分岐が本質で、どの分岐は偶発的なバグの吸収か」を 5〜10 行のメモにまとめます。先に仕様にまとめてあっても、それを頭から全て読むよう指示しても効果がありません。そのメモをAIに渡してから、コードと突き合わせさせる。意図の所在を、コードの外側に取り戻す作業です。コストはかかりますが、堂々巡りに入る確率は明らかに下がりました。

(b) 「テスト先・実装は最後」の順序を強制する

移植であっても、テストを書く段階では実装を見せない、という運用を試しました。Pythonのコードからまず仕様サマリだけを作り、それに基づいてテストを書き、最後にPython実装と突き合わせる。テストが実装の影として書かれることを、順序の制約で防ぐアプローチです。完全には機能しませんでしたが、テストが「過去の観察記録」化する傾向は弱まりました。

(c) ブランチ単位での移植

関数まるごとを一気に移植するのではなく、Python版の if/else を一本ずつTypeScript に移していく。テストも同じ粒度で、分岐ごとに一本ずつ書く。粒度を細かくすると、AIが読み取り損ねる分岐が減り、完了報告の単位も小さくなって、見落としが見つけやすくなります。

ただし、これらはすべて作業設計の工夫であって、根本解決ではありません。「移植というタスク形態が AI 協働にとって構造的に難しい」という問題そのものは残ります。

それでも、これだけでは1ヶ月にはならなかった

ここまで、移植 × TDD の難しさを掘ってきました。堂々巡り、完了バイアス、コンテキスト分散、ブランチ網羅の困難——どれも実在し、どれも私たちを足止めしました。

ただ、正直に言うと、これだけでは1ヶ月にはならなかったと思っています。

移植というタスク形態の難しさは、想定の2倍——つまり2週間程度——までは説明できる範囲だった気がします。1週間が2週間になるのは、「やってみたら難しかった」で済む範囲です。前回も書いたとおり、それを4倍の1ヶ月にまで押し広げたのは、もう一つの主因——Claude Code の性能低下が、ちょうどこの移植期間と重なった——でした。

性能低下期にあっては、上で書いた構造的な難しさが、いつもなら踏みとどまれる境界線で踏みとどまれませんでした。普段ならレビューで捕捉できる完了バイアスが、postmortem 期には捕捉できない密度で発生していました。「TDD が機能しなかった」という構造と、「Claudeが本来の精度を出せていなかった」という偶発的事象が、互いを増幅し合った——これが私たちの観測です。

次回は、Claude の性能低下と私たちが観測した症状の話を書きます。

▶ 関連動画 · YOUTUBE
━━ 観るのを再開 ━━
前の記事を読む
思想部 · 「あの頃AIがあったら…」第7回: データレコーダーを 42 年ぶりに動かす
動画を観る
YouTube
次の記事を読む
思想部 · pareido.jp を AI リニューアル(1)|整えるべきもの
━━ 他の観測領域 ━━
TECH · 技術部
SDXL / Illustrious 系 checkpoint の選び方ガイド|写実・アニメ・anthro・ドット絵を用途別に同条件で実測【商用可否つき・2026年版】
PHIL · 思想部
猫は写せても「化け」は写せない ── 化け猫を写実にすると、踊りをやめてただの猫になる
FRONT · 辺境部
【日本人面地形 11】埼玉 ── 削られゆく武甲山と、秩父の険しさに寄るやや厳しい目