closure_drift について
closure_driftは、Gitのバージョンラベルが異なるコード状態を指していないか調べる読み取り専用のPythonツールです。保守担当者やリリースエージェントが公開前にタグの衝突を検出でき、MCPやCIにも組み込めます。
主な機能
- 01バージョンラベルとファイルクロージャの比較
- 02コミットへの安全なタグ付けの確認
- 03タグ、バージョン情報源、対象範囲の設定
- 04モノレポのコンポーネント別チェック
- 05JSONレポートとCI判定の出力
活用シーン
- 01リリース前のバージョンラベル再利用の確認
- 02CIやpre-commitへのリリースゲート追加
- 03同じラベル下で異なるコードの調査
- 04モノレポのバージョンタグの監査
MCP 対応
MCPサーバーは`would_tag`ツールを公開し、コミットへのタグ付けで既存のバージョンラベルが再利用されるかをエージェントが確認できます。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
docker run -i --rm ghcr.io/luizfnsilva/closure_drift:1.2.1ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。
ghcr.io/luizfnsilva/closure_drift:1.2.1stdioツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
closure_drift
リポジトリ内で v1.4.2 は一つの意味を持っていますか?
$ git clone -q https://github.com/psf/requests && cd requests
$ closure-drift --compare v2.16.0 v2.16.1
A v2.16.0 (commit bcd0e170ac20)
label 2.16.0
closure 3d2886b87c2a7b94 17 file(s)
B v2.16.1 (commit f687e9f3d8b0)
label 2.16.0
closure 696b71ed6023f563 18 file(s)
changed requests/__init__.py
only in B requests/packages.py
==============================================================
DIFFERS UNDER ONE LABEL: both declare 2.16.0, and the code differs
in 2 path(s). If both were published, that label names two things.
このツールを使わずに確認するには、git show v2.16.1:requests/__version__.py を実行すると 2.16.0 と表示されます。closure_drift はパッケージインデックスではなく、git の履歴を読み取ります。タグに含まれる内容を表示するものであり、公開されたパッケージがどのコミットからビルドされたかを示すものではありません。
バージョンとは、人が編集する文字列です。二つのリリースでバージョンが同じなのにコードが異なると、一つのアドレスが二つの成果物を指すことになります。そして、記録されているのはバージョンだけなので、誰もそれに気づきません。
コマンド一つで確認できます。読み取り専用で、ファイルは一つ、依存関係もネットワークも不要です。
実行方法
curl -sO https://raw.githubusercontent.com/luizfnsilva/closure_drift/v1.2.0/closure_drift.py
python3 closure_drift.py # inside any git repository
これはリポジトリを測定します。./reproduce.sh は別の用途です。closure_drift 自体を検証するもので(テストスイートと長時間の実行)、リポジトリは調べません。
または pipx run --spec git+https://github.com/luizfnsilva/[email protected] closure-drift を実行します。
Python 3.9+ と git が必要です。三つの判定結果を先に確認するには、python3 examples/demo.py を実行するか、docs/DEMOS.md をご覧ください。
タグを付ける前に確認する
--would-tag は次の問いに答えます。今このコミットにタグを付けた場合、そのバージョンはすでに異なるコードを指していますか?
# GitHub Actions
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: luizfnsilva/[email protected]
最初のタグを付ける前に、ワークフローをプッシュしてください。そのワークフローが実行されるべきタグと一緒にプッシュすると、ワークフローが開始されない場合があります(その場合は、on: に workflow_dispatch: を指定して手動で実行してください)。mode: measure は、異なるラベルが二つある場合にのみ結論を出します。最初のリリースでは inconclusive となり、終了コードは 2 です。そのため、最初のタグから would-tag で判定してください。
# pre-commit, on git push
- repo: https://github.com/luizfnsilva/closure_drift
rev: v1.2.0
hooks: [{ id: closure-drift-would-tag }]
その他のパイプラインについては、docs/CI.md をご覧ください。
AI エージェント向け
タグ付けと公開を行うエージェントは、Model Context Protocol
(MCP)サーバーを通じて、標準ライブラリのみを使用する would_tag ツールで、まず同じ問いを確認できます。
claude mcp add closure-drift -- uvx --from git+https://github.com/luizfnsilva/closure_drift closure-drift-mcp
Codex CLI、Gemini CLI、Cursor、VS Code、Windsurf、および AGENTS.md、CLAUDE.md、.cursorrules に貼り付けるルールについては、integrations/ をご覧ください。サーバーはこの確認を利用可能にしますが、エージェントはそれを呼び出し、回答に従う必要があります。防止策は CI です。
判定結果
| 判定 | 終了コード | |
|---|---|---|
clean | 0 | すべてのラベルが一つのクロージャを指しており、比較したポイントのうち少なくとも同数以上で成立 |
drift | 1 | 一つのラベルが複数のクロージャを指している |
| その他の判定(一覧) | 2 | 判断するには情報が足りません。理由はレポートに記載されています |
| 拒否 | 2 | 原因は stderr に出力されます |
終了コード 0 は clean の場合に限られます。試した入力のどれもトレースバックを生成せず、失敗時に終了コード 1 となることはありません。
- ラベル — 各タグでプロジェクトが宣言するバージョン。ビルドファイル内、ビルドファイルが指すモジュール内、またはバージョンがタグから導出される場合はタグ自体に記載されています
- クロージャ — 出力を決定するファイルの
(path, git object id)を対象とした SHA-256
レポートには、比較できなかったタグの数も記載されます。
オプション
--would-tag | このコミットにタグを付けた場合、ラベルが再利用されますか? |
--tags 'py-*' | これらのタグのみが公開ポイントです(モノレポ) |
--at commits | すべてのコミットで公開します |
--closure 'src/**' | 出力を決定するファイル |
--strict | すべてのポイントが比較された場合に限り clean |
--explain LABEL | drift にあるラベルについて、スキャンで見つかったそのラベルのすべてのタグを --compare したとき、どのパスが異なるか |
--compare A B | 2つのタグを並べて比較 |
--version-file、--version-regex | ラベルの場所 |
--published FILE | 公開したバージョンのタグのみを対象にします。1行に1つ記したローカルのリストです。--tags は名前で選択します。一致するタグがリリースされていない場合は、こちらを使用してください |
--label-equality version | 1.0、1.0.0、v1 は同じラベルです |
--modes | ファイルのモード(実行可能ビット、シンボリックリンク)をクロージャーに含めます |
--component NAME | モノレポの1つのコンポーネント(下記) |
--json、--badge、--diagnose | レポート(仕様)、README バッジ、バグ報告ブロック |
設定は .closure-drift.json にコミットできます。components がない場合、フラグは設定を上書きします。components がある場合は --component が必要です。壊れたファイルは拒否されます。
モノレポではコンポーネントを宣言し、コンポーネントごとに判定結果が出ます。
{"components": {
"python": {"tags": ["py-*"], "version_file": "py/pyproject.toml", "closure": ["py/**"]},
"rust": {"tags": ["rs-*"], "version_file": "rs/Cargo.toml", "closure": ["rs/**"]}
}}
いずれかのコンポーネントが drift なら結果は drift、すべてが clean なら clean、それ以外は incomplete です。コンポーネントは宣言されるものであり、推測されるものではありません。このようなリポジトリでゲートを設定するには、--would-tag に --component NAME を渡します。
調査結果
公開リポジトリを持つ、PyPI のダウンロード数上位100プロジェクトを、デフォルト設定で調査しました。リリースと同じレポートを出力する 0.10.0 のビルドで測定しています。ルールと手法は最初の実行前に固定し、リポジトリごとの調整は行っていません(全表とすべての衝突)。
| リポジトリ数 | |
|---|---|
clean | 59 |
drift | 29 |
incomplete — 比較したタグ数より、比較していないタグ数の方が多い | 4 |
| バージョンラベルが見つからない、または判定不能 | 8 |
バージョンラベルが異なる2つのコード状態を示していたのは、判定済み88プロジェクトのうち29、全100プロジェクトのうち29でした。87個のラベルは collisions.tsv に記録されており、関係するタグごとに1行ずつ、手作業でも再現できます。
$ closure-drift --compare v2.16.0 v2.16.1 # in psf/requests
DIFFERS UNDER ONE LABEL: both declare 2.16.0, and the code differs in 2 path(s).
衝突はプロジェクトの評価を示すものではありません。よくある原因は、バージョンを上げずに作成されたタグ、7.x のようなブランチマーカー、モノレポ内でのタグ群です。リポジトリに保存された初回実行では、HEAD で選んだ1つのファイルからバージョンを読み取っていたため、100プロジェクト中63しか判定できませんでした。そのため、このリリースでは各プロジェクトがラベルをどこに保持しているかを特定しています。
6つのリファレンスリポジトリを、すべてのタグで 1.0.0 を使って測定しました(tools/reference/)。
| リポジトリ | 判定結果 | drift のラベル数 | 比較したタグ数 |
|---|---|---|---|
pallets/click | drift | 66中3 | 71中71 |
psf/requests | drift | 140中3 | 162中145 |
pypa/packaging | clean | 50中0 | 53中50 |
encode/httpx | clean | 88中0 | 88中88 |
impress/impress.js | drift | 4中2 | 15中6 |
lodash/lodash | drift | 107中60 | 440中209 |
原因は2つあります。バージョンを上げずにタグ付けされたリリース(impress.js)と、1つのバージョンファイルを共有するタグ群(lodash)です。どちらのプロジェクトの管理にも問題はありません。前者には --would-tag、後者には --tags またはコンポーネントが対応します。
制限事項
-
コードを実行することも、何かを保証することもありません。
cleanはアドレッシングに関するものであり、再現性を示すものではありません。 -
--modesを指定しない限り、ファイルモードのみの変更は検出されません。 -
解析対象には、フォルダー
test/、tests/、spec/、docs/、vendor/、node_modules/、.git/および*_test.*、*.test.*、*.mdという名前のファイルは含まれません。 -
デフォルトの解析対象 glob は推測に基づくものです。
--closureを指定してください。 -
ラベルは記載どおりに比較されます。
--label-equality versionを指定しない限り、1.0と1.0.0は別のラベルです。 -
読み取るのは Git であり、パッケージインデックスではありません。リリースされていないタグもカウントされます。ただし、
--publishedで公開済みのものを指定した場合は除きます。リストから除外されたタグは、未公開として表示されるのではなく、解析対象から除外されます。 -
ラベルの特定方法はビルドではなく、一連のルールです。
docs/LABELS.mdを参照してください。 -
ゲートでは
--version-fileまたは--publishedを指定してください。たまたまタグと一致するファイルがある場合、ルールが依然として誤ったファイルを読み取る可能性があります。
これまでに見つかったすべての失敗: docs/FAILURES.md。詳細:SCOPE.md、docs/WHY.md。
安全性
git と、指定したバージョンパターン自体のみを起動します。それ以外は起動しません。リポジトリへの書き込みは行わず、そのリポジトリの Git 設定に記載されたコマンドも実行しません。防御対象と対象外については、THREAT_MODEL.md を参照してください。報告については、SECURITY.md を参照してください。
テスト
コードを書く前に事前登録した、4 つのテストスイートです。スコアを合算することはありません。
| スイート | macOS、Python 3.14 |
|---|---|
tests/battery.py — 受け入れ証明 | 216 件を宣言 · 215 件が成功 · 0 件が失敗 · 1 件は未実行 |
tests/negative_controls.py — 検出器が壊れている場合にテスト一式が失敗することを確認 | 60 個のミュータント · 60 個を検出 · 0 個を未検出 |
tests/adversarial.py — 修正を行っていないレビュー担当者が作成 | 315 件の攻撃 · 304 件が要件どおり · 2 件は基準が緩い · 9 件は未実行。基準が緩い 2 件は宣言済みの制限です(docs/FAILURES.md O2a、O2b) |
tests/properties.py — tests/oracle.py(この実装を読んでいない人が仕様から作成した別実装)に対する 60 個の生成リポジトリ | 17 個のプロパティ · 17 件が成功 · 0 件が失敗 · 10 個のコントロール · 10 個を検出 |
CI では、Linux(Python 3.9~3.14)、macOS、Windows で同じ 4 つのテストスイートを実行します。各プラットフォームで実行できなかったものは、tests/RECORD.md に記載されています。これらのスコアが示すのは実行したケースであり、誰も試していない入力についてではありません。./reproduce.sh ですべてを実行できます。調査対象の 100 個のプロジェクトも、記録されたコミットに固定された回帰テスト用コーパスです。予測どおり、1.2.0 は 100 個すべてで記録済みの答えを返します(tools/regression/)。
大規模リポジトリ 10 個(Linux カーネル、LLVM、CPython、その他 7 個)を対象に、プロトコルを先に作成しました。カーネルのすべてのタグを処理する場合、プロセスツリー全体のメモリ使用量は 0.9.1 で 6.5 GB、1.0.0 で 900 MB でした。0.9.1 はそこで 3 件の誤った回答、またはほとんど空の回答を返しましたが、1.0.0 はそのどれも返しません。宣言済みのバージョンファイル(docs/LABELS.md)を使うと、カーネルのすべてのタグが比較されます。結果は clean、947 件中 947 件で、所要時間は 4 分です。
tools/benchmark/READING.md。
結果の送信
RESULTS.md は、作者以外の人が測定した結果を掲載する場所です。まだ空です。closure-drift --json > result.json を実行し、issue を作成するか、[email protected] までご連絡ください。このツールが誤っていることを示す結果が、最も役に立ちます。
バージョン
1.2.0 では、1階層下にあるバージョンファイルを特定し、ダーティーツリーがクロージャーに影響するかどうかを示し、出力先が端末でない場合は UTF-8 で書き込みます(issue #12 に基づく変更です)。
1.1.3 では、--explain または --compare、および --tags または --published を使うタイミングを説明しています。検出機能に変更はありません。
1.1.2 では、MCP サーバーを1行でインストールできるようになりました(closure-drift-mcp)。検出機能に変更はありません。
1.1.1 では O11 を修正し、サイズ制限(1 MB)を超えるバージョンファイルの名前を示して、結果を必ず
incomplete にします。clean にはなりません。AI エージェント向けに integrations/ を追加しています。1.1.0(スクリプトの sha256 e2a67a5a9148fe273d2c95ad4bcce9bc589a43711fc34443a4a9491fde575913)
では、--published、3つのバージョンソース、より早い段階での拒否、オプトイン方式の比較を2つ追加しています。新しいオプションを指定しない場合、結果が変わるのは --at commits を使った場合のみです(CHANGELOG.md)。満たすべき要件は先に文書化され(tests/PREREGISTRATION.md §16)、作成者ではないレビュー担当者がタグ付け前に13件の問題を見つけました(docs/REVIEW-1.1.0.md)。
SCOPE.md では、2.0 まで安定して維持される内容を説明しています。
ライセンスは Apache-2.0 です。コンセプト DOI 10.5281/zenodo.21763931 の下にあるバージョン DOI を引用してください
(CITATION.cff)。次の予定については、ROADMAP.md をご覧ください。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。