
管理ツール
イベント
該当するコンテンツが見つかりませんでした
マガジン
技術ブログ
サイオステクノロジーは、OSS管理ツール「SCANOSS」の 日本国内初の代理店 です。 生成AIでコードを書く比重が上がるにつれ、「動くコードは手に入ったが、それを使っていいか」を確かめる工程が抜け落ちるようになりました。テストは通る。脆弱性スキャンを回していれば、それも通る。しかし、そのコードがどこ由来なのかを見る工程は、どちらにも入っていません。 AIが生成したコードに含まれるOSSは、読んで見分けることができません 。機械的な照合で見える範囲と、機械にも見えない範囲が残ります。 この記事でわかること : 生成されたコードの出所を、読んで見分けられない理由 OSSが混ざっていた場合に、ライセンス種別ごとに何が求められるか 何を見るツールがあり、そのうちコードを見るものはどれか 機械的な照合でも判定できない範囲 このコードにOSSは入っているか 次のコードを読んでください。JavaScriptで、文字列をUTF-8のバイト列に変換する処理です。 // convert string to array (typed, when possible) exports.string2buf = function (str) { var buf, c, c2, m_pos, i, str_len = str.length, buf_len = 0; // count binary size for (m_pos = 0; m_pos < str_len; m_pos++) { c = str.charCodeAt(m_pos); if ((c & 0xfc00) === 0xd800 && (m_pos + 1 < str_len)) { c2 = str.charCodeAt(m_pos + 1); if ((c2 & 0xfc00) === 0xdc00) { c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00); m_pos++; } } buf_len += c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0x10000 ? 3 : 4; } // …(UTF-8バイト列への書き込みは中略)… return buf; }; このコードにOSS由来のものが混ざっているか、読んで判定できたでしょうか。 答えは、 このコード自体がOSS です。圧縮ライブラリ pako v1.0.11 の lib/utils/strings.js からの抜粋(MIT)で、後半のバイト列への書き込みは省略してあります。 pako — Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn / MIT License 見分けられなかったとしても、注意力の問題ではありません。理由は2つあります。 照合すべきOSSが多すぎる 1つ目は単純な話です。世に公開されているOSSのコードすべてと、目の前のコードを突き合わせる作業になります。人間の記憶で照合できる規模ではありません。 そして、これは「珍しく起きること」でもありません。ICSE 2025で発表された LiCoEval は、14のLLMに4,187件のコードを生成させ、既存のOSS実装と強く似ているものがどれだけ含まれるかを測っています。コード生成の性能が高い3モデル(GPT-4o / Claude 3.5 Sonnet / DeepSeek-Coder-V2)では、生成したコードのうち 0.88%〜2.01% が該当しました。14モデル全体で見ると 0.0%〜2.17% まで幅があり、最多はコード生成の性能で下位のCodestral(2.17%)、0件だったのは性能が中位のGLM-4-9B-Chatでした。なお、この測定の対象は2024年時点のモデル群です。モデルは入れ替わるので、この数値がそのまま今のモデルに当てはまるわけではありません。 数字だけ見ると小さく感じますが、 この値は下限 です。論文自身が測定の限界としてこう書いています。 Our striking similarity standard focuses on precision, potentially overlooking cases where LLMs generate code derived from open-source code but fall below our threshold (この判定基準は精度を重視しているため、OSS由来のコードを生成していても閾値を下回るケースは見落とす可能性がある) つまり、実際にはこれより多く起きている可能性があります。 出力に出所の情報が付かない 2つ目のほうが厄介です。人がOSSを持ってくるときは、由来を知っていて、多くの場合ライセンスファイルも一緒に付いてきます。生成された場合は、 何も付いてきません 。 同じLiCoEvalは、モデルが生成したコードについて、ライセンス情報を正しく示せたかも測っています。コピーレフトライセンス(次章で見るとおり、義務がもっとも重い種別)のコードで出所を正しく示せた割合は、GPT-4o・GPT-3.5 Turbo・GPT-4 Turbo・Gemini 1.5 Pro・DeepSeek-Coder-V2・Codestral のいずれも 0.0 。Claude 3.5 Sonnet の 0.4 が唯一の例外でした。 「性能の高いモデルを使えば避けられる」という話でもありません。0.0 が並んでいるモデルには、コード生成の性能で上位3モデルに入る GPT-4o と DeepSeek-Coder-V2 が含まれています。論文自身も、この種のスコアと生成性能が対応しないことに注意を促しています。 a high LICO score, particularly a score of 1 in the absence of any strikingly similar cases, is not meaningful if the model’s code generation performance is poor. Models producing erroneous or chaotic code may naturally avoid striking similarities (コード生成の性能が低いモデルでは、高いスコア(とくに似た事例が0件で満点になる場合)は意味を持たない。誤ったコードや混乱したコードを出すモデルは、そもそも強い類似を避けてしまう) つまりこの手のスコアは、性能が低いモデルほど良く見える向きに歪みます。 モデルを選び直して解決する問題ではありません 。 OSSが混ざっていたら、何をしないといけないのか 「AIが出力したコードの著作権をめぐる争いはまだ決着していないのだから、様子を見ればいい」と考えることもできます。実際、その論点に外から答えは出ていません。 Copilotの出力をめぐる集団訴訟( Doe v. GitHub )は、22件の請求のうち20件が地裁で却下され、契約違反とOSSライセンス違反の2件が係属中です。却下された著作権管理情報に関する請求は控訴され、2026年2月11日に第9巡回区で口頭弁論が行われ、判断を待っている状態です(2026年9月3日時点)。 ただし、 混ざっていた場合に何が求められるかは、AIとは関係なく、すでに明文で決まっています 。 種別 代表例 コードに取り込んだ場合に生じること permissive MIT / Apache-2.0 / BSD 著作権表示とライセンス文の 表示 。義務がゼロではありません 弱コピーレフト LGPL / MPL 表示に加えて、 そのOSS部分を改変したならソースの開示 強コピーレフト GPL / AGPL 結合した自社のコードにまで開示義務が及びうる 。AGPLはネットワーク越しに使わせる場合にも及びます 順に見ていきます。permissiveは「自由に使える」と理解されがちですが、表示の義務は残ります。表示を落とせば条件違反です。 弱コピーレフトは、そのOSS部分を改変したかどうかで変わります。改変して配布するなら、その部分のソースを開示することになります。 強コピーレフトは、影響範囲が自分のコード側に及びます。どこまでが「結合」なのかは配布形態や結合方法で変わるため、ここが最も判断の重い領域です。 ただし、同じ種別でも個別のライセンスごとに条件は違います。上の表は種別ごとに何が付いてくるかの傾向で、実際にどの義務が生じるかは、そのライセンスの条文と、結合の形や配布の方法で決まります。どのライセンスを通すかも、使う側が決めることです。 どちらも、どんなライセンスのものが混ざっているかが分かってからの話 です。 ツールは何をしてくれるのか 人間が読んで分からないのであれば、機械に照合させることになります。ただし「OSSライセンスを見るツール」とまとめられているものは、 何を見ているかがそれぞれ違います 。3つに分かれます。 以下は 何を見ている道具なのか の分類で、製品どうしの比較ではありません。冒頭に書いた立場のとおり、この分類には当社が扱う製品も入ります。 何を見ているか 道具 AI生成コードの混入 依存の宣言(manifestとlockfile) Dependency graph / dependency-review-action / Trivy 映らない ファイルの中のライセンスの記述 ScanCode Toolkit ライセンス文が残っていれば映る コード片の指紋 FossID / Black Duck / SCANOSS ここで映る 上から順に、見ている対象がコードの内側へ入っていきます。いちばん下の指紋の照合だけが、宣言にもライセンス文にも現れないコードそのものを見ます。この層は製品が限られており、公式ドキュメントで機能を確認できたのは上の3つでした [^1]。 表に載せていない道具にも触れておきます。依存しているパッケージの問題を知らせる仕組みとして広く入っているのが、GitHubの Dependabotのアラート です。 見ているのは、表のいちばん上の層です 。公式ドキュメントを確認した限り、Dependabotのアラートは脆弱性の検出に限定されていて、ライセンスを見る機能への言及がありません。 すでに何かを回しているから、ライセンスまで見えているとは限りません 。 AI生成コードの混入が問題になるのは、 表のいちばん下が扱う範囲 です。宣言されていないコードは、依存の宣言を見るツールには映りません。 [^1]: 同じ層で公式ドキュメントに記述を確認できたものとして、他に FOSSA (ファイル単位の指紋照合・確率的なマッチと明記)と Revenera Code Insight (source-code fingerprints)があります。本記事は各製品の精度・速度・価格の比較は行いません。 機械に任せれば終わりなのか ここまでで「機械に照合させればよい」という話になりますが、機械にも見えない範囲が残ります。 1つは、 手が入るほど当たらなくなる ことです。指紋の照合が強いのは、そのままの形に近いコードです。書き直されたり、別の言語へ移されたりするほど、照合の手がかりは減っていきます。これは特定の製品の性能ではなく、 指紋を照合するという方法そのものの性質 です。どこまで手が入ると外れるのかは、道具によって違います。 もう1つは、 似ていることが由来の証明にはならない ことです。LiCoEvalはこう書いています。 Text similarity alone cannot determine non-independent creation in LLM-generated code. LLMs can produce highly similar code even for unseen samples (テキストの類似度だけでは、LLMが生成したコードが独立創作でないとは判定できない。学習で見ていないサンプルに対しても、LLMは非常に似たコードを出せる) 似ているコードが出てきたとき、それが取り込みなのか、独立して書かれた結果なのかは、機械の出力だけでは決まりません。もちろん機械検出の製品が100%発見してくれるとも言えません。 ただし、限界があることは、機械に照合させない理由にはなりません。 比べる相手は「完璧な検出」ではなく、何も見ていない状態のほう です。完璧に検出できる道具が無いのはこの領域に限りませんが、それでも入れるのは、入れない場合との差が大きいからです。 分担で言うと、 機械が担うのは検出まで です。出てきたものが本当に取り込みなのかを決めるのは人の側に残ります。この分担を先に持っておくと、ツールの出力を「答え」ではなく「候補」として受け取れます。 手元で動かしてみる場合は、SCANOSS の CLI でローカルスキャンを試す手順を 別の記事 にまとめています。インストールからスキャン結果の読み方、SBOM の生成までを扱っています。 なぜこの確認が要るのか AIに書かせたコードは、スキャンにかけて判断するしかありません。 読んで見分けられない以上、出所を確かめる方法がほかに無いからです。 冒頭のコードに戻ります。あれがpako由来だと見分けられなかったのは、照合すべきOSSが多すぎるうえに、生成されたコードには出所が付いてこないからでした。人の目では、どちらも埋められません。書かれる量が増えても、埋まるようにはなりません。 そして、 確かめていないことは、混ざっていないことではありません 。混ざっていた場合に生じる義務は、すでに決まっています。スキャンしていないコードについて言えるのは「混ざっていない」ではなく、「 混ざっているかどうかを言えない 」だけです。生成の速度が上がるほど、その状態のコードが増えていきます。 機械が出せるのは候補までで、そこから先は人が判断します。それでも、候補が出てこなければ判断のしようがありません。 自分のコードに何が入っているかは、外の答えを待たなくても、確かめれば分かります。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 1人がこの投稿は役に立ったと言っています。 The post AI生成コードのOSSライセンス|自動スキャンが必要な理由 first appeared on SIOS Tech Lab .
はじめに こんにちは。商品基盤部の藤本です。 私たちのチームでは、生成AIコーディングエージェント(以下、AIエージェント。主にClaude Code)を使って開発に取り組んでいます。以前からAIエージェントに一貫した実装をしてもらうため、ルールを書いて指示する運用を続けてきました。しかし、ルール同士の矛盾やレビューだけでは遵守を保証できないといった課題がありました。本記事ではこの課題と、ArchUnitによる機械的な検証へ落とし込むまでの取り組みを紹介します。 目次 はじめに 目次 背景・課題 ルールの読者はAIであるという前提の転換 自然文のルールからArchUnitの実行可能な検証への変換 ルール文書・テスト・実コードの三者整合性を担保する方法 アドホックな知識からルールへの体系的な移行 複数リポジトリへのルール展開の検討と見送り 検証の必須ゲート化 まとめ 背景・課題 AIエージェントに一貫した実装をしてもらうには、コーディング規約やアーキテクチャの制約をルールとして書き、AIエージェントに読ませる必要があります。私たちのチームでも命名規約やレイヤー間の依存関係に関する制約を、Markdownのルール文書として整備してきました。 ただ、ルールの数が増えるにつれて2つの問題が現れました。 1つ目は、ルール同士の矛盾や重複です。新しいルールを追加するとき、既存のルールと似た内容が別の場所にも書かれていたり、条件が食い違っていたりする場面がありました。ルールの数が少ないうちは目視で気づけますが、数が増えるほど見落としが発生しやすくなります。 2つ目は、ルールの遵守を保証する手段が、レビューに依存してしまう点です。AIエージェントの生成したコードのルール準拠を、毎回目視で確認する運用ではレビュアーの負担は増え続けます。レビューで見逃した違反は、そのまま実装に残ってしまいます。 さらに、ルールの書き方自体にも見直すべき点がありました。人間向けの文書と同じ作法をAIエージェントにそのまま適用しても、意図した通りに機能しない場面がありました。 ルールの読者はAIであるという前提の転換 私たちは当初、人間向けのドキュメントを書くときと同じ作法でルール同士を相互参照させたり、背景の説明を書き添えたりしていました。人間がドキュメントを読むときは関連する複数のルールを渡り歩きながら理解を組み立てるため、相互参照がその手がかりです。 しかし、AIエージェントがルールを読む場面は、実装やレビューのたびに発生します。そのたびに複数のファイルを渡り歩いて文脈を組み立てる必要があると、参照が一段増えるごとに読み落としや誤読の起点が増えてしまいます。 そこで私たちは、「ルールの読者はAIである」という前提に立ち返りました。人間向けの相互参照は、複数の文書をまとめて理解する読み手を助けるための工夫です。AIエージェントに同じ役割を期待する必要はなく、それぞれのルールが単体で完結し、判断に必要な情報がその中でそろうように書き直すことにしました。単体で完結する形にしたことで同じ説明が複数のルール文書に重複することを懸念していました。実際に運用してみると重複は目立って増えておらず、人間のレビュアーがルール文書を読む頻度や体験にも変化はありません。 この転換は書き方を変えるだけでは不十分で、ルールが本当に守られているかを機械的に検証できる形に変換する必要がある、という次の課題につながりました。 自然文のルールからArchUnitの実行可能な検証への変換 前提を転換しただけでは、ルールが実際に守られているかどうかまでは保証できません。次に取り組んだのは、自然文で書いていたルールをArchUnitで実行可能な検証に変換することです。 ArchUnitは、Javaのアーキテクチャ制約をテストコードとして記述し、ビルド時に検証できるライブラリです。パッケージの依存関係やクラスの命名規則、メソッドの呼び出し制約などをDSLとして表現できます。 たとえば、「ドメイン層のコードではリフレクションを使用しない」という規約は、これまでルール文書に自然文で書き、実装時にレビュアーが目視で確認していました。この規約をArchUnitで検証しようとすると、まず思いつくのは、パッケージ単位で禁止する書き方です。 なお、本記事のコード例は、実際にはSpockで実装しているルールをJUnit 5形式に書き直したものです。 import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; @AnalyzeClasses (packages = "com.example.domain" ) class ArchitectureRuleTest { @ArchTest static final ArchRule domain_should_not_use_reflection = noClasses() .should() .dependOnClassesThat() .resideInAPackage( "java.lang.reflect.." ) .because( "リフレクションはドメイン層の意図を分かりにくくするため禁止する" ); } しかし、この書き方には問題がありました。 dependOnClassesThat().resideInAPackage(...) は、対象パッケージのクラスへの依存を広く検出します。メソッド呼び出しだけでなくフィールドや引数の型宣言も依存に含まれるため、リフレクションを直接呼び出していないコードまで違反として検出してしまいます。 私たちのプロジェクトでは、O/Rマッパーが生成するコードの一部が java.lang.reflect.Method 型のフィールドを持っていました。生成コードが呼んでいるのは、内部でリフレクションを使うライブラリのヘルパーメソッドです。生成コード自身がリフレクションAPIを直接呼び出しているわけではありません。しかし、パッケージ指定による型参照チェックでは、この生成コードも違反として検出されてしまいます。 そこで、検出対象を「型参照」ではなく「メソッド呼び出し」に絞った ArchCondition として実装しました。 import java.util.Set; import com.tngtech.archunit.core.domain.JavaCodeUnit; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.SimpleConditionEvent; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.codeUnits; @AnalyzeClasses (packages = "com.example.domain" ) class ReflectionRuleTest { private static final Set<String> CLASS_LOOKUP_METHODS = Set.of( "forName" , "newInstance" , "getMethod" , "getDeclaredMethod" , "getMethods" , "getDeclaredMethods" , "getField" , "getDeclaredField" , "getFields" , "getDeclaredFields" , "getConstructor" , "getDeclaredConstructor" , "getConstructors" , "getDeclaredConstructors" ); @ArchTest static final ArchRule domain_should_not_call_reflection = codeUnits().should( new ArchCondition<JavaCodeUnit>( "not call reflection / dynamic-dispatch APIs" ) { @Override public void check(JavaCodeUnit codeUnit, ConditionEvents events) { codeUnit.getMethodCallsFromSelf().forEach(call -> { var target = call.getTarget(); var ownerName = target.getOwner().getFullName(); var ownerPackage = target.getOwner().getPackageName(); var name = target.getName(); boolean violation = (ownerName.equals( "java.lang.Class" ) && CLASS_LOOKUP_METHODS.contains(name)) || ownerPackage.equals( "java.lang.reflect" ) || ownerPackage.equals( "java.lang.invoke" ); if (violation) { events.add(SimpleConditionEvent.violated( codeUnit, codeUnit.getFullName() + " calls " + ownerName + "#" + name)); } }); } }); } このテストでは、コードが実際に呼び出したメソッドの呼び出し先(オーナーの型とメソッド名)を1件ずつ確認します。 java.lang.reflect パッケージや java.lang.invoke パッケージへの呼び出しは検出対象です。加えて、 java.lang.Class が持つ forName や getDeclaredMethod のような動的なメソッド探索・生成系のメソッド呼び出しも検出対象にしています。 java.lang.Class 自体は java.lang パッケージに属しており、 java.lang.reflect パッケージの外にあります。そのためパッケージ名だけでは判定できず、対象にしたいメソッド名を CLASS_LOOKUP_METHODS として自分たちで列挙しています。これはArchUnitが提供するAPIではなく、プロジェクト側で定義した定数です。型を参照しているだけのコードは対象にならないため、生成コードを誤検出することもありません。 検出範囲もメソッド本体だけでなく、コンストラクタやフィールドの初期化子まで含めた全コードユニット( codeUnits() )にしています。これによって、レビュアーが目視で確認していた範囲を、CIで機械的に検証できるようになりました。 命名規則やレイヤー間の依存関係も同様に、目視確認からArchUnitでの検証へ順次置き換えていきました。自然文のルールをすべて機械的に検証できるわけではありません。ただし、パッケージ構造やクラス間の関係など、構造的に表現できる制約は、この方法でカバーできます。 実際に機械的な検証へ変換したルールには、次のようなものがあります。 日時を扱うクラスの now() メソッドを、 Clock 引数を指定せずに直接呼び出すことを禁止するルール(テストで時刻を固定できるようにするための制約) 特定のインタフェースを実装したrecordのcompact constructorに Objects.requireNonNull 呼び出しを必須にするルール(nullチェックの実装漏れを防ぐ制約) UseCase層のクラスが持つ特定のメソッドに、 @Transactional アノテーションを必須にするルール(トランザクション境界の付け忘れを防ぐための制約) これらはいずれも、以前は自然文のルール文書とレビューでの目視確認に頼っていたものです。 ルール文書・テスト・実コードの三者整合性を担保する方法 ルールをArchUnitのテストに変換しても、それだけでは安心できません。ルール文書とテストコード、そして実コードの3つは、別々のファイルに書かれているため、時間が経つとずれていく可能性があります。 実際に私たちのルール文書にある例を紹介します。あるUseCaseクラスの設計規約では、公開メソッドに @Transactional を必須にし、 isolation 属性はデフォルトのまま変更しないことを原則としています。ただし、バッチ処理で定期的に更新されるデータを参照する1つのUseCaseだけ、例外として REPEATABLE_READ を指定してよいことになっています。 // OK: 分離レベルを上げる場合は理由をコメントで明示する(唯一の例外) @Transactional (isolation = Isolation.REPEATABLE_READ) // バッチで定期的に更新されるデータを参照するため、整合性を保つ public ResultDTO process(SomeCommand command) { /* ... */ } この例外は、ルール文書に書くだけでは終わりません。ArchUnitのテストコード側にも、この特定のクラスを検査対象から除外する条件を書く必要があります。 import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.SimpleConditionEvent; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; @AnalyzeClasses (packages = "com.example.usecase" ) class UseCaseTransactionalRuleTest { @ArchTest static final ArchRule isolation_should_be_default = methods() .that().areDeclaredInClassesThat().resideInAPackage( "com.example.usecase.." ) .and().areDeclaredInClassesThat().areAnnotatedWith(Service. class ) .and().areDeclaredInClassesThat().doNotHaveFullyQualifiedName(SampleUseCase. class .getName()) .and().areAnnotatedWith(Transactional. class ) .should( new ArchCondition<JavaMethod>( "have the default isolation level" ) { @Override public void check(JavaMethod method, ConditionEvents events) { var isolation = method.getAnnotationOfType(Transactional. class ).isolation(); if (isolation != Isolation.DEFAULT) { events.add(SimpleConditionEvent.violated( method, method.getFullName() + " has isolation " + isolation)); } } }); } ルール文書の例外規定と、テストコードの doNotHaveFullyQualifiedName(...) は、別々のファイルに書かれた対応関係です。この対応が崩れると、新しく追加した例外が検査対象のままになって意図せずテストが失敗したり、逆に例外ではないクラスが誤って除外されたままになったりします。私たちのルール文書には、「新たに例外が必要になった場合は、テストコードの除外条件も合わせて更新する」という注意書きを直接添えています。これによって、ルール文書とテストコードの対応関係を明示しています。ただし、この注意書き自体も、人が読んで実行することに変わりはありません。例外の宣言をコード側に持たせ、テストがそれを直接参照する形にすれば、注意書きなしでも対応関係を保てます。この点は今後改善する余地として残っています。 もう1つの工夫は、ルール文書自体に「検査の保証範囲」を明記することです。たとえば、先ほどのUseCaseの設計規約には、次のような記載があります。 機械的チェックの現状: process への @Transactional 必須とisolation制約は、ArchUnitのテストで既に機械的に検査されている。一方、メソッドの公開範囲や引数の形に関する規約はまだ自動検査されておらず、レビューでの目視確認に依存する。 この記載があることで、AIエージェントも人間のレビュアーもルール文書を読むだけで、どこまでが保証されていて、どこからが目視確認頼みかを判断できます。 こうした工夫に加えて、ルールを新しく追加したときには、AIエージェントに三者を突き合わせて確認してもらうこともあります。対象は、ルール文書とArchUnitのテストコード、実コードです。矛盾点や、ルール文書の説明と実装の食い違い、目視確認に頼っている部分の取りこぼしがないかを検証してもらう狙いです。 アドホックな知識からルールへの体系的な移行 ルールをArchUnitで検証する仕組みや、三者の整合性を保つ運用を整えても、その対象になるルール自体がどこにあるかが分かりにくいという問題が残っていました。 私たちのプロジェクトには、設計判断の理由や実装パターンをまとめた知識ベースがありました。この知識ベースには、「なぜこの設計を選んだか」という背景説明と、「必ず守るべき制約」が同じ文書に混在していました。背景説明は読み手の理解を助けるものであり、検証の対象にはなりません。一方、制約は本来、検証の対象になり得るものです。両者が同じ文書に混ざっていると、どの記述がArchUnitで検証すべき対象なのかが分かりません。 そこで、知識ベースと制約を別々のディレクトリに分けました。背景説明や実装パターンは .claude/knowledge/ に置き、必ず守るべき制約は .claude/rules/ へ配置しました。なお、 .claude/rules/ は現在Claude Codeが標準で読み込むディレクトリですが、 .claude/knowledge/ はこのプロジェクト独自の配置です。標準的な文書配置がまだ定まっていなかった時期に、リポジトリの直下には置きたくないという理由で .claude/ 配下に作ったものです。 たとえば、アーキテクチャに関する文書は、同じ architecture.md という名前で両方のディレクトリに存在します。 .claude/knowledge/architecture.md には、オニオンアーキテクチャを採用した理由や各層の実装パターンといった、背景の説明が書かれています。 .claude/rules/architecture.md には、Controller・UseCaseをファットにしないための具体的な閾値や、NG・OKのコード例が書かれています。これらは、コード生成時に従うべき強制基準だけに絞られています。後者の冒頭には、次のような記載があります。 各層の責務概要・実装パターン・選択理由は .claude/knowledge/architecture.md に記載してあり、本ファイルはその知識を前提とした上で「コード生成時に従う強制基準」を定義する。 この参照は、ルールを単体で完結させるという前提の転換と矛盾しているように見えます。しかし、コード生成時に守るべき強制基準は .claude/rules/architecture.md 側で完結しています。 .claude/knowledge/architecture.md を読まなくても遵守の判断はできます。knowledge側は、なぜその基準になったかという任意の背景情報であり、参照しなくても強制基準の適用に支障はありません。 .claude/knowledge/ は .claude/rules/ と異なり無条件では読み込まれず、CLAUDE.mdの案内に沿って必要な場面ごとに参照先が示される配置です。そのため、AIエージェントが両者を区別せずまとめて読み込んでしまう事態は今のところ起きていません。 この整理によって、「検証すべき制約の一覧」がルール文書側にまとまりました。新しいルールを追加するときも、書く場所を選ぶ時点で性質を判断するようになりました。単なる背景知識なのか、AIエージェントに守らせたいルールなのか、それとも機械的に検証できる制約なのか、という観点です。 複数リポジトリへのルール展開の検討と見送り 1つのリポジトリでルールとArchUnitによる検証の仕組みが定着したところで、次に考えたのは、他のリポジトリでも同じ仕組みを使えるようにすることでした。命名規約やアーキテクチャの制約には、プロジェクトが違っても通用する部分があります。1つのリポジトリで整備したルールを他のプロジェクトでもそのまま使えれば、同じルールをゼロから作り直す手間を省けます。 そこで、ルールをマーケットプレイス形式で管理し複数のリポジトリへ配布する仕組みと、配布したルールを管理するツールを用意する計画を立てました。ここでの「マーケットプレイス形式」は、既存のプラグイン配布基盤を指すものではなく、複数のリポジトリへルールを公開・取得できるようにする、自前の配布基盤のことを指しています。 しかし、計画を進める中で見えてきたのはツールの設計上の課題ではなく、運用面の課題でした。ルールを共有する仕組み自体は用意できます。ただし、それぞれのリポジトリを担当するメンバーが、他のリポジトリで整備されたルールを積極的に取り込むかどうかは別の問題です。実際には、他のリポジトリのルールを取り込む動きはほとんど生まれませんでした。 取り込みが進まなかった背景には、大きく2つの理由があると考えています。1つは、リポジトリをまたいで本当に共有できるルールが想定していたほど多くなかったことです。プロジェクト固有の事情に依存するルールが大半で、汎用的に使い回せる部分は一部に留まりました。もう1つは、ルールを取り込むこと自体が、前述したルール文書・テスト・実コードの整合性を確認する運用という新たな運用コストを増やしてしまうことです。既存のルールをそのまま使うのではなく、自分たちのリポジトリの実装に合わせて調整し、整合性を保ち続ける必要があり、その手間が取り込みのハードルになっていました。なお、この2つの理由は、実際の取り込み状況から私たちが振り返って推測したものです。 この結果を受けてマーケットプレイスと管理ツールの計画は取りやめました。仕組みを作ることが目的化してしまうと、実際には使われない仕組みを維持するコストだけが残ってしまいます。需要が確認できていない段階で大掛かりな仕組みを作るより、まずは個別のリポジトリでルールと機械的な検証の仕組みを定着させることを優先する判断です。 この判断を見直す条件があるとすれば、共有できるルールの数がたまたま増えることではなく、共通化に必要な材料がそろうことだと考えています。ルールを先に一般化してから展開するトップダウンの手順は、複数チームでの実例が積み重なっていない段階では成立しにくいというのが、今回の見送りから得た実感です。各チームがルールの背景を記録しつつ、記録した内容をチーム横断で比較する仕組みと運用が必要です。ルール文書・テスト・実コードの三者突き合わせと同様に、この比較もAIエージェントに任せられる見通しが立った時点で、改めて展開を検討したいと考えています。 検証の必須ゲート化 ArchUnitのテストを書いても、実行される保証がなければ意味がありません。テストコードとして存在していても、実行タイミングが曖昧だと気づかないうちに検証が素通りしてしまうことがあります。 私たちのプロジェクトには、SpockベースのArchUnitルールとは別の検証ルールもあります。たとえば、参照型を返すメソッドに @Nonnull ・ @Nullable のいずれかを必須にするルールなどがあります。このルールは check タスクに組み込まれていましたが、 test タスクには組み込まれていませんでした。これは意図的な設計ではありませんでした。Gradleの標準的なタスク依存構造( check が test に依存する一方、逆方向の依存はない)上、静的な検証系のタスクが慣習的に check 側へ接続されることによるものでした。この違いに気づかないまま ./gradlew test だけをローカルで実行してプッシュしたところ、 check 側のルール違反がローカルでは検出されず、CIで初めて検出される事態が起きました。テストが通ったことを確認しても、 check の検証は素通りしていたということです。 そこで、プッシュ前に実行すべきコマンドを ./gradlew check に統一しました。 check タスクはSpotlessによるフォーマットチェック・全テスト・ルールの検証をまとめて実行するため、 test だけを実行して安心してしまう事態を防げます。この方針はルール文書側にも明文化しています。 あわせて、CI側でもプルリクエストごとに検証しています。ただし、CIでは check タスクをそのまま呼ぶのではなく、構成要素を複数のジョブに分けて並列実行しています。DBを使うテストと使わないテストを別ジョブに分けてDBコンテナの要否を切り分け、フォーマットは「チェックして落とす」のではなく「自動修正してコミットする」別ジョブにしているためです。ルールを検証するジョブは、GitHubのブランチ保護ルールで必須ステータスチェックに指定されています。ルールの検証に失敗すると、そのプルリクエストはマージできません。ローカルでの実行規律だけでなく、CIでも強制することで、実行を忘れたまま気づかずマージしてしまう事態を防いでいます。 新しいルールを追加するときは、検証が実際に機能しているかどうかも確認します。たとえば、ドメイン層のパッケージ配置に関するルールを追加したときは、意図的にルールに違反する配置のダミークラスを用意し、検証が失敗(FAILED)することを確認しています。ルールを追加した時点で既存のコードに違反がないかどうかも確認し、違反がゼロであることを確かめてからマージしています。 こうして、ArchUnitなどを用いた検証は、書いただけのテストコードではなく、プッシュ前に必ず通過するゲートとして機能するようになりました。 まとめ 本記事では、AIエージェント向けにコーディングルールを書き、ArchUnitで機械的に検証する取り組みを紹介しました。 AIエージェントは、指示すればすぐに多くのコードを書いてくれます。しかし、その分だけルールに違反したコードが生まれる機会も増えます。レビューだけでこれを検出しようとすると、コードが増えるほどレビュアーの負担も増えていき、いずれ追いつかなくなります。ArchUnitでルールを実行可能な形にしておけば、検証の速度をコードが生成される速度に合わせられます。 この検証の仕組みを維持する作業にも同じ考え方が当てはまります。ルール文書とテストコード、実コードの整合性を確認する作業を人手だけで追いかけようとすると、ルールが増えるほど負担が増えていきます。そこで、この確認作業もAIエージェントに依頼しています。ルールを守る対象であるAIエージェントに、ルールを整備する側も手伝ってもらう、という体制です。 ルールの読者はAIであるという前提に立ち返ったことは、書き方を変えるだけの話ではありませんでした。ルールを「読んで理解してもらうもの」から「実行して確認できるもの」に位置づけを変える、という判断につながりました。AIエージェント向けにコーディングルールを整備することを検討している方がいれば、ぜひ参考にしてみてください。今後は、まだレビューでの目視確認に依存している規約も対象に含め、ArchUnitに限らずさまざまな手段で機械的に検証できる範囲を広げていきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
「だいたい○○ケースくらいですかね」の正体 テスト計画でスケジュールを立てるとき、テストケース数の見積もりはどうやっているだろうか。 正直なところ、多くの現場では「前回と同じくらいの規模感だから○○ケースくらい」「経験的にこの手の機能なら△△ケース」という見積もりが主流だと思う。自分もそうだった。 これで困るのは3つの場面。 初めての領域。 過去実績がないから「だいたい」が通用しない 説明責任。 上席に「なぜその工数なのか」と聞かれたとき、根拠を示せない 精度のばらつき。 見積もる人によって2〜3倍の差が出る 結局、見積もりの「勘と経験」の中身を分解してみたら、ちゃんと数式















