本記事はcogley.jpの完全版の要約版です。詳細は完全版をご覧ください。
はじめに:AIは速い。だからこそ危ない
SvelteKitのリポジトリを10ほど管理していて、開発にはAnthropicのClaude Codeを活用している。AIコーディングアシスタントは高速かつ有能だが、速いからこそ厄介な問題がある。気をつけないと、見つけにくい形で一貫して間違えるのだ。
「コードが実行しない」という問題ではない。こういう間違いだ。
-
.safeParse()ではなく.parse()を使う - D1のSQLに
.bind()ではなくテンプレートリテラルで値を埋め込む(SQLインジェクションの温床) - データベースへの書き込み結果を確認しない(fire-and-forget)
- load関数に4段階のネストされた非同期ロジックを詰め込む
コードは動く。TypeScriptも通る。PRのdiffでも問題なく見える。
新人プログラマにとって、これは特に危険だ。「動く」=「正しい」と思いがちだが、動くコードにもSQLインジェクション、N+1クエリ、無制限のデータ取得など、本番で爆発する地雷が埋まっていることがある。AIはそれを「自信満々」に生成する。
問題:ガイドファイルは「お願い」に過ぎない
CLAUDE.md(やその他のコーディングエージェント向けガイドファイル)に「常にsafeParse()を使え」「SQLを文字列補間するな」と書いても、それは制約ではなく提案に過ぎない。AIはそれを読み、従うこともあれば従わないこともある。忘れてもコンパイルエラーは出ない。赤い波線も表示されない。
指示が非決定的なら、遵守も非決定的になる。
解決策:バックプレッシャーという考え方
油圧工学では「バックプレッシャー」は流れを制御するために加える抵抗のことだ。バックプレッシャーがなければシステムは氾濫する。ソフトウェアでの等価物は、悪いパターンを「推奨しない」のではなく構造的に不可能にすることだ。
| 例 | |
|---|---|
| 張り紙(無視可能) |
CLAUDE.mdに「常にsafeParse()を使え」と書く |
| 圧力弁(無視不可能) | Zodスキーマに対する.parse()をフラグするlintルール |
目標は、すべての「常に」と「決して」をドキュメントから機械的なものに移行すること。CLAUDE.mdに残すべきは文脈と意図(「なぜ」)であって、具体的な指示(「何を」)ではない。
3層リンティング:なぜ1つでは足りないのか
各ツールには異なる強みがある。1つですべてをカバーしようとすると、パターンの見落としか速度低下のどちらかになる。
oxlint(~50ms)
Rustベースのリンター。約200のJavaScript/TypeScript汎用ルールを約50ミリ秒でチェックする。空港の金属探知機のようなもので、高速に明らかな問題を検出する。
ESLint + Svelteプラグイン
.svelteファイルをスクリプト・テンプレート・スタイルブロック全体として理解する。oxlintには見えないSvelte固有のパターンを検証できる。そしてカスタムのバックプレッシャールールをホストする。
| ルール | 検出内容 |
|---|---|
no-raw-html |
sanitizeHtml()なしの{@html expr}(XSS防止) |
no-binding-leak |
load関数からのplatform.env.*のリターン |
no-schema-parse |
Zodスキーマの.safeParse()ではなく.parse()
|
no-silent-catch |
エラーを握りつぶす空のcatch {}ブロック |
この4つのルールは、以前CLAUDE.mdにしか書かれていなかった「常に」と「決して」を正確にエンコードしている。
ast-grep(構造パターンマッチング)
最新の追加であり、最も興味深いツール。tree-sitterパーサーを使い、コードの構造でマッチングする。ルールは宣言的なYAML。
例えば、N+1クエリの検出:
id: n-plus-one-query-map
language: TypeScript
severity: warning
message: >-
Potential N+1 query: database call inside .map().
Use db.batch() or WHERE IN instead.
rule:
pattern: $ARR.map($$$ARGS)
has:
pattern: $DB.prepare($$$SQL)
stopBy: end
oxlintにもESLintにも表現できないパターンだ。配列イテレーション内にネストされたデータベースクエリ。SvelteKitのload関数からCloudflare D1にアクセスする場面では、パフォーマンスの惨事になる。バッチクエリ1回で済むところを、アイテムごとに1往復。AIは小さなデータセットでは正常に動くコードとしてこれを頻繁に生成する。
ast-grepのルールセット全体:
| ルール | 検出内容 |
|---|---|
sql-injection-d1 |
db.prepare()内のテンプレートリテラル |
sql-injection-concat |
db.prepare()内の文字列結合 |
n-plus-one-query-* |
.map()、.forEach()、for...of内のDBコール |
unbounded-query-all |
SQLにLIMITのない.all()
|
unchecked-db-run |
結果を確認しないfire-and-forgetの.run()
|
empty-catch-block |
エラーの無言の握りつぶし |
上流追跡:「最新情報」の自動監査
機械的な強制は既知のパターンに対応する。だがSvelteとCloudflareは常に新機能をリリースしている。SvelteKitだけで5.0以降50回以上のリリースがあった。AIは先週リリースされた機能を知らない。
そこで補完システムを構築した。シェルスクリプトがSvelteKitパターンフィード(grep対応の検索シグネチャを持つJSON Feed 1.1エンドポイント)とCloudflareのチェンジログRSSを取得し、各リポジトリが使用しているプロダクトのみにフィルタリングして、ソースコードからレガシーパターンを検索する。
出力は具体的だ。「リポジトリxにwritable()ストアを使っているファイルが3つある。Svelte 5.29以降$stateクラスで置換可能。」
フィードにまだ収録されていない新機能を発見すると、「フィードギャップ」としてフラグする。監査がフィードの遅れを教えてくれるので、フィードは常に最新に保たれるフィードバックループだ。
10リポジトリ全体で約10秒で完了する。シェルスクリプトであり、AIコールではない。
一元化と配布
リント設定、ast-grepルール、監査スクリプト、GitHub Actionsワークフロー、これらすべてが1つの.githubリポジトリに集約されている。同期スクリプトが10のコンシューマーリポジトリに配布する。1回のsync-all.sh実行ですべてが更新される。
マルチリポジトリ構成で最も多い障害モードはドリフトだ。1つのリポジトリだけ修正が適用され、他は取り残される。同期がこれを防ぐ。
実際に検出したもの
ast-grepルールだけでフラグされた実例:
- 「検索フィルタを追加して」と頼んだ際にAIが生成したload関数内のテンプレートリテラルSQLインジェクション
-
{#each}ブロックのSSRで、AIがアイテムごとに個別にawaitしたN+1クエリパターン - 開発環境では完璧に動作(5行)し、本番環境ではタイムアウトしたであろう(50,000行)無制限の
.all()クエリ - AIがD1コールをtry/catchで囲みながら、エラーハンドリングを忘れた空のcatchブロック
どれもTypeScriptエラーは出なかった。oxlintにもESLintにも引っかからなかった。構造レイヤーがなければ、次のgit pushで本番に出荷されていた。
新人プログラマへのアドバイス
AIコーディングアシスタントを使い始めたばかりなら、覚えておいてほしいことがある。
-
「動く」は「正しい」ではない。 AIが生成したコードがTypeScriptを通り、テストも通り、画面に期待通りの結果が表示されたとしても、SQLインジェクションやN+1クエリが埋まっている可能性がある。
-
ガイドファイルだけに頼らない。
CLAUDE.mdに書いたルールは、AIが従わないことがある。lintルールや型制約で機械的に強制する方が確実だ。 -
リンターは1つでは足りない。 oxlint(高速・汎用)、ESLint(フレームワーク対応)、ast-grep(構造解析)、それぞれが異なる種類のバグを検出する。3つ合わせて50ms + 2s + 200msでも、人間のレビュー1回より速い。
-
上流の変更を追跡する。 使っているフレームワークやプラットフォームの新機能を知らなければ、AIも古いパターンを生成し続ける。
AIはコードを速く書く。答えはAIを遅くすることでも、レビュアーを増やすことでもない。コードベースが不正なパターンを自動的に、即座に拒否するようにすることだ。
完全版(英語):cogley.jp
Svelteのパターンフィードは公開中。ご自分のツールからも参照できる。
Rick Cogley(コグレー・リック)は株式会社イソリアのCEO兼創業者。東京で日英バイリンガルITアウトソーシングとインフラサービスを提供中。