ベースライン: 既存コードでしきい値をラチェットする

既存のコードベースにメトリクスのしきい値を導入すると、たいてい同じ壁にぶつかります。妥当なしきい値はどれも既存の関数を何百件もフラグし、CI はプッシュのたびに赤になります。現実的な採用経路は「現状からラチェットし、新規の違反者だけを失敗させる」ことです。ベースラインファイルは、bca check がそのワークフローを支えるための仕組みです。

ベースラインはソース内の抑制マーカーを補完するものであり、その代替ではありません。関数が意図的に恒久的に複雑な場合(パーサー、状態機械、生成コード)は抑制マーカー(抑制マーカー)を使ってください。チームが負債を返済していくつもりの場合はベースラインを使います。両方を同じリポジトリで併用でき、抑制の方が先に評価されます。

エンドツーエンドの採用フロー

ワンショットのショートカット: bca init は、統合された bca.toml マニフェスト(pathsexclude_frombaseline[thresholds] テーブルを含む)、それが参照する .bcaignore、そして現在のツリーから導出した初期 .bca-baseline.toml を 1 つのコマンドでスキャフォールドします。マニフェストが配置されていれば、引数なしの bca check がそれを自動発見し、設定なしでゲートします。既存ファイルを上書きするには --force を、ツリーの走査をスキップするには --no-baseline を渡します。以下の長めのレシピは、ベースラインをブートストラップする前にしきい値を調整したい場合に有用です。

1. 初期しきい値を決める

感覚的な数値(cyclomatic=15cognitive=20)を使うか、リポジトリ全体に対する bca check --no-fail の実行結果から現在の分布を確認して決めます。

# bca.toml — リポジトリのルートに置くと `bca check` が自動発見します。
paths = ["src"]

[check]
baseline = ".bca-baseline.toml"

[thresholds]
cyclomatic = 15
cognitive = 20
"loc.lloc" = 200

2. ベースラインをブートストラップする

bca check --write-baseline

パスなしの --write-baseline は、先ほど作成した bca.tomlbaseline キーに書き込むため、ファイル名は 1 か所だけで管理されます。明示的なパス(--write-baseline <file>)を渡すのは、デフォルトとして使えるマニフェストの baseline がない場合だけにしてください。マニフェストがない場合、パスなしの形式はファイル名を推測せずエラーになります。

両方のファイルを同じ変更でコミットします:

git add bca.toml .bca-baseline.toml
git commit -m "ci: introduce metric thresholds with baseline"

ベースライン内のパスキーは、ベースラインファイル自身のディレクトリ(アンカー)からの相対パスで保存されます。--paths .--paths src/--paths "$PWD" はバイト単位で同一のベースラインを生成し、CI がどの --paths 形式を使っても --baseline の実行結果は一致します。--write-baseline を再実行することなく、自由に切り替えられます。

3. CI ゲートを組み込む

GitHub Actions:

- name: Check code complexity thresholds
  run: |
    bca check
  # `paths`、しきい値、`baseline` はすべてリポジトリルートで
  # 自動発見された `bca.toml` マニフェストから取得されます。

GitLab CI(該当ジョブ向けのスニペット):

threshold-check:
  image: rust:1
  before_script:
    - cargo install --locked big-code-analysis-cli@<VERSION>
  script:
    - bca check

終了コード: 0 はクリーン、2 はリグレッションまたは新規違反、1 はツールエラーです。CI 環境ごとの幅広い対応表については CI 統合 を参照してください。

4. チームの負債返済に合わせてベースラインを更新する

数週間ごと、または集中的なリファクタリングの後に:

cp .bca-baseline.toml .bca-baseline.old.toml
bca check --write-baseline
bca diff-baseline .bca-baseline.old.toml .bca-baseline.toml

diff が縮小していくことが目標です。変更のないツリーに対して --write-baseline を 2 回実行するとバイト単位で同一の出力が生成されるため、余計な diff が現れるのは実際の違反箇所が変わったときだけです。

Before tightening a limit, price it at both tiers

Paying debt down invites tightening the limit that produced it, and the obvious measurement — bca check --threshold <metric>=<candidate> — answers only half the question. A --threshold value is applied last and absolutely, never scaled, so it has no soft tier: a candidate that costs nothing at the hard gate can still put a whole population permanently inside the --tier=soft band, and you will not see it until you have edited bca.toml and run the other gate.

bca check --explain-threshold cognitive=15

Weigh the new column, not the offender count: it is how many baseline entries the change would add, and it is what a reviewer is actually being asked to approve. A cluster: line means the candidate landed on top of an existing population, and those entries can never be retired — see Tightening a limit onto a cluster.

5. PR レビューのヒューリスティクス

生の git diff .bca-baseline.toml を頭の中で解析する代わりに、bca diff-baseline <old> <new> を実行してサマリーを読みます。エントリは (path, qualified, metric) のアイデンティティで対応付けられるため、ファイル内で位置が上下しただけの関数は削除 + 追加としては報告「されず」、実際の変更はすべて次のバケットに分類されます:

1 added, 1 removed, 2 worsened, 0 improved

## Added
  src/new.rs::shiny        cognitive  = 30

## Removed
  src/gone.rs::old_fn      nargs      = 9

## Worsened
  src/bar.rs::act_on_file  cognitive  60 → 63
  src/foo.rs::do_thing     cognitive  25 → 27

各バケットを従来のヒューリスティクスに対応付けると:

  • removed(ベースラインが縮小)。 負債が返済されました。追加の対応は不要です。
  • added(ベースラインが拡大)。 誰かが意図的に新しい違反をファイルに追加しました。値をレビューしてください — これは意図的な暫定措置だったのか、それとも作者がゲートを回避したのか。意識的な判断であればどちらでも構いません。このファイルをコミットする意義は、その選択をレビュー可能にすることにあります。
  • worsened(エントリの value が上昇)。 関数が悪化した後に作者が --write-baseline を再実行しました。added と同様に扱い、レビューでその変更を明示してください。
  • improved 記録済みの違反がベースラインから外れることなく改善しました。無害であり、リファクタリングが機能している良い兆候です。

PR ボット向けには、bca diff-baseline <old> <new> --format markdown がスティッキーコメントにそのまま貼り付けられるフェンス付きブロックを出力し、--worsened-only / --added-only フィルタでレビュアーが必ず見るべきリグレッションだけに絞り込めます。--format json は同じ diff を他のツールに渡せます。このコマンドはデフォルトで終了コード 0 を返します — レビューに情報を提供するものであり、ゲートではありません(ゲートは bca check 自体です)。ただしオプトインの --exit-code フラグを使うと、フィルタ後の diff が空でない場合に終了コード 2 を返します。

ゲート出力の読み方

bca check --baseline の実行が失敗すると、残った各違反にタグのプレフィックスが付き、リストの後にファイルごとのロールアップが続きます:

bca: filtered 422 violations via baseline
[regr +60%] src/foo.rs:1-865: <file>: halstead.effort = 1557107.72 (limit 50000)
[new] src/bar.rs:506-747: act_on_file: cognitive = 63 (limit 25)
...

--- summary ---
src/foo.rs: 5 violations (worst: halstead.effort = 1557107.72 vs limit 50000 at L1)
src/bar.rs: 4 violations (worst: cognitive = 63 vs limit 25 at L506)

タグのプレフィックス:

  • [new] — 修飾シンボル(行トレランスの範囲内)でも、--baseline-fuzzy-match 指定時のボディハッシュでも、この違反に一致するベースラインエントリがありません。ベースライン作成後に新たに発生した違反です。解決順序についてはマッチングを参照してください。
  • [regr +N%] — ベースラインに記録された値があり、現在の値がそれより N% 高い状態です。特殊なケース:
    • [regr from 0] — 記録された値が 0.0 で、非ゼロのパーセンテージがゼロ除算になる場合。
    • [regr +>9999%] — リグレッションがベースライン値の 100 倍を超えた時点でこの上限表記になります。
    • [regr NaN] — 現在のメトリクス値が NaN の場合(自明な関数における退化した Halstead 入力)。

Tags only appear when --baseline is passed; without it the line format is byte-identical to the no-baseline default. CI tooling that reads the merged streams can suppress the trailing summary with --no-summary.

サマリーフッターは違反をファイルごとにグループ化し、ファイルごとに最悪のメトリクス 1 件(value / limit 比の最大値)を提示し、行を違反数の降順、次にパスの昇順でソートします。長い違反リストを読み、どのファイルから着手すべきかを見つける最速の方法です。

Merging two branches

A baseline is generated wholesale, so a textual merge of two branches that both touched it is never right: each side's values were measured against its own tree, and the merged tree matches neither. Tell git not to try, with one line in .gitattributes:

.bca-baseline.toml -merge

Git then leaves the file conflicted as a whole instead of splicing the two sides together, and the resolution is always the same — regenerate:

bca check --write-baseline
git add .bca-baseline.toml

Unlike a merge=ours driver, -merge needs no per-clone git config, so it works for everyone who clones the repository rather than only for those who already knew to set it up.

6. ベースラインを退役させる

When .bca-baseline.toml contains only version = 6 and no entries, drop the --baseline flag from CI and delete the file. The thresholds now stand on their own.

ティア/ヘッドルームの来歴

--write-baseline(v5 以降)で書き込まれたベースラインは、「どのゲートに対して書き込まれたか」を [provenance] テーブルに記録します:

version = 6

[provenance]
tier = "soft"
headroom = 0.95
  • tier = "hard" — ハードゲート(bca check --write-baseline …)によって書き込まれたもの。headroom キーはありません。
  • tier = "soft", headroom = <ratio> — ソフト比率でスケールされたソフトゲート(bca check --tier=soft=0.95 --write-baseline …)によって書き込まれたもの。
  • tier = "soft"headroom なし — [thresholds.soft] テーブル(メトリクスごとの制限で、単一の比率なし)に駆動されるソフトゲートによって書き込まれたもの。

この来歴はコメントではなく本物の TOML テーブルなので、bca diff-baseline や外部ツールが読み取れます。古い bca(v2–v4)で書き込まれたベースラインは来歴を持ちませんが、エラーなく読み込まれます。

ベースラインより厳しい場合の警告

bca check は、ベースラインの来歴と現在の実行の実効制限を単一の「厳格度」スカラーに縮約し(ハード → 1.0h でスケールされたソフト → h、小さいほど厳しい)、現在の実行がベースライン書き込み時よりも*「厳しい」*場合に警告します:

warning: this check's effective limits (strictness 0.9) are stricter
than the baseline was written against (strictness 0.95); the baseline
may under-cover and the gate can fire on untouched files. Refresh it at
the matching tier, …

これはベースライン更新の規律が防ごうとしているサイレントな不整合です。現在のゲートより「緩く」書き込まれたベースラインは、より厳しいゲートが検出するすべての違反を網羅していない可能性があり、誰も触れていないファイルで突然ゲートが発火することがあります。

この警告は方向性を持ちます。現在の実行のほうが厳しい場合にのみ発火します。安全な方向では沈黙します — ハードチェック(厳格度 1.0)がソフト 0.95 のベースラインを読む場合、そこには自身の違反の「上位集合」が含まれており、これはまさに make self-scan(ハード)と make self-scan-headroom(ソフト)が同じ .bca-baseline.toml を通じてラチェットする、意図された単一ベースライン構成です。来歴が等しい場合、v5 より前のベースライン(来歴不明)の場合、どちらか一方が [thresholds.soft] テーブル方式のベースライン(比較すべき単一の比率がない)である場合も沈黙します。本当の警告を解消するには、対応する --write-baseline レシピを使って現在のティアでベースラインを更新します。

マッチングの仕組み

各エントリは (path, qualified_symbol, metric) をキーとします。修飾シンボルとは、囲んでいる名前付きコンテナを :: で連結した連鎖に関数名を加えたものです(MyStruct::do_thingmy_namespace::MyClass::method)。ファイルのトップレベルのスペースは <file> に畳み込まれます。違反は次の順序でベースラインに対して解決されます:

  1. 修飾シンボル。 違反の (path, qualified_symbol, metric) を共有するエントリがちょうど 1 つであれば、行番号に関係なく一致します。そのため、関数より上のコードを編集しても [new] として再キー化されることはなくなりました。

  2. 開始行トレランス。 複数のエントリがそのキーを共有する場合(アナライザが区別できなかった異なる impl ブロック上の同名メソッド is_valid、オーバーロードなど)、記録された start_line が違反に最も近く、かつ --baseline-line-tolerance 行以内(デフォルト 50)にあるエントリが選ばれます。トレランスを超えた違反は [new] になります。

    This is the only rule that reads a line number, so from schema v6 a start_line is written only for entries in such a group. Everywhere else the field is absent, and the file therefore does not change when an edit above a baselined function moves it — which is what keeps a real baseline change (a value moving) visible in a diff instead of buried in line-number noise.

  3. ボディハッシュ(オプトイン)。 --baseline-fuzzy-match を指定すると、修飾シンボルがもはや一致しない違反は、同じ (path, metric) 内で正規化されたボディハッシュが同一のエントリと照合されます。これにより、関数の形を保ったままのリネームを吸収できます(ダイジェストは関数自身の名前を除外し、インデント・空行・CRLF の影響を受けません)。ハッシュは --baseline-fuzzy-match が設定されている場合にのみベースラインに書き込まれるため、ファジー読み取りを有効にするには一度ファジーな --write-baseline でシードしてください。両方のキーは bca.toml[check] の下に baseline_line_tolerance および baseline_fuzzy_match として設定します(トップレベルに直接書く綴りは非推奨で警告が出ます。#599 を参照)。

匿名関数(クロージャ、ラムダ)には安定した名前がないため、その修飾シンボルには行番号が焼き込まれます(outer::<anon@L42>)。したがって移動すると [new] として再キー化されます。シンボルによる対応付けが行のずれに耐えるのは、ベースラインのチャーンの大半を生む「名前付き」のトップレベル関数とメソッドに束縛された関数だけです。

修復フッター

When the gate finds violations, bca check emits a trailing --- next steps --- block on stderr — the offender rows themselves go to stdout — and inside the $GITHUB_STEP_SUMMARY digest, that names the artifact, prints a copy-paste-safe --write-baseline refresh invocation, and links back to this recipe. The refresh invocation mirrors the gate's resolved --paths / --exclude / --exclude-from / --config / --baseline arguments, so a first-time reader of a failing CI log can refresh the baseline without leaving the page.

Suppress the block with --no-remediation if downstream tooling reads the merged streams and the trailing block confuses it.

抑制マーカーとの組み合わせ

--write-baseline は、bca: suppress または #lizard forgives マーカーで抑止された関数をあらかじめ除外するため、同じ関数が 2 か所に登録されることはありません。関数を恒久的に除外するつもりであれば、ソース内マーカーを優先してください(コードの隣に置かれ、リファクタリングに耐え、コミットすべき追加ファイルもありません)。ベースラインは、チームが本当に修正するつもりのある違反にのみ使ってください。

フィルタされていない違反の全体 — 抑制やベースラインに関係のないすべての違反 — を監査するには、--no-suppress を渡し、--baseline を省略します:

bca check --paths src/ \
    --no-suppress \
    --no-fail

--write-baseline と組み合わせると、--no-suppress は抑制マーカーが通常隠す違反も含め、すべての違反を記録します。

すべての除外を一度に監査する

ベースラインは、コードがゲートを逃れる 3 つの方法のうちの 1 つです。残りの 2 つは、ソース内の bca: suppress マーカーと [check.exclude] グロブです。bca exemptions はこの 3 層すべてを 1 つのレポートに列挙するため、レビュアーは 3 つのコマンドを実行することなく、bca check がスキップしているものを一覧できます:

bca exemptions --paths src/
# In-source markers (2)
  src/parser.rs:120  bca: suppress       metrics=all  parse_long
  ...

# [check.exclude] globs (1)
  tests/**

# Baseline (.bca-baseline.toml, 417 entries)
  src/markdown_report.rs write_language_section cognitive 29
  ...

ベースラインのセクションは、bca check と同じ --baseline / bca.toml[check] baseline ソース(またはデフォルトの .bca-baseline.toml)を読み取ります。ベースライン化された違反だけを列挙するには --baseline-only を、PR コメント用には --format markdown を、ダッシュボード用には --format json を使います。PR レビュー時には、前述の bca diff-baseline <old> <new> と組み合わせてください。diff はベースラインで何が「変わった」かを示し、bca exemptions は現在の除外面の全体を示します。完全なフラグリファレンスは抑制マーカーのページを参照してください。

制限事項

  • あいまいなシンボル。 2 つの関数が修飾シンボルを共有し(アナライザが異なるコンテナを解決できなかった、または言語がオーバーロードを許している)、かつ両方が記録された行から --baseline-line-tolerance を超えてずれている場合、どちらも判別できず、違反は [new] として現れます。--write-baseline で更新するか、トレランスを引き上げてください。
  • 匿名関数。 クロージャとラムダは、その合成シンボルに行番号が埋め込まれているため、移動すると再キー化されます(マッチングの仕組みを参照)。
  • OS 間の可搬性。 パスは書き込み時にスラッシュ区切りへ正規化され、読み取り時にも再正規化されるため、Linux で生成したベースラインは Windows 上の同じツリーと一致します。UTF-8 でないパスは損失のある表示形式にフォールバックし、正確にラウンドトリップしない場合があります。
  • しきい値の引き締め。 制限を下げると、これまでクリーンだった関数が新たに露出することがあります。それらはベースラインに含まれないため、CI は失敗します。これは正しい挙動です — 引き締めは新しい違反を露出させるべきです。チームが新しいエントリを吸収すると決めた場合は、ベースラインを更新してください。