ベースライン: 既存コードでしきい値をラチェットする
既存のコードベースにメトリクスのしきい値を導入すると、たいてい同じ壁にぶつかります。妥当なしきい値はどれも既存の関数を何百件もフラグし、CI はプッシュのたびに赤になります。現実的な採用経路は「現状からラチェットし、新規の違反者だけを失敗させる」ことです。ベースラインファイルは、bca check がそのワークフローを支えるための仕組みです。
ベースラインはソース内の抑制マーカーを補完するものであり、その代替ではありません。関数が意図的に恒久的に複雑な場合(パーサー、状態機械、生成コード)は抑制マーカー(抑制マーカー)を使ってください。チームが負債を返済していくつもりの場合はベースラインを使います。両方を同じリポジトリで併用でき、抑制の方が先に評価されます。
エンドツーエンドの採用フロー
ワンショットのショートカット:
bca initは、統合されたbca.tomlマニフェスト(paths、exclude_from、baseline、[thresholds]テーブルを含む)、それが参照する.bcaignore、そして現在のツリーから導出した初期.bca-baseline.tomlを 1 つのコマンドでスキャフォールドします。マニフェストが配置されていれば、引数なしのbca checkがそれを自動発見し、設定なしでゲートします。既存ファイルを上書きするには--forceを、ツリーの走査をスキップするには--no-baselineを渡します。以下の長めのレシピは、ベースラインをブートストラップする前にしきい値を調整したい場合に有用です。
1. 初期しきい値を決める
感覚的な数値(cyclomatic=15、cognitive=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.toml の baseline キーに書き込むため、ファイル名は 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 が現れるのは実際の違反箇所が変わったときだけです。
You do not have to guess when a refresh is due. A gated run whose offenders have measured better than their recorded values says so on stderr — see the stale-entry warning — because the difference between recorded and live is suppression nobody chose.
制限を引き締める前に、両方の層でコストを見積もる
負債を返済すると、その負債を生んだ制限を引き締めたくなります。しかし、すぐに思い付く測定 — bca check --threshold <metric>=<candidate> — は、問いの半分にしか答えません。--threshold の値は最後に絶対値として適用され、決してスケールされないため、ソフト層がありません。ハードゲートでは何のコストもかからない候補でも、母集団全体を恒久的に --tier=soft の帯の内側に置いてしまうことがあり、それは bca.toml を編集してもう一方のゲートを実行するまで見えません。
bca check --explain-threshold cognitive=15
見るべきは違反者数ではなく new 列です。これはその変更が追加することになるベースラインエントリの数であり、レビュアーが実際に承認を求められているものです。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. A recorded offender got better without dropping out of the baseline — a good sign the refactor is working, and the reason to land the refreshed file rather than leave it. Until it is refreshed, the old entry keeps suppressing the offender up to a value the tree no longer produces.
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%]— the baseline contains a recorded value and the current value isN%worse. "Worse" is measured in the metric's own direction, so for the lower-is-worsemi.*family a drop ofN%below the recorded value reads+N%like any other regression; the tag has one shape across every metric, which is what makes it safe to grep for. Cases:[regr from 0]— 記録された値が0.0で、非ゼロのパーセンテージがゼロ除算になる場合。[regr +>9999%]— リグレッションがベースライン値の 100 倍を超えた時点でこの上限表記になります。[regr NaN]— 現在のメトリクス値が NaN の場合(自明な関数における退化した Halstead 入力)。
タグは --baseline を渡したときにのみ表示されます。渡さない場合、行のフォーマットはベースラインなしのデフォルトとバイト単位で同一です。統合されたストリームを読む CI ツールは、--no-summary で末尾のサマリーを抑制できます。
サマリーフッターは違反をファイルごとにグループ化し、ファイルごとに最悪のメトリクス 1 件(value / limit 比の最大値)を提示し、行を違反数の降順、次にパスの昇順でソートします。長い違反リストを読み、どのファイルから着手すべきかを見つける最速の方法です。
2 つのブランチのマージ
ベースラインは丸ごと生成されるものなので、双方が触れた 2 つのブランチの「テキスト上の」マージが正しくなることはありません。各側の値はそれぞれ自分のツリーに対して測定されたものであり、マージ後のツリーはどちらにも一致しません。.gitattributes の 1 行で、git に試みるなと伝えてください:
.bca-baseline.toml -merge
これで git は、両側を継ぎ合わせる代わりにファイル全体を競合状態のまま残します。解決方法は常に同じで、再生成です:
bca check --write-baseline
git add .bca-baseline.toml
merge=ours ドライバーと異なり、-merge はクローンごとの git config を必要としないため、設定方法をすでに知っていた人だけでなく、リポジトリをクローンする全員に対して機能します。
6. ベースラインを退役させる
.bca-baseline.toml が version = 6 のみでエントリを含まなくなったら、CI から --baseline フラグを外してファイルを削除します。以降はしきい値だけで成立します。
ティア/ヘッドルームの来歴
--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.0、h でスケールされたソフト → 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 レシピを使って現在のティアでベースラインを更新します。
The stale-entry warning
The ratchet suppresses a violation for as long as it has not worsened past its recorded value, so everything on the improving side classifies alike: a function measuring 5 against a recorded 7 is treated exactly like one still measuring 7. The two points between them are gate headroom nobody chose — the function can grow back to 7 and the gate will not notice.
A gated run reports that drift in one stderr line:
warning: 3 baseline entries improved past the recorded value (worst:
src/spaces/compute.rs::metrics_inner halstead.effort 119147.75 →
116715.61); that gap is gate headroom nobody chose, so refresh with
`--write-baseline`. …
It names one example rather than every entry, because the response to any number of them is the same wholesale refresh. Direction follows the metric: for the lower-is-worse mi.* family the stale direction is a rise above the record. An entry sitting exactly on its recorded value is silent, so a freshly written baseline never warns about itself.
It finds only half of the staleness. A function that stopped breaching its limit altogether produces no violation at all, so it never reaches the baseline matcher and cannot be counted here. Its entry stays in the file, inert and invisible, until someone regenerates. Closing that half needs a scheduled --write-baseline run whose output is diffed against the committed file; the warning covers only the offenders still above their limits.
マッチングの仕組み
各エントリは (path, qualified_symbol, metric) をキーとします。修飾シンボルとは、囲んでいる名前付きコンテナを :: で連結した連鎖に関数名を加えたものです(MyStruct::do_thing、my_namespace::MyClass::method)。ファイルのトップレベルのスペースは <file> に畳み込まれます。違反は次の順序でベースラインに対して解決されます:
-
修飾シンボル。 違反の
(path, qualified_symbol, metric)を共有するエントリがちょうど 1 つであれば、行番号に関係なく一致します。そのため、関数より上のコードを編集しても[new]として再キー化されることはなくなりました。 -
開始行トレランス。 複数のエントリがそのキーを共有する場合(アナライザが区別できなかった異なる
implブロック上の同名メソッドis_valid、オーバーロードなど)、記録されたstart_lineが違反に最も近く、かつ--baseline-line-tolerance行以内(デフォルト 50)にあるエントリが選ばれます。トレランスを超えた違反は[new]になります。行番号を読むルールはこれだけなので、スキーマ v6 からは
start_lineはそのようなグループに属するエントリにだけ書き込まれます。それ以外の場所ではこのフィールドは存在しません。そのため、ベースライン化された関数より上での編集で関数が移動してもファイルは変化しません — これにより、本当のベースライン変更(valueの変動)が行番号ノイズに埋もれず、diff で見えるままになります。 -
ボディハッシュ(オプトイン)。
--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] として再キー化されます。シンボルによる対応付けが行のずれに耐えるのは、ベースラインのチャーンの大半を生む「名前付き」のトップレベル関数とメソッドに束縛された関数だけです。
修復フッター
ゲートが違反を検出すると、bca check は stderr に(違反行そのものは stdout に出力されます)、および $GITHUB_STEP_SUMMARY ダイジェスト内に、末尾の --- next steps --- ブロックを出力します。このブロックは対象アーティファクトを名指しし、そのままコピー&ペーストできる --write-baseline の更新コマンドを提示し、このレシピへのリンクを示します。更新コマンドはゲートが解決した --paths / --exclude / --exclude-from / --config / --baseline 引数をそのまま反映するため、失敗した CI ログを初めて読む人でも、ページを離れることなくベースラインを更新できます。
下流のツールが統合されたストリームを読んでいて末尾のブロックが混乱を招く場合は、--no-remediation でブロックを抑制します。
抑制マーカーとの組み合わせ
--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 は失敗します。これは正しい挙動です — 引き締めは新しい違反を露出させるべきです。チームが新しいエントリを吸収すると決めた場合は、ベースラインを更新してください。