循環的複雑度で本当に測るべき、デリバリーリスク
要約
循環的複雑度(CC)はコード品質メトリクスの定番ですが、本来は段階的ロールアウトのリスク指標として機能すべきです。同じCC値でも実運用では大きく異なるリスクを抱えており、差分ベースの測定とテスト負荷との関連付けが、本当に効果のある運用につながります。
循環的複雑度で本当に測るべき、デリバリーリスク
循環的複雑度(CC)は関数を通じる独立経路の数を数える指標です。NISTは関数あたりの安全な上限を10に設定しており、多くのチームがCIで同様の基準を運用しています。ただし、この数値が教えてくれないのは、これからデプロイしようとしているカナリアが5%の本番トラフィックをどの経路に送るのか、そしてそのギャップが実際のロールアウトインシデントのきっかけになる、という事実です。循環的複雑度をリント規則ではなく、デリバリーリスクの入力信号として扱い始めると、その数値が実際の価値を生み出します。
循環的複雑度の測定対象と限界
McCabeのメトリクスはグラフ理論に基づいています。ノード、エッジ、連結成分を数えることで、if、for、while、case、ブール演算子のそれぞれが経路を追加します。複雑度が25の関数には最低でも25の線形独立経路が存在します。これは意見ではなく、基底経路テスト(basis path testing)の数学であり、関数を端から端まで一度カバーするために必要なテストケース数の下限を設定します。
多くのチームはこの下限に到達していません。カバレッジツールは行カバレッジを報告するため、関数は90%のカバレッジを示していても、CIで実行されない分岐が半分あることがあります。これはPRテンプレートに書かれていない部分で、「テストが通った」と「これはデプロイして安全だ」が異なる主張であることを認識できない理由です。
十分なポストモーテム分析を行ったチームなら気づいています。インシデントレビューにはエラーバジェットの消費行と検出時間の行がありますが、差分でどのような複雑度の関数が変更されたかについては、ほぼ記録されません。これは書類の不備というより、ツールの盲点です。

フラットな複雑度スコアが本当のリスク半径を隠す理由
ここがほぼすべての複雑度ダッシュボードの落とし穴です。2つの関数が同じ循環的複雑度スコアを掲示していても、運用上のリスクは大きく異なる場合があります。20分岐の well-tested なスイッチ文で処理を振り分けるのと、4つの条件のポケット、それぞれ3段階のネストを持つ80行の関数は、全く別の獣です。CodeSceneはこの2番目のパターンを「bumpy road」と呼んでおり、ネストの深さこそがワーキングメモリを圧迫し、テストされていないエッジケースを隠すのです。
プラットフォームチーム向けには、より微妙な問題があります。カナリアは平均的な複雑度で失敗するのではなく、今週の差分でたった1つ触れられた関数で、真夜中のロード下で失敗します。ポストモーテム分析では繰り返したパターンがあります。ルート原因となった関数は、リポジトリで最も高い複雑度スコアをもつことはほとんどなく、このデプロイで変更された差分における複雑度の差分(complexity delta)が最大なのです。これは異なる信号であり、ほぼ誰も測定していません。
リポジトリの複雑度トレンドが下がっていることを示すダッシュボードと、このロールアウトが今、3つの新しいネストされた分岐を追加した関数に触れたことを示すダッシュボードは異なるものです。前者は四半期に一度読むヘルスレポート、後者はデプロイパイプラインで本当に欲しいゲートで、SLOチェックの横に置くべきもので、誰もインシデント中に開かない静的解析ツールの奥底に埋もれるべきではありません。
リング型やパーセンテージ型のロールアウトは既に、差分によってリスクが異なることを前提としています。それが段階的ロールアウトの全体的な考え方です。しかし、ほとんどのパイプラインが実施していないのは、差分の複雑度の形状そのものがカナリアの拡大速度に影響するようにすることです。現在、これはコードレビュー上の懸念として扱われ、PRがマージされた瞬間に忘れられます。
CIで誰もが10に設定するゲートが、実際には何も動かさない理由
循環的複雑度を10で上限にしてビルドを失敗させよ、というのは、ほぼすべてのエンジニアリングブログが推奨し、ほぼ誰も実際のインシデントデータに照らして検証しない標準的なアドバイスです。間違っているわけではありません。ただし、チームがリスクを処理したと信じているのに、実はそれを移動させているだけという形で不完全です。
2つの失敗パターンが一般的です。
1つ目:数値ゲーム Extract Methodは教科書的な修正で、機能します。関数あたりの循環的複雑度は低下します。ただし、抽出が実際の意思決定数を減らさず、単に1つの関数から3つの関数に分散させているなら、システムの複雑度は変わっていません。ゲートは緑になります。リスク半径は縮小しません。単に1つの差分ビュー内で見えにくくなるだけです。
2つ目:形状を無視 フラットな閾値は12分岐のディスパッチャと12経路の bumpy road を同等のリスクとして扱います。違います。ディスパッチャはおそらく安全です。機械的なルーティングです。bumpy road は、3段階のネストの深さで状態を追跡することをやめたレビュアーが次のロールバックをもたらす場所です。
AI コーディングエージェントはこれをさらに悪くしてから良くします。DevinやReplit Agentのような自律的なエージェントは高速にPRをデプロイします。「高速」は往々にして、既存の分岐をリファクタリングする代わりに分岐を追加することを意味します。これはテストスイートの成功に最適化しているモデルにとって最小抵抗の道であり、レビュアーの認知負荷最適化ではありません。チームがAIで作成された差分を大量にマージしている場合、複雑度差分/PRは、ダッシュボードに表示させておくべき指標であり、インシデントレビューで後付けで発見するべきものではありません。

複雑度差分がテスト負荷について教えてくれること
絶対スコアは一旦置いておきます。インシデントリスクを予測する数値は、単一の差分で導入された複雑度の変化であり、それをその差分に関連するテストが実際に増加したかどうかと相互参照したものです。
1つのPRで複雑度が8から19に上昇した関数は、上記の基底経路テストの数学により、必要なテスト数がおよそ2倍になっています。PRが2つのテストを追加した場合、カバレッジギャップが成功したCI実行に偽装されてデプロイされています。そのギャップはカナリアが本番トラフィックの5%をテストされていない分岐に送るまで表示されず、その時点では既にインシデントとなっており、PRスレッドに未解決で座っているコードレビューコメントではありません。
これはupstreamapi のAI Paletteが閉じようとしているツール上の計測ギャップです。複雑度差分に基づいてカナリアのパーセンテージと保留時間をゲートし、下流のエラーレートSLOだけでなく、出荷された差分のテスト差分に照らし合わせます。エラーレートSLOは何か既に壊れたことを示しています。複雑度差分ゲートは、差分がライブトラフィックをサービスする前に何かを壊す可能性が高いことを示しており、その情報がまだ実行可能な唯一のポイントです。
// Simplified canary gate: widen slowly on low-risk diffs, hold on high-risk ones
function canaryStep(diff: DiffMetrics): CanaryDecision {
const complexityRisk = diff.complexityDelta / Math.max(diff.testDelta, 1);
if (complexityRisk > 3 && diff.slo.errorBudgetBurn > 0.1) {
return { action: "hold", trafficPct: diff.currentTrafficPct };
}
if (complexityRisk > 3) {
return { action: "extend_bake_time", bakeMinutes: 45 };
}
return { action: "advance", trafficPct: diff.currentTrafficPct + 10 };
}実務的に採用するなら、こうゲートを設計する
3つのステップ、優先順位の順に。
差分あたりの複雑度差分を計算 リポジトリ全体の平均は、今週実に重要な1つの関数を隠します。差分あたりの差分はほとんどの静的解析ツールで低コストで計算でき、その後の48時間で実際に壊れるものと相関しています。
テストの数ではなくテスト差分と相互参照 複雑度が8から19に上昇した40のテストを持つ関数で新しいテストがゼロであることは、初日から複雑度15で一致するカバレッジを持つ新鮮な関数より大きなリスクです。
マージ承認ではなく、ロールアウトペースにフィードバック 高い複雑度差分は必ずしもレビューで許可を拒否するべきではありません。正当に複雑なドメインロジック(ステートマシン、プロトコルパーサー)は常に高いスコアになります。より遅いカナリアと短いエラーバジェットのリード鞭を必要としており、それはコードレビュー決定ではなく、ロールアウト決定です。
ロールアウト前にランブック エントリを記述し、ページ後ではなく、このドキュメント不備を避けます。朝の3時にインシデントチャネルを開くオンコール エンジニアが、なぜ「きれい」なPRがエラーバジェットを燃やしたのかを逆算する必要があるなら、ドキュメントはコードより先に失敗しています。1行のデプロイログメモ(「複雑度差分11、テスト差分1、20%でホールド」)は書くのに何もコストがなく、その後のすべてのインシデントレビューの最初の15分を節約します。

追跡する価値があるか、それともダッシュボード上の別の数値か?
どこに付着させるかによって全く異なります。リポジトリのヘルス トレンドラインとしての循環的複雑度は、ほぼ装飾です。四半期のスライドには良いですが、朝の2時には役に立たません。単一差分の差分としての循環的複雑度で、カナリアペースにフィードバックされ、テストの成長と相互参照されたものは、「このロールアウトが誰かをページングするつもりだ」という最も安いリード指標の1つです。
ポストモーテムは、ロールバック前にSLOバジェットがどう見えたかを尋ねます。ますます、私たちのものはまた、デプロイされた差分の複雑度差分がどう見えたかを尋ねます。その質問を本番のインシデント前に自分のランブックに追加する価値があります。レトロスペクティブでなく、答えが肩すくめと「次回はもっと良いテストを追加しよう」という約束ではなく。