コンテンツにスキップ

Cache slice taxonomy — pre-v0.1.0 design notes

ステータス: 草案。規範的ではありません。v0.1.0の永続的なオンディスクキャッシュ(ROADMAP.mdおよびADR-2 §「登録、設定、キャッシュ」を参照)が準拠すべきキャッシュディスクリプタスキーマを記述します。純粋な設計出力であり、このドキュメントからコード変更は発生しません。このドラフトの後継は、スキーマが実装された時点でdocs/internal-spec/内の規範的仕様となります。

この作業は、v0.0.7のReflectionファサード移行(docs/internal-spec/reflection.md)に続くpre-v0.1.0サブスライスです(20260505-v0.1.0-readiness.mdにてシーケンシングを参照)。この設計から生まれる永続化レイヤーはv0.0.xには含まれません——プラグインAPIとともにv0.1.0でリリースされます。

このドラフトがADR-2と矛盾する場合、ADRが拘束力を持ち、ドラフトは古くなっています。

すべての解析器内部プロデューサーがキャッシュする値に無効化ディスクリプタを付与するための、単一の型付きキャッシュキースキーマを定義します。スキーマは以下を満たさなければなりません。

  1. ウォームなrigor checkの実行で、入力が変更されていない処理をスキップできるようにする。
  2. 複数のプロデューサーが同一の値に寄与する場合に確定的にコンポーズできる(例: あるメソッドの解決済み型がRBSシグネチャ + ソース内defの形状 + プラグインの動的メンバーファクトを使用する場合)。
  3. 「1つのソースファイルを編集した」「1つのgemバージョンを上げた」という操作が無効化するスライスを可能な限り狭く保つ——キャッシュ全体を決して無効化しない。
  4. 既存のコンシューマーを壊さずに拡張できる: 新しいスロットの追加はアディティブな変更であり、スロットの名前変更や削除にはスキーマバージョン移行が必要。
  • 永続化ストレージのレイアウト。オンディスクフォーマット(sqlite、msgpack、単一ファイルフラット、シャードディレクトリなど)はこのドラフトの対象外です。以下のスキーマはストレージに依存しません。
  • ホットリロード。v0.1.0のキャッシュは実行間のみです。長期実行デーモン / LSPは将来の対象です。
  • クロスマシンキャッシュ共有。スキーマ内のパスはすべてプロジェクト相対です。あるマシンで構築したキャッシュは同じプロジェクト / gem状態の別マシンで利用可能ですが、配布セマンティクスはこのドラフトの対象外です。

ADR-2 §「登録、設定、キャッシュ」では、すでに4スロット構造が固定されています。

キャッシュ依存関係は、事後的な任意読み取りリストではなく、明示的なディスクリプタであるべきです。最初の実装では、固定スロットセットとエントリーごとの比較器(comparator)を持つ型付きスロットスキーマを使用します(フラットなkindタグ付きエントリーリストではなく):

  • files: プロジェクトまたは外部ファイルの入力。各エントリーはパスとダイジェストまたはmtimeポリシーを持ちます。
  • gems: gem名とバージョン制約またはピン留めバージョン。
  • plugins: プラグイン識別子とピン留めプラグインgemバージョン。
  • configs: 設定キーとその受け入れ値のハッシュ。機能フラグの切り替えが依存するスライスのみを無効化できます。

この設計ドキュメントは、ADR-2が実装に委ねた各スロットの形状の詳細とコンポジション / 無効化セマンティクスを補完します。

キャッシュディスクリプタは4つのスロットを持つRigor::Cache::Descriptor値オブジェクトです。各スロットは型付きエントリーの配列であり、空の配列は「このスロットに依存関係なし」を意味します。

各エントリーは1つのファイル入力——プロジェクトソース、生成済みシグネチャ、ベンダー化RBS、スキーマ、フィクスチャなど、プロデューサーがキャッシュ値を構築する際に読んだもの——をピン留めします。

FileEntry := { path: String, comparator: Comparator, value: String }
Comparator := :digest | :mtime | :exists
  • pathはファイルがプロジェクトルート配下の場合はプロジェクト相対パス、それ以外は絶対パス。シンボリックリンクはハッシュ前にターゲットに解決されます。
  • comparator: :digestはファイル内容のSHA-256を持ちます。ソースファイルのデフォルト;マシン間で決定的です。
  • comparator: :mtimeはISO-8601文字列としてファイルのmtimeを持ちます。ダイジェスト計算よりチェックが安価です;ダイジストコストが支配的な大きなベンダー化RBS / 生成アーティファクトに適しています。プロデューサーが選択し、キャッシュレイヤーは両方を均等に扱います。
  • comparator: :exists"yes"または"no"を持ちます。値の内容ではなく存在に依存する場合(例: オプショナルなsig/local.rbs)に使用します。

プロジェクトファイルのFileEntryはプロジェクト相対パスを使用しなければなりません(MUST)。1つのプロジェクトディレクトリに対して構築されたキャッシュディスクリプタは、そのプロジェクトのルートに対してのみ有効です。

各エントリーはBundler / RubyGemsの依存関係をピン留めします。

GemEntry := { name: String, requirement: String, locked: String? }
  • nameはgem名です(例: "rbs""prism")。
  • requirementはGemfile / gemspecに記述されたアクティブなバージョン制約です(">= 4.0""~> 0.20")。プロデューサーがロックバージョンのみを気にする場合はrequirement: "*"を設定します。
  • lockedGemfile.lockから解決されたバージョンです("4.0.3")。nilの場合、エントリーはrequirementを満たす任意のバージョンにマッチします。

2つのGemEntryの値は、nameが一致し、かつ(requirement, locked)が一致する場合に等しくなります。lockedがnullの場合、requirementを満たす任意のロック値にマッチします;これによりプロデューサーはすべてのパッチバンプで再無効化されることなくバージョン範囲に依存できます。

各エントリーはプラグインgemと、プロデューサーを制御するユーザー提供のプラグイン設定をピン留めします。

PluginEntry := { id: String, version: String, config_hash: String? }
  • idはプラグイン識別子です(ADR-2 §「プラグイン診断由来」が予約する名前空間plugin.<plugin-id>にマッチします)。
  • versionは解決済みのプラグインgemバージョンです。必須——プラグインのコードが変わった場合はプラグイン由来のファクトを再計算しなければなりません(MUST)。
  • config_hashはプラグインの正規化された設定(ソート済みキー、JSON直列化された値)のSHA-256です。2つのプラグインエントリーが設定のみで異なる場合、:strict_modeに依存するキャッシュスライスは:include_pathsに依存するスライスとは独立して無効化されます。

各エントリーはプロデューサーに影響した単一のRigor設定キーをピン留めします。

ConfigEntry := { key: String, value_hash: String }
  • keyはドット区切りの設定パスです(例: "target_ruby""fold_platform_specific_paths""plugins.rails.eager_load_paths")。
  • value_hashはJSON直列化された設定値のSHA-256です。ハッシュ化によって、設定値が大きなハッシュや配列であってもディスクリプタのペイロードを小さく保てます。

キャッシュされた値のディスクリプタは、それを生成するために寄与したすべての依存関係ディスクリプタの和集合です。同じ(スロット,キー)に寄与する2つのプロデューサーは比較器と値について一致しなければなりません(MUST);競合する寄与はキャッシュ整合性エラーであり、キャッシュレイヤーはサイレントに選択するのではなく診断を表出します。

スロット別のコンポジションルール:

  • files: pathによる和集合。2つのプロデューサーが異なる比較器で同じパスのFileEntryを付与する場合、より厳格な比較器が勝ちます(digestがmtimeに勝ち、mtimeがexistsに勝ちます)。より厳格な比較器下で値が一致しない場合、キャッシュスライスは無効化されます。
  • gems: nameによる和集合。(requirement, locked)の競合はスライスを無効化します。
  • plugins: idによる和集合。(version, config_hash)の競合はスライスを無効化します。
  • configs: keyによる和集合。value_hashの競合はスライスを無効化します。

プロデューサーは自身のディスクリプタに同じ(スロット,キー)を2回追加してはなりません(MUST NOT)——重複検出はプロデューサー側のバグであり、キャッシュレイヤーの関心事ではありません。

格納された値のキャッシュキーは、以下の安定したシリアライゼーションに対するSHA-256です。

  1. スキーマバージョン(破壊的なスキーマ変更時にインクリメントされるRigor側の整数)。
  2. プロデューサーの識別子(例: "reflection.method_definition""plugin.rails.dynamic_attributes")。
  3. プロデューサーの入力パラメータ(プロデューサーが呼び出された引数——メソッド定義キャッシュの場合は(class_name, method_name, kind)タプル)。
  4. コンポーズされたDescriptor(スロットはソート済み、エントリーはキーでソート済み、ハッシュは小文字16進数)。

シリアライゼーションは正規でなければなりません(MUST): ソート済みキー、空白なし、固定エンコーディング(UTF-8 NFC)。異なるコードパスで構築された2つの等価なディスクリプタは同一のキャッシュキーを生成しなければなりません(MUST)。

各キャッシュプロデューサー(組み込みまたはプラグイン)はMUST:

  • キャッシュする値ごとにDescriptorを構築し、値を計算する際に読んだすべての入力に名前を付ける。
  • 宣言済みスキーマ外のソースを含む入力を持つ値をキャッシュしないこと(MUST NOT)。新しい入力次元はスキーマ変更(およびADR-2 §「キャッシュ無効化には宣言的APIが必要」に従うADR修正)が必要です。
  • 値が依存する最も狭いスライスを使用する。class_definition_for("Foo")のリフレクションキャッシュは、プロジェクト内のすべてのファイルではなく、Fooを宣言するソースファイルに依存すべきです。
  • プロデューサー識別子をキャッシュキーを通して公開し、無効化がクラスごとではなくプロデューサーごとになるようにする。

キャッシュ値は、そのDescriptor内のすべてのエントリーについて、エントリーの入力のライブ値がキャッシュされた値と一致する場合に有効です。

  • FileEntry:digest): ライブのSHA-256がキャッシュされた値と等しい。
  • FileEntry:mtime): ライブのmtimeがキャッシュされた値と等しい。
  • FileEntry:exists): ライブの存在がキャッシュされた値と等しい。
  • GemEntry: ライブのrequirementとライブのlockedが一致する。
  • PluginEntry: ライブのプラグインバージョンとconfig-hashが一致する。
  • ConfigEntry: ライブのvalue-hashが一致する。

単一の不一致でもキャッシュ値が無効化されます。キャッシュレイヤーは値を削除してプロデューサーを再実行しなければなりません(MUST);スロットエントリー間での部分的な無効化を試みてはなりません(MUST NOT)。

ADR-2はすべてのプロデューサーに対して実用的な最狭スライスを指定しています。具体的には:

プロデューサーファミリースライスごとのキー依存関係ディスクリプタの例
reflection.class_definition_for(name)namefiles: [<クラスを宣言するファイル>] + gems: [<rbs gem locked>]
reflection.method_definition_for(class, method, kind)(class, method, kind)class_definition_for(class)の依存関係を継承 + RBS側メソッドレコードファイル
inference.user_method_return(def_node)defノードフィンガープリントfiles: [<defのソースファイル>] + 推移的に読まれたすべてのリフレクション / カタログ依存関係
plugin.<id>.<provider>プロバイダ独自のキーfilesgemsplugins: [<id, version, config_hash>]configs: [<読まれたキー>]
catalog.<class>.method_record(name)(class, name)files: [<data/builtins/ruby_core/<class>.yml mtime>]

プラグイン全体の無効化(プラグインの設定全体に依存する)は許容されますが推奨されません。推奨される粒度は動的メンバープロバイダごと、生成済みシグネチャ単位ごと、レシーバーファミリーごと、またはフロー貢献ごとです——ADR-2の「キャッシュ無効化には宣言的APIが必要」に対する作業上の回答を参照してください。

スキーマバージョンはキャッシュキーの一部です。これをインクリメントするとすべてのキャッシュ値が無効化されます。バージョニングルール:

  • アディティブな変更(新しいオプショナルな比較器、古いプロデューサーに対してデフォルトでnilになる新しいスロットエントリー形状): バージョンバンプは不要。古いディスクリプタは読み取り可能のまま。
  • スロットまたは比較器の名前変更または削除: バージョンをバンプする。古いキャッシュは古いスキーママーカーを持つ値の最初の読み取り時にドロップされます。
  • 新しいトップレベルスロットの追加(例: env_varshost): ADR修正 + バージョンバンプ。ADR-2は環境変数がそのような変更になることを明示的に記しています。

Rigor::Reflection.instance_method_definition("Hash", :fetch)はキャッシュされたMethodDefinitionを生成します。そのディスクリプタ:

files: [
{ path: "vendor/bundle/ruby/4.0.0/gems/rbs-4.0.0/core/hash.rbs",
comparator: :digest, value: "<sha256>" }
]
gems: [
{ name: "rbs", requirement: ">= 4", locked: "4.0.0" }
]
plugins: []
configs: []

プロデューサー識別子: "reflection.instance_method_definition"。 入力パラメータ: (class_name: "Hash", method_name: :fetch)

キャッシュキー: (schema_version, producer_id, params, descriptor)に対する正規JSONのSHA-256。

ユーザーがRBSを4.1.0にアップグレードすると、ロックバージョンが変わり、ディスクリプタのgemsエントリーがマッチしなくなり、キャッシュ値がドロップされ、次の解析でRBSを通じてメソッドが再解決されます。無効化されるのはHash#fetchのキャッシュスライスとrbs gemに依存する他のスライスのみ——rbs gemを参照しない他のリフレクションキャッシュはすべて有効のまま残ります。

これらは意図的に実装フェーズに委ねられています;作業上の回答が設計上の契約です。

  1. 同時キャッシュ書き込み。マルチプロセスのrigor check実行が競合する可能性があります。作業上の回答: 単一ライタープロセスによるキーごとの書き込みを持つファイルロックされたsqlite。v0.1.0は単一プロセスモデルでリリース;同時実行性は後で対応。
  2. キャッシュサイズ上限。現在は上限なし。作業上の回答: 設定可能なバイト上限(デフォルトは寛大な上限、例えば256 MiB)を持つLRUで、ほとんどのプロジェクトが到達しないようにします。
  3. プラグインの信頼 + キャッシュポイズニング。悪意のあるプラグインが常に有効と主張するディスクリプタを書く可能性があります。作業上の回答: ADR-2の信頼済みgemトラストモデルが維持されます——プラグインはユーザーのGemfileによって選択され、信頼されたRubyコードです。それ以上のキャッシュ整合性はインプロセスの不変条件であり、セキュリティ境界ではありません。
  4. クロスRuby互換性。Ruby 4.0でビルドされたキャッシュはRuby 4.1では再利用すべきではないでしょう。作業上の回答: スキーマバージョンはRigor側の構造的変更を暗黙的にキャプチャします; Rubyバージョンはプロデューサー識別子サフィックスの一部としてキャッシュキーに入ります。
  5. Gemfile.lockパーサの実装。Bundler側またはハンドロール? 作業上の回答: Bundlerが利用可能な場合はBundlerを使い、スキーマが必要なわずかなフィールド(gem名 + ロックバージョン)に対してはregexパーサにフォールバックします。キャッシュレイヤー自体はBundlerランタイムを必要としません。

設計ドキュメント自体のステータス

Section titled “設計ドキュメント自体のステータス”

これはv0.0.7ワーキングツリーのキャッシュ設計意図のスナップショットです。以下の場合に更新されます。

  • 永続化レイヤーの最初の実装スライスが着地した時——ストレージバックエンド、ロックモデル、退避ポリシー。
  • ADR-2のキャッシュに関する未解決の問題(同時書き込み、サイズ上限、環境変数入力スロット)が規範的なルールに収束した時。
  • リフレクション / 推論 / カタログ / プラグイン以外のプロデューサーファミリーがキャッシュキーサポートを必要とし、新しいスロットやコンポジションルールを公開した時。

永続化レイヤーがv0.1.0でリリースされた後、このドキュメントはdocs/internal-spec/cache.mdの規範的仕様に取って代わられ、歴史的記録としてdocs/design/に残ります。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.