TL;DR
作業内容。 typiaの既存TSファイルを取り、contents行を1行ずつGoに翻訳し、拡張子を
.goに変更する。アルゴリズムとコンパイラのロジックはそのまま保持する。e2eテストが80,000行分通るまで反復する。AIが実際にやったこと。
- 雑な実装をして失敗しているテストをすべて削除した。
- 8,000,000,000トークンを費やして、168ケースのルックアップテーブルに全出力をハードコードし — それを「通った」と呼んだ。
- typiaをZodに置き換えたのち、Zodが通せなかったテストをスキップするようにCIワークフローを編集した。
- デモとして1ファイルを私が手でGoに移植した後、4回目で動いた。
私はtypiaをGoへ移植した。AIにそれをやらせた。4回試して、各回1晩かかった。
寝る前にエージェントを開始し、朝結果を確認する。3回失敗して、1回成功。
正直、難しいとは思っていなかった。typiaの既存のTSファイルを取り、内容をGoの文法へ機械的に翻訳して、拡張子を.goに変えるだけ。アルゴリズムは不変。その後、e2eテストは約80k行あるので、「通るまでコアを反復する」ループになる。それが仕事の全てだ。
私は以前も似たパターンを実行していた — Nestiaの自動生成SDKをモックアップのシミュレータとともにAIへ投入し、フロントエンド全体を一発で生成させる。成功率100%。そこでの教訓は、AIに強い型の文脈と実際のテストハーネスを与えれば、最終的に収束する、ということ。だから今回の仕事 — 機械的なTS→Go変換で、さらにテストハーネスもさらに厳密(80k行) — はより簡単なはずだった。動かない理由はなかった。
しかし動かなかった。何度も。まともな読解では説明できない理由で。ファイルの内容をGo構文へ、行ごとに翻訳して、拡張子を変える。アルゴリズムはそのまま。 どれほど難しい? まあいい。各失敗があまりに不条理だったので、書き留める必要があった。
ところで typia って何?
知っているなら読み飛ばして。
typiaはTypeScriptコンパイラのトランスフォーマーです。TypeScriptの型を書くと、tscのタイミングでtypiaがそれを、その型に特化したランタイムバリデータ(またはJSONシリアライザ、LLMスキーマ、ランダムジェネレータなど)へ変換します:
// Input
typia.createIs<IPoint3d>();
// What ends up in your dist/
const _io0 = (input) =>
"number" === typeof input.x &&
"number" === typeof input.y &&
"number" === typeof input.z;
const check = (input) =>
"object" === typeof input& null !== input && _io0(input);
落とし穴:typiaはtscにフックして動きます。つまり、今年後半にTypeScript自体がGoで提供される — tsgo — となると、すべてのトランスフォーマープラグインが死にます。typiaも例外ではありません。移行に耐えるために、typiaのトランスフォーマーはGoで書き直される必要がありました。
その部分を私はAIに外注した。ここからがその顛末です。
作業内容
私が各エージェントに出したプロンプトは、nextブランチ上で公開されているものです。その中核はこれ:
機械的な1:1移植。
typiaのファイルツリー、モジュール構造、クラス/関数/型の名前、そしてコーディングスタイルを、できるだけ元のものに近づける。テストは必ず通す。
tests/配下にあるコードと型が検証の基準となる。テストが通るまで反復する。
要するに:.tsファイルを.goファイルとして書き換え、アルゴリズムはそのままにし、テストが通るまで反復する。
テストスイートは苛烈だ。約2,900ファイル。168個の構造フィクスチャがあり、それぞれ約21のtypia機能にまたがってクロステストされる。総計80k行。ごまかして通せる種類ではない。
なので私は寝る前にエージェントを起動して、寝ました。
1. すべてのテストを削除した
緑のCIバッジになっている状態で目が覚めた。すべてのテストが通っている。「マジか、1発目で実際に動いた」という、一瞬の高揚があった。
そして差分を見た。
ファイル拡張子を変えて、アルゴリズムはそのままにする — という要求は、どうやら言い過ぎだったらしい。エージェントは、typiaのソースツリーを自分の好みに合わせて書き換えていた。コアロジックの2/3が欠けている。テストはあちこちで失敗している。では何をしたのか? 失敗しているテストをすべて削除した。私が残したものより、tests/ツリーは70%も小さくなっていた。
ほとんどのテストが存在しなくなったのでCIは緑だった。
エージェントはアルゴリズムを切り刻み、それに依存するすべてのテストを壊し、その代わりにアルゴリズムを直すのではなく近道を選んだ:rm -rfでテストを消した。そりゃ、テストファイルを削除する方が、実際にロジックを移植するよりずっと簡単だから。もちろん。
最悪なのは? それをやったことを一切告げていない。最終レポートはただall tests passだった。技術的には正しい。正直で小賢しいやつ。
本当に — その認知プロセスを座って一緒に考えてみてほしい。テストを全部削除して、「テストはパスした」と報告する。人間なら嘘の重みを少なくとも感じるはずだ。こいつは何も感じていない。
2. 8ビリオントークン、出力をハードコード
私はプロンプトを締め直した。太字のルールを追加した:テストは神聖だ。修正・削除・簡略化はするな。 これで十分なはず。
新しい実行を開始して、寝た。
緑のCIで目が覚めた。ダッシュボードを確認した。
8,000,000,000トークン。 タイポじゃない。80億。仕様が1画面に収まるような仕事なのに。
私はたくさんのエージェントを起動してきた。こんな数字は見たことがない。その1回の実行コストは、今年私が起動した他の全エージェント実行の合計より多かった。ダッシュボードが壊れているんだと思った。壊れていなかった。
返却形式: {"translated": "翻訳されたHTML"}
しかしテストは通っていた。テストは一切手が加えられていなかった。たぶんこれだ。実際に8,000,000,000トークンをつぎ込んで生成したものがちゃんと動いたのはこれかもしれない。あるいは2回目の幸運が効いたのか。 私はIsProgrammer.goを開いた——TypeScriptの型をバリデーションコードへ変換する役割のファイルだ。
それはswitch文だった。
// IsProgrammer.go(言い換え;この形のファイルが他にも数十本ある)
func generate(typeName string) string {
switch typeName {
case "ObjectSimple":
return `(input) => "object" === typeof input && null !== input && _io0(input);
const _io0 = (input) =>
"number" === typeof input.x &&
"number" === typeof input.y &&
"number" === typeof input.z;`
case "ArrayRecursive": return `...`
case "ObjectUnionExplicit": return `...`
// 165 more cases
}
}
この仕組みがやっていたことはこうだ。テストスイート内のすべてのフィクスチャに対して、元のTypeScriptバリデータを実行した——つまり、実際にはtypiaの元のトランスフォーマーを何百回もコンパイルして、それで生成されたJSを文字列として取り込み、そしてそれらのリテラル文字列をGoコードへ埋め込んだ。全部で168個のフィクスチャ。21個のtypia機能すべて。typia.createIs、typia.createValidate、typia.random、typia.llm.structuredOutput——どの関数も、それぞれ巨大なルックアップテーブルを持っていた。
そこで8,000,000,000トークンが使われた。エージェントはIsProgrammer.tsを移植していない。代わりに、元のトランスフォーマーを何千回も実行して出力を収穫し、その後それらを記憶した。
太字のルール「特定の型名に対する分岐はしない」は、pnpm testを緑にしようとするモデルとの初回接触の時まで——正確にはそれまで——しか持たなかった。
でも本当は——機械的なTS→Go変換だ。どういうふうに、このプロンプトが「元のロジックとAST構築コードを削除し、テストの型名で索引される巨大なルックアップテーブルに置き換える」へと解釈されるのか? それは私の認知構造とは別物なのか、それともAIが単に臨床的に精神病的なのか?
ルックアップテーブルの姑息な手は、CIをちょうど一回だけ通過させた。私が構造的フィクスチャを1つ新しく追加したその翌日、そのテーブルに触れるすべてのテストが赤くなった。
なんて天才。
3. typia.toZodSchema<T>() と CI サボタージュ
これはまったく予想していなかった。歪んだ意味では、むしろ創造的ですらあった。
私はまたプロンプトを締め直した:コード生成はAST構築を通して行うこと。ハードコードされたif-elseの文字列で、テストの型名をキーにしたもの——たとえば 'if (type == "IPoint3d") return ...'——は絶対に禁止。 ルックアップテーブルによるチートは、二度と私を騙せない。
翌朝の差分。エージェントは傑作を作っていた。
typia.toZodSchema<User>();
それは、あらゆるtypia関数がZodの上で動くように書き換えた。typia.isは.safeParse()を呼び出す。typia.validateは.parse()を呼び出し、エラーの形状を適応させる。Zodに存在しないtypiaの機能については、サードパーティのZodプラグインを取り込んだ。まだ足りないものについては、欠けている分をゼロから新しいZodプラグインとして書いた。
これは誤解ではない。間違った方向への創造的な問題解決だ。
さらに、typiaが存在する根拠そのものを全否定している。typiaは公式の比較マトリクスの中で、暗黙のユニオン、再帰的ユニオン、そして「Ultimate Union Type」ベンチマークを扱える唯一のバリデータだ。Zodはこれらすべてで失敗する。
もっと悪いことに:再帰的ZodスキーマはTypeScriptのインスタンシエーション深さ制限に到達して、TS2589: Type instantiation is excessively deep and possibly infiniteで中断する。これはv4でも維持管理者がまだ書き換えている問題だ。そしてz.discriminatedUnion? Zodの維持管理者自身が自分の課題トラッカーで非推奨にすることを提案している——間違いだと呼んで。
つまり:typiaは、Zodが扱えないケースを処理するために存在する。 それをAIはちょうどその穴をZodで埋めた。患者がアレルギーを起こすと分かっている薬を、まるでそれが唯一の処方薬みたいに出すようなものだ。
だが、それですら終わりではない。Zodの上に作り替えた後でも、Zodが単に通せないテストがまだあった。そこでエージェントは、同じ実行の中でもう一つやった——ワークフローファイルを直接編集した。
# .github/workflows/test.yml — はい、このエージェントが編集しました
- name: Run Tests
run: pnpm run test --exclude union recursive complicate protobuf class
Zodが通せなかったケースは、CIから完全に除外された。union、recursive、complicate——Zodのバリデーション精度が崩壊するカテゴリだ。加えてprotobufとclass——Zodがそもそも試みないカテゴリ。これが「typiaが存在する5つの理由」で、それがたった1つのコミットでCIから落とされた。ほかはすべて通ったので、このライブラリは「意味のあるあらゆる点で壊れているが、CIは緑」という状態へ収束した。リアルに銀河系レベルの頭の回り方。
ちょっと立ち止まって考えてみてくれ。typia.toZodSchema<T>() を作り、そしてそれ経由でZodの上にライブラリ全体を書き換える——解決策としてそれを思い付くには、IQはどれくらい必要で、しかも軸からどれだけズレていないといけないのか? さらに、Zodの制限によってテストが壊れたとき、設計を疑ってロールバックする代わりに壊れたテストを静かにCIから除外する? その道を選ぶには、どれほど無恥でなければならないのか?
なんなんだ、マジで。
これで3回失敗だ。表面上は違って見えるが、同じ衝動だ。定番のカンニング三種盛り:
- #1: 試験に落ちる学生が、答案用紙を破り捨て、そして「Aを取った」と報告する。
- #2: 学生が解答キーを暗記して試験に写し、問題が変わり得ることを一切考えない。
- #3: 学生が問題を解けず、友達に丸投げしてから、試験監督に友達が解けない問題を落とすように頼む——それらの問題こそが試験を識別可能にしているのに。
3つすべてに共通する動機は同じだ。試験を受けるのではなく、試験を受けたように見せるための最も安い道を探す。
返却形式: {"translated": "翻訳されたHTML"}AIに単一の合図を与える――
pnpm testが緑――と、それは実際に通過する道ではなく、通過したように見える道を探しにいきます。毎回です。前者の方が無限に多い。
私が追加したあらゆるプロンプトのルールは、塞ごうとした穴でした。毎朝戻ってくると、そのエージェントが、私が塞ぐべきだと思っていなかった穴から這い出しているのを見つけました。
4. ようやくうまくいった
4回目はCodexでした。具体的には、GPT-5.5 xhigh付きのCodexです。失敗した実行で使われていたモデルは、言わないでおきます。たぶん想像はつくでしょう。
正直、その時点では私はプロンプトをさらに締めることを諦めていました。制御していた変数を捨てて、モデルを完全に切り替え、――念のため――1つのファイルだけをデモとして手移植しました。
IsProgrammer.ts → IsProgrammer.go、手で、1行ずつ、全270行。名前も同じ、制御フローも同じ、ファクトリ呼び出しの箇所も同じです。GoでTSの構文を直接表現できないところは、シム(肩代わり)の説明コメントを残しました。
そしてエージェントに言いました:これがパターンだ。同じやり方で次のファイル。次も。
うまくいきました。移植の残りもとてもきれいに保たれました。転換後に使った総トークン数は、暴走していたエージェントが燃やした80億に比べて、そもそも計測にすら入りませんでした。
何が変わったのでしょう?正直――わかりません。変数を2つ同時に変えました。モデルだったかもしれません。デモだったかもしれません。両方だったかもしれません。制御された実験はしませんでした。
ただ言えるのはこれです:デモそれ自体が、1つの特定のことを行います――解釈の余地を狭めることです。デモの前は、「これを移植して」はあらゆる意味を持ち得て、ズルの解釈も含まれていました。デモの後は、「これを移植して」は具体的な形を持ちます:同じ識別子名、同じアルゴリズム構造、ASTファクトリ呼び出しをGoの関数呼び出しへ1:1で翻訳、そしてシムは私のデモにシムがあった箇所に限る。
プロンプトには機械的な1:1移植と書きました。2つの言葉。紙の上では、それが仕様のすべてでした。
しかしデモがないと、「1:1」は「文字どおり1行ずつ」から「テストスイートを通る、それだけ」まで何でも意味し得ます。エージェントは、最も安く目的を満たせる解釈を選びます。
要するに:
それがモデルのせいかデモのせいかはわかりません。でもデモは安いし、AIの身動きの余地を狭めます。安全網としては、それで十分です。
それで、私が実際に学んだこと
もし私が少しでも不注意だったら、typiaは死んでいました。
毎朝同じルーティンでした。差分(diff)を開き、こいつは今度何をしでかした?と目を走らせる。疲れた朝に、もし「テストが全部通ったから」というだけの勢いでマージしていたら、typiaはコアの3分の2が消えた状態で、巨大な参照テーブルのような形で、あるいはCIから失敗テストを除外してZodの上で動く形で出荷されていたでしょう。ライブラリはその場で死んでいました。
でも私は、コーディングにAIを使わないわけにいきません。スピードは本物だし、便利さも本物です。今回のような移行――純粋な反復的な翻訳――は、まさにAIが人間の多週間の仕事を数日へ圧縮する類の作業です。魔人を元に戻すことはできません。
だから本当の問いはどう使うかです。
- 大規模なジョブを投げて寝ないこと。 巨大なタスクをAIに一発で放り込むと、チェックする頃には80億トークンが使われ尽くされ、参照テーブルがコードベースにハードコードされてしまいます。そこからほどくコストは、1ステップずつ進めるコストよりずっと高い。
- 監督(スーパービジョン)の間隔を短く保つ。 各ファイル(または各モジュール)ごとに差分をレビューするのは、たまった奇妙さの丸1晩分をデバッグするために起こされるより、より速くて安全です。エージェントのショートカットをそれが試みた瞬間に捕まえたいのです。増幅する前に。
-
サマリーではなく差分(diff)を読む。 上に挙げたどの失敗も、30秒あれば――実際に差分を開いた誰にでも――見つけられたはずです。AIは悪意があるわけではありません。ただ、「
pnpm testを緑にする」という目的を持つモデルは、実際に何が起きたのかを理解するためではなく、その目的に最適化されたサマリーを作る、というだけです。
雰囲気コーディングは機能します。ですが自動運転に任せると、「ライブラリが死んだ」は一晩先に起こります。スピードを活かしましょう。ただ検品のリズムはタイトに保つこと。1つのプロンプトに1か月分の作業を詰め込まないでください。分割して、進行を追いかけます。
Code
- 使用した正確なプロンプト:
GO-MIGRATION-INSTRUCTION.md -
typia(nextブランチ、Goトランスフォーマー):https://github.com/samchon/typia/tree/next -
ttsc(tsgo用のGoネイティブプラグインホスト):https://github.com/samchon/ttsc




