mcpField
C
callidescope-trace

callidescope-trace

github.com/organizzolini開発0277 回インストール今日更新
リポジトリ

callidescope-trace について

callidescope-traceは、TypeScriptワークスペースの呼び出しグラフ分析とレポート解釈を支援するスキルです。依存性注入を通る呼び出しや深さ・幅の判定、個別の呼び出し経路の調査を扱います。

主な機能

  1. 01依存性注入を介した呼び出しの追跡
  2. 02プロジェクトごとの深さ・幅の判定
  3. 03直接の呼び出し元と呼び出し先の検索
  4. 04呼び出し元・呼び出し先の経路表示
  5. 05呼び出しスタックをMermaidで統合表示

活用シーン

  1. 01設定した深さ制限を超える呼び出しスタックの発見
  2. 02リネーム前の呼び出し元の確認
  3. 03責務を抽出する切り分け箇所の特定
  4. 04レイヤーのインライン化や折りたたみの検討

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/organizzolini/codebase --skill callidescope-trace

インストール後、Claude Code で/callidescope-traceを入力して読み込まれたか確認します。

詳細

callidescope の実行方法と出力の読み方

Callidescope は TypeScript ワークスペース全体の呼び出しグラフを構築し、その構造を測定します。注入された依存関係を通じた呼び出し、つまり this.someService.load() から SomeService.load への遷移を追跡します。これはファイル単位で解析するツールでは把握できないエッジであり、フレームワークのコードベースにおける制御フローの大半が実際に存在する場所です。

3 つのコマンドが、それぞれ異なる 3 つの問いに答えます。

npx callidescope --config configuration/callidescope.config.ts                                        # the whole workspace
npx callidescope depth --config configuration/callidescope.config.ts --addresses src/foo.service.ts#FooService.bar    # one callable, vertically
npx callidescope breadth --config configuration/callidescope.config.ts --addresses src/foo.service.ts#FooService.bar  # one callable, horizontally

3 つすべてで --config が必須で、ワークスペースの構成ファイルを指定します。検索やデフォルト設定は行われないため、これを省略した実行はフラグ名を示して拒否されます。解析対象プロジェクト自身の callidescope.config.ts は別の話です。このファイルは存在し、完全な内容でなければならず、そうでない場合は解析が始まる前に実行が拒否されます。callidescope-configure スキルを参照してください。実行時に何をゲート条件とするかと、何を書き出すかは、どちらもオプトインであり、どちらもこのスキルで説明しています。このページでは結果の読み方を扱います。

callidescope:ワークスペース全体

プロジェクトごとに 2 種類の検出結果を報告します。

深い呼び出しスタック。 各エントリポイントの下で最も深い経路を 1 つだけ報告します。ただし、limits.maximumDepth を超える場合に限ります。エントリポイントごとに構築される経路は 1 つだけなので、幅の広いグラフでも狭いグラフよりコストが増えることはありません。

幅。 ある呼び出し可能な要素が、直接呼び出す呼び出し可能な要素の数です。常に報告されますが、プロジェクトで limits.maximumBreadth が宣言されている場合に限り、上限に照らして評価されます。これは、どこにもデフォルト値がない唯一の上限です。

各検出結果には、評価に用いた上限が付記されます。その上限は実行についてではなく、スタックのルートが属するプロジェクトについての情報だからです。1 つのレポートに複数の異なる数値が含まれるのは日常的なことであり、2 つのプロジェクトに同一のスタックがあっても、コードに違いがないまま、一方では検出結果となり、もう一方では何も報告されないことがあります。

スタックの読み方

Stack #1 | 🚨 [DEPTH ≥ 10 > 6] (decorated-method)
🚀 SomeCommand.run(options: SomeOptions): Promise<void> [.../some.command.ts:220]
   ↳ Measure the repository and write every configured output.
  └─> SomeService.measure(args: MeasureArguments): Result [.../some.service.ts:115]
     ↳ Measure aggregated statistics for the provided directory.
  • ≥ 10 は測定値ではなく下限です。 この経路上に、パラメーター経由で呼び出されるコールバックや、計算されたメンバー名など、追跡できなかったものがあります。過少報告を黙って行うのではなく、そのことを実行結果で示しています。実際の深さは少なくとも 10 です。これは不具合ではなく、疑うべき数値でもありません。上限が 6 のときの ≥ 10 は、正真正銘の上限超過です。サマリーの Unfollowable calls は、そのような呼び出しの数を示します。
  • 括弧内の種類は、ルートをエントリポイントとして認識したルールです。 orphan-root は、リポジトリ内でその要素を呼び出すものがないことを意味します。デッドコードであるか、構成にエントリポイントのルールが欠けています。
  • 各フレームには file:line が付いています。次の手順は、そのファイルを開くことです。
  • 各フレームには型チェッカーの情報が注釈されています。シグネチャと、1 行にまとめた JSDoc の説明です。これにより、スタックは単に調べる場所の一覧ではなく、読みやすいものになります。(…): ReturnType と出力されたフレームは、シグネチャが 80 文字を超えています。ほとんどの場合、多数の注入されたサービスを受け取るコンストラクターです。120 文字を超える説明は、先頭の文だけが印を付けずに出力されます。文の境界で分割できない単一の文だけは、途中で切り詰められ、… が付けられます。

短縮されるのは、出力されるツリーだけです。 JSON レポートにはコメント全文が含まれるため、完全なテキストが必要なスクリプトでは、ツリーを解析せずに JSON を読み込んでください。

mermaid の描画では、すべてのスタックを 1 つのフローチャートとして表示します。個別の図にはしません。スタックが 1 つだけなら直線になり、直線は余計な手順を加えたリストにすぎません。複数のスタックをまとめて描くと共通の末端が合流します。すべてのコマンドが同じリポジトリに到達する様子や、すべてのリゾルバーが同じサービスに行き着く様子です。この合流こそが図で示せるものであり、インデント付きのツリーでは示せません。エントリポイントはスタジアム形、それ以外は四角形で描きます。読者がどのテーマでも図を読めるよう、色ではなく形で区別します。図は呼び出し可能な要素 300 個で打ち切られます。図に描かれていない要素へ向かうエッジが含まれないよう、スタックを切り詰めるのではなくスタック全体を省略し、省略した数を示します。

プロジェクト単位の判定を読む

@callidescope/nx を使うワークスペースでは、ブランチが実際に実行するのは、あるプロジェクトの gate であり、そこで出力されるのは縮小版のレポートではありません。判定に用いた2つの指摘だけが出力されます。それ以外は出力されません。

## Call stacks over the depth limit (1)
...the stack, as a tree...

## Callables over the breadth limit (1)
- `GatedLeafService.read` — 3 direct callees, limit 2 (…/gated-leaf.ts)

読む際に押さえておくべきことは、次の4つです。

  • 指摘の対象は、判定対象プロジェクト自身です。 gate は、そのプロジェクトと Nx の依存関係をたどったうえで、名前が付けられたプロジェクト自身が所有するものだけを判定します。依存先の違反は、その依存先自身の gate が扱うものです。つまり、失敗した gate が示しているのは1つのプロジェクトであり、修正するのはそのプロジェクトのコードか、そのプロジェクト自身の callidescope.config.ts です。
  • gate の終了コードが判定そのものであり、それ以外ではありません。 書き込み先を読み込まないため、失敗した gate がコミット済みファイルを変更することはなく、成功しても何も公開されません。
  • 概要の Deepest stack は、判定に使われた数値ではありません。 スコープ指定実行のカウントは、依存関係の推移閉包も含むトレース全体を示すため、報告される最深スタックは、判定対象プロジェクトが所有するものより深いことがよくあります。概要行に表示される数値ではなく、判定が示す数値を使ってください。同じ注意は、🔭 Finished an analysis ログ行にも当てはまります。詳しくは callidescope-triage スキルを参照してください。
  • ## Read nothing of its own (0 files) は、コードではなく実行に関する失敗です。 gate は、ソースを一度も開いていないプロジェクトを判定しました。したがって、成功判定になっても、何も確認していないということにしかなりません。trace ターゲットは同じブロックを出力して成功しますが、gate はこれを失敗として扱います。

兄弟の trace ターゲットは、代わりにレポート全体を出力します。概要、プロジェクトごとにそのプロジェクト自身の制限値と照らし合わせた1行を載せる Projects インデックス、そして Depth headroom のスコアボードです。プロジェクトを理解するには trace を使い、パイプラインが赤になった理由を理解するには gate を読んでください。

コミット済みのプロジェクトブロックを読む

トレースされた各プロジェクトの README には、## 🔭 Callidescope セクションがあり、そのプロジェクトに適用される数値は ### Limits テーブルに記載されています。

| Limit | Value |
| --- | --- |
| `maximumDepth` | 4 |
| `maximumBreadth` | none |

origin 列はなく、記録すべき2つ目の origin もありません。 トレースされた各プロジェクト自身の callidescope.config.ts は、ワークスペースの projectDefaults を展開し、必要な項目を上書きする完全な設定です。そのため、どちらの数値もこの README と同じ場所にあるファイルに記載されています。そうでなければ、何もトレースされる前に実行が拒否されています。none は、そのプロジェクト自身のファイルで breadth の制限を指定していないことを意味します。これは省略ではなく意図的な指定である理由については、callidescope-configure スキルを参照してください。

上にある概要の Deepest stack 行を、ここにある depth 制限と照らし合わせてください。この2つで、そのプロジェクトの gate が判定する内容のすべてがわかります。

1つの呼び出し可能要素を指定する

depth と breadth は <file>#<qualified-name> を受け取ります。これはファイルパスと、callidescope が各スタックフレームにすでに出力している修飾名を # でつないだものです。Python のトレースバックや ESLint のルール ID と同じ形式なので、レポートからそのままコピーできます。

同じ修飾名の宣言が複数あるファイル(オーバーロードが2つある場合や、同じプロパティにバインドされたコールバックが2つある場合など)では、末尾に :<line> を付けて区別します。

npx callidescope depth --config configuration/callidescope.config.ts --addresses src/foo.service.ts#FooService.bar:118

どれを指すのか判別できない場合、実行はその旨を示し、すべての候補の行番号を出力するため、行番号を付けたアドレスをコピーするだけで指定できます。

どちらのコマンドも何も書き込まず、書き込み先との比較も行わず、--check、--write、--json、--markdown、--mermaid、--maximum-depth、--maximum-breadth も受け取りません。検索結果は常に出力されるだけです。どちらのコマンドも、グラフの形状を決めるすべてのフラグ(--directories、エントリーポイントの上書き、2つの除外リスト)に加えて、必須の --config と --format を受け取ります。アドレスを解決する場合も、まずワークスペースをトレースする必要があるためです。

breadth:何を呼び出し、何から呼び出されるか

呼び出し可能要素の直接の呼び出し先と直接の呼び出し元を並べて表示します。リファクタリングや名前変更を安全に行う前に、両方について同時に答える必要があります。

  • 名前変更。 呼び出し元は、更新が必要な箇所を漏れなく示します。依存性注入を使うコードベースでテキスト検索がうまく答えられないのは、この問いです。this.someService.load()を介した呼び出しは型チェッカーには見つかりますが、クラス名でgrepしても見つかりません。
  • 抽出。 幅広い呼び出し可能要素から責務を抽出するとは、それが呼び出す要素の一部を切り出すことです。呼び出し先の一覧は、切り分ける境界を選ぶための素材になります。共通の関心事を持つ要素を探し、それらをまとめて切り出します。
  • インライン化。 呼び出し元が1つで、呼び出し先が少ない呼び出し可能要素は、その呼び出し元に統合する候補です。breadthは「呼び出し元が1つ」という部分を、推測ではなく確認します。

問題として指摘されていない呼び出し可能要素も対象にします。指摘事項がなくても、実行する価値があります。

depth:上と下のすべての経路

呼び出し可能要素の上にあるすべての経路と、その下にあるすべての経路を表示します。つまり、ルートまでの呼び出し元のチェーンと、末端までの呼び出し先のチェーンをすべて表示します。callidescope自身のレポートのように、それぞれの方向を最も深い1つの経路にまとめることはありません。

この違いが重要です。ワークスペースのレポートは「どれくらい深くなるか」に答えるため、エントリーポイントごとに1つの経路があれば十分です。depthは「この呼び出し可能要素が実際には何に関わっているか」に答えます。多数の場所から到達される、または多数の末端に到達する呼び出し可能要素は、まさにその形をすべて表示するための対象です。

それぞれの方向で経路数は200に制限され、上限に達した実行ではその旨が示されます。広く呼び出され、呼び出し先も同じように広がっているユーティリティでは、分岐が掛け合わさります。すべての経路を列挙する処理には、最も深い経路を1つ選ぶ場合のような構造上の上限がないため、この上限が設けられています。

  • depthの指摘事項を計画に落とし込む。 ワークスペースのレポートはスタックを示します。その中間にあるフレームをdepthで調べると、そのフレームが関わる他のすべてのチェーンがわかります。これにより、転送レイヤーをまとめても安全なのか、ほかの3つの呼び出し元がそれに依存しているのかを判断できます。
  • 「この場所は適切ではなさそう」という見立てを検証する。 呼び出し元のツリーを見ると、呼び出し可能要素が実際にどこから使われているのかを、最も深い1つの経路にまとめずにすべて確認できます。

どちらを使うか

問いコマンド
このワークスペースに深すぎる、または広すぎる箇所はありますか?callidescope
これの名前を変更すると、何が壊れますか?breadth
この呼び出し可能要素をどこで2つに分けますか?breadth
これをインライン化できますか?breadth
このレイヤーをまとめられますか?depth
この呼び出し可能要素は実際には何に関わっていますか?depth
これはこのファイルに置くべきですか?depth

スコープ指定実行で測定されるもの

--directories は、実行対象を指定したプロジェクトディレクトリに絞り込みますが、トレースはそれらのディレクトリ外への呼び出しも追跡します。スコープ指定実行では、指定した各プロジェクトと、そのインポートが推移的に到達するすべてのプロジェクト(つまり、そのプロジェクトの依存関係の閉包)についてプログラムを構築します。そのため、別のワークスペースパッケージへの呼び出しは実際のフレームに到達し、実行対象に指定されていないパッケージのフレーム、スタック、プロジェクトごとの行もレポートに表示されます。これは閉包によるものであり、漏れではありません。

変化した深さの読み方。 プロジェクト自身の tsconfig.json には、インポートするパッケージが記載されていないため、以前はスコープ指定実行で指定ディレクトリ外への呼び出しが、トレース対象のどのプロジェクトにも属さないコードに残っていました。所有者のいないコードへの呼び出しは、インストール済みパッケージへの呼び出しと同じように、外部呼び出し、つまりリーフとして扱われます。スタックはパッケージの境界で終わり、そこで追跡が早く終わったことを示す情報はレポートに何もありませんでした。説明できる変更がないのに数値が上がった場合、それはこの過小測定が修正されたことを意味します。

次の3点を踏まえ、数値が変化した理由を確認してください。

  • スコープ指定時の深さは測定値であり、下限ではありません。 ≥ が示すのは、追跡できない呼び出しがある場合だけです。スコープ指定時にそれが表示されなかったため、スコープ指定による過小測定が見えなくなっていました。
  • 数値は、どの実行で測定したかには左右されません。 ファイルは、どのプログラムが取り込んだかにかかわらず、そのファイルを含む最も深いプロジェクトルートに属します。そのため、どちらもそのファイルを宣言するプロジェクトをビルドする限り、2つのスコープ指定実行とワークスペース全体の実行で、同じ呼び出し可能要素の数値は一致します。ただし、拒否された宛先のプロジェクトはその条件を満たしません。
  • ワークスペース全体の実行では、閉包によって何も変わりません。 ディレクトリを指定しないため、すべてのプロジェクトがすでに開始プロジェクトとなり、その閉包はワークスペース全体です。そこで数値が変化したなら、別の理由によるものです。

閉包が到達しないものと、それぞれを除外する理由は次のとおりです。

  • 意図的に依存元は含めません。 走査は下方向のみです。スコープ対象のプロジェクトをインポートするプロジェクトはビルドされず、そのスタックも表示されません。これにより、依存元での編集がスコープ対象プロジェクトの数値に影響しなくなります。
  • package.json のないプロジェクトルート。 マニフェストがあることで、そのディレクトリは他のプロジェクトが依存できる対象になります。tsconfig.json だけを含むルートは共有設定であり、どのプロジェクトからも依存されるのではなく、すべてのプロジェクトから読み込まれます。このルールがなければ、そのようなディレクトリ1つによってワークスペース全体が取り込まれてしまいます。
  • ワークスペースルート。 他のすべてのプロジェクトを含むルートを持つプロジェクトは、それらのプロジェクトのいずれにとっても意味のある依存先にはなりません。

どちらの除外も、宛先 のみに適用されます。そのため、直接指定するか、ディレクトリを指定しない実行であれば、どちらの種類も完全にトレースされます。ただし、除外されたディレクトリへの呼び出しにはフレームが割り当てられません。

下方向のみのスコープでも、深さと幅はどちらも維持されます。 これは自明ではないため、明記しておく価値があります。深さと幅はどちらも、呼び出し可能要素の推移的な呼び出し先を集計します。呼び出し先は下方向にあり、閉包が完全に含む対象そのものです。どちらの測定にも依存元は必要ありません。

測定範囲を広げても、公開範囲は広がりません。 各プロジェクト自身の write.markdown(および、宣言されている場合は write.mermaid)は、実行のスコープ対象プロジェクトについてのみ書き込まれます。そのため、スコープ指定実行が、測定しただけの依存先にあるセクションを書き換えることはありません。

実際のスコープ指定実行で到達するプロジェクトと、その理由を示す例は、@callidescope/examples パッケージの examples/dependency-closure です。

グラフに含まれるものと含まれないもの

3 つのコマンドすべてに同じ解決ルールが適用され、それによって各コマンドで把握できる内容が決まります。

記述形式解決先
helper()呼び出し先のシンボル。インポートエイリアスをたどって解決します
this.service.load()メンバー名のシンボル。依存関係が注入されるケースです
provider.ingest()インターフェースを構造的に満たすすべてのクラス。上限あり
super.run()チェッカーが解決する基底宣言
new Thing()コンストラクター。本文がある場合です
list.map(callback)コールバック自体のフレーム。map 自体は外部です
target[key]()何もありません。推測せず、追跡不能として記録されます

覚えておくべき重要な点が 4 つあります。

  • 構造的マッチングは必須です。クラスは implements を記述せずにインターフェースを満たすことがよくあるためです。また、呼び出し元の一覧には、実行時には呼び出し対象を実際に呼び出さないクラスが含まれる場合もあります。この上限は @callidescope/graph 内の固定定数であり、設定可能な制限ではありません。そのため、結果の幅が不自然に広く見える場合は、上限を引き上げるのではなく、ファンアウトを調べてください。
  • 計算されたメンバー呼び出しは何にも解決されません。 その方法で呼び出し可能な対象に到達する呼び出し元は表示されないため、計算名を使ってディスパッチするコードベースでは、breadth だけではリネームの影響をすべて把握できません。
  • インストール済み の依存関係への呼び出しはリーフになります。 Array.prototype.map がどれほど深く実装されているかは、あなたの レイヤーが深すぎるかどうかとは無関係です。また、その数を数えると、関連のないアップグレードのたびにすべての数値が変わってしまいます。同じワークスペース内の別プロジェクトへの呼び出しは、これには該当しません。実行範囲にかかわらず、実際のフレームに解決されます。
  • excludeCallees のグロブに一致する対象は、グラフから完全に除外されます。そのため、設定で除外した呼び出し可能な対象(通常はロガー)は、どの一覧にも表示されず、深さや幅にもカウントされません。

深さを測定する前に、サイクルはまとめられます。そのため、相互再帰する 3 つのまとまりは、フレームの合計が上限のないスタックにおける妥当な下限として、3 フレーム分だけ寄与します。これにより、実行ごとに数値が変わることはありません。探索中に再訪を検出すると、どの経路が先に到達したかによって答えが変わってしまいます。数値が勝手に変わるリンターは、ゲートとして使えません。

深さはルートとの相対でのみ意味を持ちます。また、フレームワークのコードベースにあるコードの大半は、リポジトリではなくフレームワークから呼び出されます。そのため、ルートは設定可能です(デコレーター付きメソッド、ライフサイクルフック、ブートストラップ、インデックスエクスポートなど)。呼び出し元がないまま残ったものは、削除せずに孤立ルートへ昇格します。

読了後

実行を絞り込む、ゲートを設定する、公開する場合や、上記のしきい値を変更する場合は、callidescope-configure スキルを使ってください。実行に失敗した場合や、レポートが古く見える場合は、callidescope-triage を使ってください。

リファクタリングによって呼び出しグラフが変わるため、コミット済みのレポートはすべて古くなります。変更が反映されたら、書き込み設定を再実行してください。また、再生成する前にリンターを実行してください。すべてのフレームには file:line が含まれるため、クラスメンバーを並べ替えるフォーマッターを使うと、それ以降のすべての行番号が変わります。その並べ替えより前に書き出されたレポートは、並べ替えが反映された時点で古くなります。

コメント