ZeitwerkのEager LoadからActiveRecord統合を切り離す

crmne/ruby_llm

ruby_llmのコアgemがZeitwerk::Loader.eager_load_allを呼ばれた際にActiveRecord統合コードを読み込んでしまうバグが修正されました。lib/ruby_llm/active_recordディレクトリをZeitwerkのignore対象とし、ロード責任をRailtieに明示的に委譲することで、スタンドアロン環境とRails環境の境界を明確にしています。

背景

lib/ruby_llm/active_recordはオプショナルなRails統合コードであるにもかかわらず、gemレベルのZeitwerkローダーの管理下に置かれていたため、Rails未起動環境でのeager loadが意図しない動作を引き起こしていました。

具体的には、bundle exec appraisal rails-7.2 ruby -e "require 'ruby_llm'; Zeitwerk::Loader.eager_load_all"を実行した際に、ActiveRecordがロードされていない状態でActiveRecord統合ファイルが読み込まれてしまいました。その結果、RubyLLM::ActiveRecord::ModelMethodsがActiveSupportのdelegateを使用しているにもかかわらず、そのrequireが済んでいないためNoMethodError: undefined method 'delegate' for module RubyLLM::ActiveRecord::ModelMethodsが発生していました。

直接的な原因は単一のrequire漏れでしたが、本質的な問題はActiveRecordディレクトリ全体がgemのZeitwerkオートロード対象に含まれていたことです。lib/ruby_llm.rbではtasksgeneratorsrailtie.rbはすでにignore指定されており、active_recordディレクトリだけが例外的に漏れていた状態でした。

技術的な変更

この修正は4つの部分から構成されており、ロード境界の再定義とその副作用への対処を段階的に行っています。

lib/ruby_llm/active_recordをZeitwerkのignore対象に追加

lib/ruby_llm.rbに1行追加することで、gemレベルのZeitwerkローダーがActiveRecordディレクトリを管理しないようにしました。

loader.ignore("#{__dir__}/tasks")
loader.ignore("#{__dir__}/generators")
loader.ignore("#{__dir__}/ruby_llm/active_record")  # 追加
loader.ignore("#{__dir__}/ruby_llm/railtie.rb")
loader.setup

lib/ruby_llm.rbからのActiveRecord直接requireを削除

変更前はRails::Railtieが定義されている場合にrailiteとacts_asの両方をrequireしていましたが、後者を削除してRailtieのみをrequireするように変更しました。

変更前:

if defined?(Rails::Railtie)
  require 'ruby_llm/railtie'
  require 'ruby_llm/active_record/acts_as'
end

変更後:

require 'ruby_llm/railtie' if defined?(Rails::Railtie)

③ RailtieがActiveRecord統合のロードを明示的に担当

lib/ruby_llm/railtie.rbActiveSupport.on_load :active_recordブロック内で、ActiveRecord統合ファイルを明示的にrequireするよう変更しました。これにより、ActiveRecordがロードされるタイミングで必要なファイルが確実に読み込まれます。

initializer 'ruby_llm.active_record' do
  ActiveSupport.on_load :active_record do
    require 'ruby_llm/active_record/payload_helpers'
    require 'ruby_llm/active_record/chat_methods'
    require 'ruby_llm/active_record/message_methods'
    require 'ruby_llm/active_record/model_methods'
    require 'ruby_llm/active_record/tool_call_methods'

    if RubyLLM.config.use_new_acts_as
      require 'ruby_llm/active_record/acts_as'
      ::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAs
    # ...
    end
  end
end

④ 各ActiveRecordファイルに明示的なrequireを追加

Zeitwerkのオートロードに依存していた暗黙の依存関係を解消するため、各ファイルに必要なrequireを追加しました。Zeitwerk ignoreによってオートロードフォールバックが無効になるため、各ファイルが独立してロード可能である必要があります。

ファイル 追加されたrequire
acts_as.rb active_support/concern, active_support/inflector
acts_as_legacy.rb active_support/concern, active_support/inflector
chat_methods.rb active_support/concern
message_methods.rb active_support/concern, ruby_llm/active_record/payload_helpers
model_methods.rb active_support/concern, active_support/core_ext/module/delegation
payload_helpers.rb active_support/core_ext/object/blank, json
tool_call_methods.rb active_support/concern, ruby_llm/active_record/payload_helpers

設計判断

「Zeitwerk ignoreによるロード境界の明示」という設計方針が採用されました。

代替案として、各ファイルへのrequire追加だけで対処することも考えられましたが、それではZeitwerkの管理下に置きながらrequireを手動で補完するという矛盾した状態が続きます。このPRでは、オプショナルな統合コードをZeitwerkの管理外に置くという明確な境界を設け、ロードの責任をRailtieに一元化しています。

この判断は既存のコードとも一貫しています。tasksgeneratorsrailtie.rbがすでにignore指定されているのと同じ扱いをActiveRecordディレクトリに適用したに過ぎず、「コアgemのオートロード対象外のものはignoreする」というルールが統一されました。

CIマトリクスにeager-loadガードが追加されたことも重要な設計判断です。bundle exec appraisal ${{ matrix.rails-version }} ruby -e "require 'ruby_llm'; Zeitwerk::Loader.eager_load_all"を各Railsバージョンで実行することで、将来のリグレッションをCI段階で検出できるようになりました。

まとめ

本PRは、Zeitwerkのignore1行と明示的なrequireの追加という最小限の変更で、オプショナルなRails統合コードのロード境界を正しく再定義しています。スタンドアロンeager loadの安全性を担保しつつ、RailsアプリでのActiveRecord統合の動作は変更していないため、既存ユーザーへの影響を抑えながらgemのロードセマンティクスを堅牢にした変更といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
95309f72

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という「総論→各論→結論」の構成が記事全体で明確に適用されており、非常に分かりやすいです。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトやGitHubのPRリンク記法がガイドラインに沿って正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Zeitwerk、Railtie、eager loadといった専門用語が適切に使用されており、専門知識を持つエンジニアという対象読者に適合した内容となっています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論・各論・結論の構造を持ち、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されています。これにより、記事の論理構造が明快になっています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックや変更点の解説は、提供されたDiff情報と正確に一致しています。特に、変更点を複数のカテゴリに分けて解説する構成が、Diffの意図を理解する助けになっています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Zeitwerk、Railtie、ActiveSupport.on_loadなど、関連する技術用語が文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

変更の背景にある問題(意図しないeager load)から解決策(Zeitwerk ignoreと明示的なrequire)に至るまで、技術的な説明はPR情報と一致しており、論理的で正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(問題の原因、具体的なエラー、設計判断など)は、提供されたPRのDescriptionやDiff内容によって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#504)やコード内のクラス名・メソッド名などの固有名詞が正確に使用されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「ZeitwerkのEager LoadからActiveRecord統合を切り離す」は、PRの核心的な変更内容を的確に要約しており、PRの趣旨と完全に一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容は提供されたPR情報に限定されており、バージョンのサポート状況など、PRに記載のない外部知識の追記は見られませんでした。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「すでにignore指定されており」といった時間表現が、PR Description内の「existing treatment」という記述と正確に対応しており、時間的な前後関係が正しく記述されています。