設計なんて、後でAIと全部書き直せばいい
株式会社ブレインパッドプロダクトユニットでRtoaster GenAIの開発をしている依田です。
本記事は、Rust初学者の私がClaude Codeと共に、合計16時間でOSSツール uv-sbom を作り上げた全記録です。
きっかけ
CycloneDX-Pythonが生成するSBOM(Software Bill of Materials)はJSON形式かXML形式で提供されますが、その書式故に人が読むのは非常に辛いものです。
具体例として、私が作成したOSS「ja-complete」に対し CycloneDX-Python を使って生成したSBOMのjsonファイルをご覧ください。非常に長大で、実に1,600行にも及びます。
このファイルで何が示されているかを大まかに言うと、ja-completeでは推移的参照を含む40種のライブラリを使用していて、それぞれのバージョンとライセンスは何か、ということです。人間が読むには辛すぎます。
SBOMは日本語訳すると「ソフトウェア部品表」で、ソフトウェアが依存するライブラリなどを見える化し、脆弱性対策などに役立てる用途で使われます。
とにかく、動くものが欲しかった
「SBOMをMarkdownの表形式で見たい」という思いと、「今まで使ったことのないプログラミング言語を学びたい」という思いから、私はClaude Codeを開き、こう打ち込みました。
プロジェクト「uv-sbom」は、Pythonのuvを使用したプロジェクトに対しSBOMを生成するためのCLIツールを開発します。
- プログラミング言語: Rust
- 出力: CycloneDXフォーマットのJSONまたは、Markdownの表形式
開発を進めるにあたって、詰めるべき詳細があれば会話しながらステップバイステップで進めましょう。
こうして、事前設計なしのVibe Codingが始まりました。
結末を先に述べると、1時間で私が期待した動作をするアプリケーションが出来上がりました。その後、全く振る舞いを変えることのない15時間のリファクタリングと、セキュリティ対策が待っていました。
1時間のVibe Codingがもたらした、期待以上の成果
Claude Codeとの最初の1時間は、まさに魔法でした。
私はRust初学者。cargo newすら知らない状態からのスタートでした。しかし、Claude Codeは私の要求を次々と形にしていきます。
最初の5分で決まったこと
- データソース:
uv.lockファイル - 出力形式: CycloneDX 1.6 JSON / Markdown表
- ライセンス情報: PyPI APIから取得
- CLI設計:
--path <filepath>,--format json|markdown,--output <file>
次の10分で出来上がったもの
- 7ファイル構成のRustプロジェクト
- 動作するCLIツール
src/
├── main.rs # エントリーポイント
├── cli.rs # CLI引数パース
├── lockfile.rs # uv.lockパーサー
├── license.rs # PyPI APIクライアント
├── cyclonedx.rs # CycloneDX JSON生成
├── markdown.rs # Markdown表生成
└── error.rs # エラー型
コマンドラインで cargo run -- --format markdown を実行したら、すぐに以下のような結果を得られました。たったの15分で、当初求めていたものが出来上がりました。
「動くもの」があるからこそ見えた真のニーズ
動くものがあるからこそ、更に欲が出ます。前述の構成ではアプリケーションが直接依存しているライブラリと、依存しているライブラリが依存しているいわゆる推移的依存のライブラリの見分けがつきません。よって、直接依存のライブラリの一覧と、推移的依存のライブラリ一覧、それぞれがわかるような構成に拡張したくなりました。
ここで私は 「まず作る」ことの価値を体感しました。
早速改善に着手します。Markdownのレイアウトを検討し、期待結果を作成→Claude Codeへ修正案を投げVibe Coding→再度の動作確認を行いました。45分で以下のファイルを出力できるまでにアプリケーションを改善できました。
技術的負債
ただし、1時間で出来上がったコードは、まさに「技術的負債」でした。以下は一例です。
問題点:
- I/O操作とビジネスロジックが密結合
- テストが書けない(実ファイル・実APIが必要)
- 機能追加のたびに
main.rsが肥大化
しかし、この時点では技術的負債を容認していました。そもそも、コードは1行も読んでいませんでした。優先すべきは「動くアプリケーション」だったからです。
実装量から鑑みて、リファクタリングは規模がさほど多くないであろうこと、Claude Codeの力があれば、ものの1時間もあればそれも済むであろう、と思っていました。
15時間のリファクタリングとセキュリティ対策
さて、きちんと動作するアプリケーションができたので、次は公開を目指します。OSSとして公開するからには、成果物に対し著作者自身、つまり私がコードに対する責任を負うことになります。
そこでようやく前述のコードを真面目に読み始めました。つい1時間前までは cargo new コマンドさえ知らなかった私なので、もちろん泥団子化しているコードを読み解けるはずがありません。
無茶振り
リファクタリングするからには、人間が読み解きやすい構造のプロジェクトになっていることが求められます。そこで、Claude Codeに対して以下のように指示を出しました。
以下の内容で全てのコードをリファクタリングして下さい
- ドメイン駆動設計
- ヘキサゴナルアーキテクチャ
- 叫ぶアーキテクチャ
あまりに雑な指示です。これを実務で人に対して行おうモノなら無茶振りです。
しかし、Claude Codeは応えました。
AIが提示した「開発計画」の衝撃
Claude Codeは、いきなりコードを書き始めるのではなく、私に対しいくつかの選択肢を選ばせるようなやり取りをした上で、以下のように詳細な開発計画を提示してきました。
# フェーズ
1. ドメインモデルの実装
2. ポートの定義(LockfileReader、LicenseRepository等)
3. アダプターの実装
4. ユースケースの実装
5. main.rsでのDI配線
6. テストの追加
開発者が設計者とのコミュニケーション不足により、設計者の意図と異なる実装をしてしまうことはよくある話です。この点において、Claude Codeは事前にディレクトリ構成を提案し、私の承認を得ながら計画を立てたことは非常によくできていると感じました。
7ファイルから29ファイルへ
リファクタリング後のファイル構成は、こうなりました。
src/
├── main.rs # DI配線のみ
├── lib.rs # ライブラリルート
│
├── sbom_generation/ # ドメイン層
│ ├── domain/ # ドメインモデル
│ │ ├── package.rs # Package値オブジェクト
│ │ ├── dependency_graph.rs # DependencyGraph集約
│ │ ├── license_info.rs # LicenseInfo値オブジェクト
│ │ └── sbom_metadata.rs # SBOMメタデータ
│ ├── services/ # ドメインサービス
│ │ ├── dependency_analyzer.rs # 依存関係分析(純粋関数)
│ │ └── sbom_generator.rs # SBOM生成
│ └── policies/ # ビジネスポリシー
│ └── license_priority.rs # ライセンス選択ルール
│
├── application/ # アプリケーション層
│ ├── use_cases/
│ │ └── generate_sbom.rs # GenerateSbomUseCase
│ └── dto/
│ ├── sbom_request.rs # リクエストDTO
│ └── sbom_response.rs # レスポンスDTO
│
├── ports/ # ポート
│ ├── inbound/
│ │ └── mod.rs
│ └── outbound/
│ ├── lockfile_reader.rs
│ ├── project_config_reader.rs
│ ├── license_repository.rs
│ ├── formatter.rs
│ ├── output_presenter.rs
│ └── progress_reporter.rs
│
├── adapters/ # アダプター
│ ├── outbound/
│ │ ├── filesystem/
│ │ │ ├── file_reader.rs
│ │ │ └── file_writer.rs
│ │ ├── network/
│ │ │ └── pypi_client.rs
│ │ ├── formatters/
│ │ │ ├── cyclonedx_formatter.rs
│ │ │ └── markdown_formatter.rs
│ │ └── console/
│ │ └── progress_reporter.rs
│
├── shared/ # ドメインに依存しない共通モデル
│ ├── error.rs
│ ├── result.rs
│ └── security.rs
│
└── cli.rs # CLI引数パース
7ファイルから29ファイルへ。 正直にいうと、ソフトウェアの規模に対して過剰な分割です。
そもそも、ヘキサゴナルアーキテクチャに触れた書籍の中でも、ここまで厳密な分割をせずとも良いと述べているものもあります。
ポート&アダプターパターンの威力
一方で、リファクタリング後のmain.rsは、非常にシンプルになりました。
run メソッドの中身は以下の順序で呼び出しのみ行い、具体的なロジックを含まないようになりました。
- 引数のパース
- バリデーションチェック
- アダプターの作成(DI)
- ユースケースへ注入(DI)
- ユースケース実行
- 出力
main.rsには、I/O操作が一切ありません。 すべてがポート(インターフェース)経由です。
これにより、どこに何の実装があるか、責務が明確になったため、実装を理解するための認知負荷を低減できました。一例として、PyPIからデータを取得する以下の処理が挙げられます。
Claude Codeに起因するボトルネックと、人間にしかできない品質の追求
リファクタリング中最も辛かったのはクォータ制限です。開発における、最大のボトルネックがこれでした。大量のコード生成を短時間で行うと、使用制限に引っかかります。
私の場合、個人で契約しているプランが Claude Code Pro だったため、リファクタリング中、2〜3回制限に引っかかり待ち時間が生じました。
容易なリファクタリングであれば、生成AIの使用量を少しでも削減できるよう、適宜分業していくのが良いでしょう。
生成したコードの細かな漏れ抜け
リファクタリング完了後、コンパイラが警告を出すようになりました。一例としては、未使用な関数があるなどの警告です。Claude Codeは、大枠の設計は完璧に実装しましたが、細部に「散らかし」がありました。
この点に関しては、事前にコンテキストとして「コンパイル時に警告が発生しないようにすること」などの条件を設定しておくと良いでしょう。
セキュリティ上の考慮漏れ
最も深刻だったのが、セキュリティ検証の欠如でした。
// Before(セキュリティリスク)
fn read_lockfile(&self, project_path: &Path) -> Result<String> {
let lockfile_path = project_path.join("uv.lock");
fs::read_to_string(&lockfile_path)? // シンボリックリンク攻撃に脆弱
}
攻撃シナリオとして、以下のようなケースが考えられます。
# 攻撃者が uv.lock をシンボリックリンクに置き換え
$ ln -s /etc/passwd uv.lock
$ uv-sbom --format json
# → /etc/passwd の内容が読み取られる
対策として、以下のようなセキュリティ検証モジュールを追加しました。
pub fn validate_regular_file(path: &Path, file_description: &str) -> Result<()> {
let metadata = fs::symlink_metadata(path)
.map_err(|e| anyhow::anyhow!("Failed to read {} metadata: {}", file_description, e))?;
if metadata.is_symlink() {
anyhow::bail!(
"Security: {} is a symbolic link. For security reasons, symbolic links are not allowed.",
path.display()
);
}
if !metadata.is_file() {
anyhow::bail!("{} is not a regular file", path.display());
}
Ok(())
}
このようなセキュリティリスクは、私からの指摘で判明しました。Claude Codeは、事前に「セキュアな設計をせよ」と明示していなかったため、具体的な脅威モデルまでは考慮していませんでした。
上記の他にもDoS対策として、ファイル制限のチェックなども実装していますが割愛します。
リファクタリングとセキュリティ対策には、Claude Codeが稼働していた時間+私が手を動かしていた時間を合わせて、およそ15時間かかりました(クォータ制限の解除待ち時間を除く)。
ここまで時間がかかったポイントとしては、私がRust初学者だったことも大きく影響していますが、その点を加味しても振る舞いを変えない設計変更とセキュリティ対策には、プロトタイプを開発した1時間より3〜4倍以上はかかるものと考えられます。
OSSとしての体裁を整える
リファクタリングとセキュリティ対策が終わったら、いよいよOSSとしてリリースする準備を始めます。
GitHub ActionsによるCI/CD構築
OSSとして公開するため、GitHub Actionsでバイナリ配布を自動化しました。この点もClaude Codeに丸投げでプロトタイプを作成させる→実際に動作させて、不満な点を改善する、といった流れで進めました。これも、Claude Codeがものの数分で構築してくれました。
PyPIでの公開
uv-sbom はRust製のCLIツールなので、そのままではPyPIで公開できません。
しかし、このツールは uv のためのツールなので、やはり uv tool install コマンドでインストールできることが理想的です。
そのため、RustのバイナリをPythonでインストールするラッパーを用意しました。
あとはPyPIの設定でこのリポジトリを参照させるようにし、GitHub ActionsでPyPIへPublishするCDを構築すれば、 uv によるインストールが可能になります。
PyPIの設定は初回だけ人の手で行う必要がありますが、その手順の明示も、ラッパーの実装も、CDの実装も、全てClaude Codeとのチャットで完結しました。
生成AIが登場する前は、CI/CDの構築もソフトウェアの公開手順も、全てWeb検索して多くのハズレ記事を踏みながら、多大な時間をかけて先人の手順を見つけることがままありましたが、今はその手順も不要です。
ただし、一般的に広く使われているツールや言語に限定されす。生成AIもWeb上にある情報を元にしていることが多いため、ニッチなツールや言語でWeb上に有益な情報がほとんど、あるいは全くない場合、平気で誤った情報を返す点には十分注意しましょう。
AI時代の設計
この16時間の開発から、私は以下のように感じました。
- 小規模かつ、生成AIのクォータ制限を気にしなくて良いプロジェクトであれば、設計を後回しにしてプロトタイプを作っても良い
- 最大のボトルネックはクォータ制限と、細かなリファクタリング後の後始末漏れです
- リファクタリング後のコード規模が大きくならないのなら、人力での修正漏れ検出も現実的な時間で完了できます
- 価値の検証を最優先にすることで、当初想定していなかった真のニーズが見えてきます
- 小規模かつ、生成AIのクォータ制限を考慮しなければならないプロジェクトであれば、アーキテクチャ設計は後回しにしつつ、事前にコンテキストを設計すると良さそう
- 小規模ならば人間によるリファクタリングも現実的な時間で対処可能
- 生成AIを効率的に使うためには、リファクタ後のコードで細かな漏れ抜けが起こらないようなルールがあると、効率よく生成AIを活用できる
- 中規模以上のプロジェクトでは、生成AIによるリファクタリングも多大な時間がかかるので、事前にアーキテクチャ設計を決定し、それをコンテキストとして生成AIに渡す方が効率は良さそう
- コードベースが大きすぎて、AIのコンテキストに収まらない
- レガシーコードとの互換性維持が必要で、生成AIが破壊的変更をしないよう配慮する必要もある
おわりに
uv-sbom は、MIT Licenseで以下のリポジトリで公開しています。
# インストール
$ uv tool install uv-sbom
# 使用例
$ uv-sbom --format markdown
$ uv-sbom --format json --output sbom.json
ちなみに、Vibe CodingではRustの知識は全く身につきませんでした。
結局、Hello WorldとFizzBuzzの実装が一番手っ取り早いのだなぁ。