Claude が書いたパッケージマネージャーを信頼できるまでに必要だったこと
この記事は AI で英語から翻訳しました。英語の原文を読む
jpm を作るとき、僕はコードを一行も書いていないし、読んですらいない。jpm は npm、pnpm、yarn、bun のプロジェクトで使える JavaScript のパッケージマネージャーだ。サイズを小さくするために、TLS、暗号、http のモジュールまで AI に自前で書かせたけど、それも含めての話だ。最初は正真正銘のバイブコーディングのプロジェクトだった。しかも午後だけで出来上がった。あの状態のものを、僕も、ほかの誰も信頼すべきではなかった。でもそこから、プロのソフトウェアエンジニアとして 27 年以上やってきた経験を活かして、既存のテストスイートに対してテストさせ、さらに 5 日間かけて自分でコードを書かせ、徹底的にテストさせて、堅牢にしていった。
僕のバックグラウンド
これまでのキャリアでたくさんのプログラミング言語を使ってきたけど、ほとんどは C#、Elixir、JavaScript みたいな高水準の言語だった。子どもの頃に C を覚えて、夏休みに父の職場で低レベルのディスクコントローラーの仕事を手伝って以来、低レベルな言語は関数をいくつか書いた程度しか使っていない。だから Rust を覚えて、それで 60k+ 行のプロジェクトを作るなんて、短期間でできることじゃなかった。僕が身につけてきたのは npm、pnpm、bun がどう動くかだ。Node.js 0.8(2012)の頃から仕事で Node.js を使ってきたからね。それに、ソフトウェアには本当はどれだけのテストが必要か、そして一度やって終わりじゃなく自動化することがどれだけ大事かも知っている。テストを先に書けと言っていたアジャイルの人たちの言うことを聞いておいて、ちゃんと報われた!
なぜ jpm なのか
YouTube の Syntax というポッドキャストを見ていた。ホストは Wes、Scott、CJ で、その回の冒頭で upm の話をしていた。速くてすごく小さいパッケージマネージャーだけど、JavaScript で書かれている。まず思ったのは、小さいというのは実は違うということ。Node.js ランタイムか、何かしらのランタイムを含めないといけないからだ。upm を書いたのは Pooya で、CJ はプロジェクトが出しているベンチマークを見せて速さを紹介していたけど、僕はただ、もっと速くできるか試してみたかった。Pooya は JavaScript エコシステム向けにいいツールをたくさん作っている優秀なエンジニアだから、簡単じゃないことは分かっていた。最後に、僕はモデルとして Opus 5.5 を使っていて、その出来に圧倒されていた。だから、これだけ多くのツールが Rust で作られてきたんだし(ポッドキャストでもそのネタで冗談を言っていた)、Rust なら本当に小さくて速いパッケージマネージャーを作れるかもしれない、と思った。
ポッドキャストでは、ホストたちがリスナーから寄せられた AI についての質問に答えていた。今みんなが聞いている質問だ。AI は本物のソフトウェアを作れるのか、それともデモだけなのか? エンジニアは心配すべきなのか、それともワクワクすべきなのか? たいていの人は、AI は何でも一発で作れると思っている。ネタバレすると、作れる。ただ、指揮する人がいて、ソフトウェア開発を理解していないと、いい結果にならないだけだ。
あの日の午後
それでその日の午後、本業の仕事を片付けたあと、ターミナルでタブを開いて、upm を Rust の CLI として書き直すには何が必要かを Claude に聞いた。まとめに目を通して OK を出すと、Claude は作業を始めた。たしか 45 分くらいで動くバージョンができていたと思う。ただ、サードパーティの Rust クレートをたくさん使っていたせいでかなり大きくなっていたし、テストも小さな package.json ファイルでしかしていなかった。小さくするために、何がそんなにサイズを大きくしているのかを分析させて、その日の夜が終わる頃には、自前の HTTP、JSON、暗号、TLS モジュールと、ずっと小さなバイナリができていた。必要な部分だけを書くように指示したけど、同時に、できればほかのプロジェクトのテストで、無理なら自分たちのテストとファジングで、徹底的にテストするようにも指示した。しっかりしたものになっているか確かめたかったからだ。
5 日間
ここからが、「バイブコーディング」でプロジェクトを作っている人たちがみんな省いてしまう部分だ。たしかに、Windows の WSL2 上の Linux で、小さな package.json ファイルでは動いていた。でも Mac や Windows では試していなかったし、本当に大きなプロジェクトも一切試していなかった。そこで、堅牢にするためのスクリプトを書かせた。100、最終的には 166 の大規模な公開プロジェクトを取ってきて、インストールとビルドを試す。次に、そのプロジェクトが使っているパッケージマネージャーでインストールとビルドをして比較する。できるところではテストも実行する。テストによってはシークレットやほかのセットアップが必要だったからだ。さらに同じことを Windows のコマンドラインと PowerShell でもやらせて、僕は Mac でもエージェントを立ち上げて、Mac で同じことをさせた。そして 5 日後、何をどうサポートするかについてたくさんの判断を下しながら、マージされた PR は 261 本になり、堅牢でしっかりテストされたパッケージマネージャーができあがった。
指揮するとはどういうことだったか
というわけで、コードは書かなかったけど、指揮はしたし、細かい判断もたくさん下した。たとえば Claude は、もっと速いロックファイル形式にしても数 ms しか縮まらないと言ったけど、僕はそれでも変更を頼んだ。Claude はグローバルストアをオプトインの機能にしたがったけど、99% のパッケージで使えるならなぜそうするのかと指摘して、デフォルトになった。それから、許可するものとして明示的に列挙されていない限り、依存パッケージのインストールスクリプトも含めて、スクリプトを実行させたくなかった。今は AI が作業のほとんどをやってくれるんだから、パッケージのスクリプトを承認する前に、まず AI に問題がないかレビューしてもらえばいい、というのが僕の考えだ。
Claude が本気で反対してきたことも 2 回あった。でも 2 回とも僕は押し通した。
1 回目は TLS だ。バイナリを小さくしたくて、その大部分を占めていたのが TLS と暗号のクレートだった。Claude は、それは絶対に自分で書いてはいけないコードだと言った。僕はそれは古いエンジニアリングの考え方だと返した。Rust を使っているんだから、Heartbleed のようなものの原因になったメモリのバグはそもそも問題にならないし、自分たちの実装だって同じくらい、いやそれ以上に徹底的にテストできる。Claude はリスクを大げさに言いすぎたと認めて、それを証明できるテストスイート(Wycheproof、NIST のテストベクター、x509-limbo、BoGo)を挙げた。これが本当にいい考えだったのか、今でも疑問に思っている。Claude はすぐこっちをおだててくるから、リスクを大げさに言ったと認めたのも、ただ合わせてくれただけかもしれない。ほかのプロジェクトで実際にあったバグをもとにしたテストも含めて、たくさんのテストに通っている。誰が書いたかに関係なく、今ならずっと信頼できるのは分かっている。
2 回目は HTTP/2 だ。もっと速くしたくて、HTTP/2 について聞いた。Claude はまず curl で試して、作る価値はないと言った。HTTP/1.1 で行う jpm のインストール全体のほうが、curl の HTTP/2 でのダウンロード単体よりもすでに速かったからだ。それでも僕は押した。curl はあらゆるケースを扱わなきゃいけないけど、僕たちに必要なのは 1 つのレジストリへの大量の GET だけだ。bun に勝つには常識の外で考えないといけない。Claude は納得して、2 時間ほどで動くクライアントを作った。
それから計測した。自宅の回線では HTTP/2 のほうが約 17% 速かった。GitHub のランナーでは互角か負けで、nuxt のコールドインストールでは最大 2 倍遅く、大きな tarball では CPU を 8〜15% 多く使った。インストールが本当に時間とお金を食うのは CI だし、バイナリも 60 KB 増えた。だから 3,671 行を全部削除させた。あれは僕が間違っていたし、数字がそれを教えてくれた。
逆のパターンもあった。jpm は必要なら Node もインストールしてくれる。僕は、Node の署名チェックはインストールを遅くすると思って、フラグで有効にする形にしたかった。Claude は計測して 15〜19 ms だと示し、フラグにするのは誰もオンにしないのと同じだと言った。今はデフォルトで有効になっていて、セキュリティが上がるから、そうしてよかったと思っている。
誰も打っていないコードを信頼する理由
手短に言えば、Wycheproof、node-semver、pnpm、yarn、bun それぞれのテスト、そしてテストに使った 166 のリポジトリのおかげだ。その過程で見つかったバグの一部を挙げる:
- node-semver 自身のテストで、jpm の結果が食い違うケースが 25 件見つかった。たとえば
^9007199254740991.0.0を範囲として受け入れてしまうなど。 - npm-package-arg のテストは、最初は 154 件中 117 件しか一致しなかった。
a@a.tar.gzがタグとして解釈されていた。 - セキュリティスキャンで、クローンしたリポジトリの
.npmrcが、レジストリのトークンをそのリポジトリ自身のプロキシ経由で送らせられることが分かった。概念実証ではBearer USER-SECRET-TOKENを取得できた。今はプロジェクトの.npmrcでセキュリティを下げることはできない。 - 実際のリポジトリで、jpm のインストール結果が pnpm と食い違う依存リンクは 1,320 から 183 に、npm との食い違いは 614 から 14 に減った。
- macOS で試すまで、macOS ではそもそもコンパイルすら通らなかった。
これだけ長くソフトウェアを作ってきたから、どれだけテストを書いてどれだけテストしても、バグのないものなんてないことも分かっている。信頼は勝ち取るものだから、これからは自分のすべてのプロジェクトでこれを使っていくつもりだし、修正への対応も続けていく。AI が送ってきた修正であってもだ。全部取り込むという意味じゃない。テストで何かを直していることを証明してもらう必要がある。要するに、PR を閉じるつもりはないけど、その対応には AI の手を借りるということだ。
現状
実行ファイルは 2MB になった。いつも最速というわけじゃないけど、トップクラスにはいる。承認なしにスクリプトを実行しないので、最も安全なものの 1 つでもある。公開から少なくとも 1 日経っていないパッケージは、最新バージョンに更新しない。それに .npmrc まわりのセキュリティもある。
GitHub Actions のランナー上で、ほかのパッケージマネージャーと比べてどれだけ速くなれるかを示したグラフの 1 つがこれだ:

すべてのマシン、フェーズ、プロジェクトの結果は getjpm.sh にある。
最速ではないところ
正直に言うと、ベンチマークは小規模で、リポジトリは 3 つだけだ。それに、いつも最速というわけでもない。たとえば Windows の nuxt では、Defender のスキャンが足を引っ張っている。macOS では、nuxt のウォームインストールで bun と aube が勝っている。だから、速さを最終目標にするなら改善の余地はある。Linux、macOS、Windows で 166 のプロジェクトのインストールを実行したし、バグを見つけるために Linux ではさらに多くのランダムなプロジェクトも試した。でも、そのひとつひとつについて CI や各マシンで性能テストをしたわけではない。
jpm は完璧か? いや。僕が 27 年間に手で書いてきたものも、人間が書いてきたどんなものも、完璧じゃない。完璧なんて存在しない。あるのは「十分」と「素晴らしい」、そして自分の手元にあるのがどちらなのかを知るのに十分なテストだ。
macOS と Linux では curl -fsSL https://getjpm.sh | sh、Windows では irm https://getjpm.sh/install.ps1 | iex で試してみて、好きなプロジェクトで jpm install を実行してほしい。バグを見つけたら GitHub で issue を立ててほしい。