変更履歴(VCS)メトリクス
bca vcs は、ソースの AST(抽象構文木)ではなくバージョン管理履歴から導かれるシグナルである変更履歴リスクでファイルをランク付けします。これはプロジェクト初の、言語非依存かつ非 AST のメトリクスファミリーです。目的は、欠陥予測・脆弱性予測の実証研究文献が最も一貫して裏付けているシグナルを用いて、バグや脆弱性を抱えている可能性が最も高いファイルを浮かび上がらせることです。
履歴の走査は呼び出しごとに 1 回だけ実行され(ファイルごとには決して実行されません)、設定可能な 2 つのウィンドウ — 長期ウィンドウ(デフォルト 12mo ≈ 365 日)と直近ウィンドウ(デフォルト 90d)— にわたるファイルごとのシグナルを生成します。
クイックスタート
$ bca vcs --paths src --top 20
Change-history risk (long window 365d, recent 90d, formula v2)
RANK RISK COMMITS rec/long CHURN rec/long AUTHORS long FILE
1 7.2 68/68 11634/11634 1 src/metrics/cyclomatic.rs
2 6.9 68/68 7299/7299 1 src/metrics/npa.rs
...
--format を指定しない場合、人間が読めるランク付きテーブルが出力されます。レンダリングされたレポートページには --format markdown|html を、構造化出力には --format json|yaml|toml|cbor|csv を渡します。bca metrics / bca ops(これらの --output-dir はファイルごとの出力を格納する「ディレクトリ」です)と異なり、変更履歴レポートはリポジトリ全体で 1 つの文書なので、bca vcs --output <file> は1 つのファイルを書き出します(バイナリ形式である CBOR には --output が必須です)。グローバルな --paths / --include / --exclude / --no-ignore フィルタは、どの追跡ファイルをレポートするかの選択に再利用されます。
bca vcs を git 作業ツリーの外で実行すると、明確なエラーになります。
ファイルタイプのスコープ
デフォルトでは、bca vcs は bca がメトリクスを計算するファイルのみ — bca metrics が解析するのと同じ集合 — をランク付けします。チャーンの多い非ソースファイル(CHANGELOG.md、Cargo.lock、生成された設定ファイル)は保守性の観点では意味を持たない一方でチャーン / コミット / 作者のシグナルを最大化するため、ソースコードと並べてランク付けするとノイズになります。メトリクス対象ファイルへのスコープ設定には、スタンドアロンのランキングを bca report --vcs の AST ホットスポットテーブルと揃えておく効果もあります。
--file-types <SCOPE> でスコープを選択します。
| 値 | 意味 |
|---|---|
metrics (デフォルト) | bca が言語 / メトリクスを持つファイルのみ(拡張子で判定) |
all | 追跡中の、バイナリでもシンボリックリンクでもないすべてのテキストファイル |
rs,py,toml,… | カンマ区切りの拡張子許可リスト(先頭のドットは省略可、大文字小文字は区別しない) |
bca vcs # ソースファイルのみをランク付け(デフォルト)
bca vcs --file-types all # 追跡中のすべてのテキストファイルをランク付け
bca vcs --file-types rs,py # Rust と Python のファイルのみをランク付け
この判定は拡張子のみで行われ(ファイル内容は読み取りません)、--paths / --include / --exclude / --no-ignore フィルタと AND 条件になります — ランク付けされるには、ファイルが両方を通過する必要があります。拡張子のないファイル(Makefile、Dockerfile、LICENSE)や未知の拡張子は metrics スコープの対象外です。カスタムリストはリテラルな拡張子フィルタなので、toml のようなメトリクス対象外のタイプも含められます。空または空白のみのカスタムリストは、何もランク付けせず沈黙するスコープになるのではなく、明確なエラーになります。
レンダリングされたレポートページ
bca vcs --top 50 --format html --output vcs.html
bca vcs --top 50 --format markdown --output vcs.md
--format html は、bca report html とまったく同じスタイルの、自己完結型でソート可能なページを生成します(任意の列ヘッダーをクリックすると並べ替えられます)。--format markdown は同じランク付きテーブルを GitHub-Flavored Markdown として生成します。どちらもすべてのシグナル列をレンダリングします(構造化フォーマットが持つのと同じデータの、完全でソート可能なビューです)。列セットは一度だけ定義され両方のレンダラーで共有されるため、両者が乖離することはありません。
ランキングをスタンドアロンページではなく集約品質レポートに組み込むには、bca report --vcs を渡します。これは report markdown / report html に「Change-history risk」セクションを追加します。
シグナル
| フィールド | 型 | 説明 |
|---|---|---|
commits_long / commits_recent | u32 | 各ウィンドウでファイルに触れた個別コミット数 |
churn_long / churn_recent | u64 | 各ウィンドウにおける Σ(added + deleted) 行数 |
authors_long / authors_recent | u32 | 各ウィンドウにおける正規化済み作者 ID の個別数 |
ownership_top_share | f64 ∈ [0,1] | トップ作者に帰属する編集の割合(低いほど所有権の希薄化が進んでいる) |
burst | f64 ∈ [0,1] | commits_recent / commits_long |
bug_fix_commits | u32 | メッセージがバグ修正キーワードに一致する長期ウィンドウのコミット数 |
security_fix_commits | u32 | セキュリティキーワード(CVE-####、security、vuln、exploit、sanitize など)に一致する長期ウィンドウのコミット数 |
revert_commits | u32 | 件名がリバート / ロールバックである長期ウィンドウのコミット数 |
age_days | u32 | ファイルのウィンドウ内で最初のコミットからの経過日数(長期ウィンドウが上限) |
last_modified_days | u32 | ファイルのウィンドウ内で最も新しいコミットからの経過日数 |
change_entropy_long / change_entropy_recent | f64 | ウィンドウごとの変更エントロピー(ビット単位、下記参照) |
cochange_entropy_long / cochange_entropy_recent | f64 | ウィンドウごとの共変更グラフエントロピー(ビット単位、下記参照) |
risk_score | f64 | 複合スコア。式はバージョン管理されます(下記参照)— 順序尺度であり、基数尺度ではない |
hotspot_score | f64? | complexity × churn_recent。AST メトリクスが同時に計算される場合のみ存在します |
risk_score_version / vcs_schema_version | u32 | 前方互換性のためのバージョンスタンプ。long_window_days / recent_window_days と並んでレポートのエンベロープに 1 回だけ付与され、ファイルごとの vcs ブロック内では繰り返されません(issue #635) |
作者 ID はリポジトリの .mailmap を通じて正規化され、小文字化したメールアドレスで数えられます。Co-authored-by: トレーラーは参加者を追加します。ボット ID(dependabot[bot]、renovate[bot]、github-actions[bot] など)はデフォルトで除外されます。バイナリファイルとシンボリックリンクはスキップされます。未追跡ファイルにはレコードがまったく存在しません(ウィンドウ内の活動がゼロの追跡ファイルとは区別されます)。
変更エントロピーと共変更エントロピー
2 つのプロセスエントロピーシグナル(risk_score_version 2 で追加)は、ファイルの変更量だけでなく、「どのように」変更されるかを捉えます。
- 変更エントロピー(Hassan, 2009 — Predicting Faults Using the Complexity of Code Changes)。各コミットについて、そのコミットが触れたファイル群にわたるチャーン分布の Shannon エントロピー(ビット単位)が、その変更がどれだけ「分散」していたかを測ります。1 ファイルのみのコミットは 0 で、n 個のファイルへ均等にチャーンを広げるコミットは log₂(n) に近づきます。次に、各ファイルには参加した各コミットのチャーンシェア
pᵢ·Hがクレジットされます(Hassan の History Complexity Metric)。値が高いほど、そのファイルは拡散的で横断的な変更に繰り返し巻き込まれていることを意味します。後続研究(下記の arXiv 2504.18511)では、8 つの Apache プロジェクトにおいてファイルレベルの変更エントロピーと欠陥数との Pearson 相関が最大 0.54 と測定されました。 - 共変更グラフエントロピー(arXiv 2504.18511, 2025)。同じコミットで変更されたファイル同士は、重み付きエッジ(重み = 共有コミット数)で結ばれます。ファイルの共変更エントロピーは、そのエッジ重み分布の Shannon エントロピーです。常に同じ相手と共変更される場合は低く、変更が多くの異なるファイルへ波及する場合は高くなります。変更エントロピーと組み合わせることで、8 つの Apache プロジェクトにおいて v1 シグナルセットに対し 82.5% のケースで AUROC を改善しました。
どちらもウィンドウごとに報告されます。0.0 は欠損値ではなく計算された値です。そのファイルが常に単独で変更されていた(共変更の隣接ファイルがない、または変更エントロピーがゼロの単一ファイルコミットのみ)ことを意味します。1000 ファイル超に触れる一括インポートコミットは、エッジ数が O(width²) で増えるため共変更グラフからは除外されますが、O(width) の変更エントロピーには引き続き寄与します。
複合リスクスコア
デフォルトの加重(weighted) 式は、カテゴリ別の乗法的ボーナスを伴う、対数スケールの加重和です。
recency_churn = ln(1 + churn_recent)
recency_count = ln(1 + commits_recent)
long_count = ln(1 + commits_long)
long_churn = ln(1 + churn_long)
author_factor = ln(1 + authors_long)
dilution = (1 - ownership_top_share).clamp(0, 1)
fix_factor = ln(1 + bug_fix_commits + 2 * security_fix_commits)
size_factor = ln(1 + sloc)^2 / 100 // タイブレーカーではなく本来の係数
entropy_factor = 0.10 * change_entropy_recent + 0.05 * cochange_entropy_recent
new_file_bonus = 0.15 if age_days < recent_window_days else 0
dev_bonus = 0.35 if authors_long >= 9 else 0.15 if authors_long >= 6 else 0
base = 0.30 * recency_churn
+ 0.25 * recency_count
+ 0.15 * long_count
+ 0.15 * author_factor * (1 + dilution)
+ 0.10 * fix_factor
+ 0.05 * long_churn
+ entropy_factor
+ size_factor
risk_score = base * (1 + dev_bonus + new_file_bonus)
各項の重みは文献に基づいています。直近のチャーンとコミット頻度が最大の重みを持ちます(Nagappan & Ball の相対チャーン、just-in-time 欠陥予測、Firefox の NumChanges PD 86)。作者ファクターは所有権の希薄化でスケールされます(Avelino の DoA / トラックファクター、Bird ら)。カテゴリ別の開発者数ボーナスは、9 人以上の開発者が触れたファイルは脆弱性を抱える可能性が約 16 倍だったという RHEL4 の知見をエンコードしています。セキュリティ修正には 2 倍の重みが付きます(Sentence-Level VFC 研究、PySecDB)。そして直近ウィンドウの変更エントロピーおよび共変更エントロピーの項は加算的に入ります(Hassan 2009、arXiv 2504.18511)。完全な導出は src/vcs/score.rs にあります。
このスコアは順序尺度であり、相対的な順位のみが意味を持ちます。単一の risk_score_version(現在は 2)が両方の式をバージョン管理します。加重和「または」 --risk-formula percentile ブレンドのどちらを変更してもバージョンが上がります。直近のエントロピー 2 項は両方の式に加わっています。
--risk-formula percentile は代替の式です。各シグナルを解析対象集合内のパーセンタイルに再ランク付けし、その平均を取ります — 文献は、プロジェクト横断の頑健性のために、固定しきい値よりも相対的なトリガーを推奨しています。
フラグ
| フラグ | デフォルト | 意味 |
|---|---|---|
--long-window <DUR> | 12mo | 長期ウィンドウ(12mo、2y、8w、365d、ISO 8601 の P1Y) |
--recent-window <DUR> | 90d | 直近ウィンドウ |
--top <N> | 50 | 上位 N 件のみ表示(0 = すべて) |
--file-types <SCOPE> | metrics | ランク付け対象のファイル: metrics、all、または拡張子リスト(rs,py) |
--ref <REF> | HEAD | 解析対象のリビジョン |
--full-history | オフ | DAG 全体を走査(デフォルト: first-parent のみ) |
--include-merges | オフ | マージコミットを含める |
--no-follow-renames | オフ | リネームの追跡を止める(デフォルト: 追跡する) |
--no-exclude-bots / --bot-pattern <RE> | 除外 | ボット作者のフィルタリング |
--as-of <WHEN> | 現在時刻 | 再現可能なスナップショットのための基準「now」(RFC 3339 / @unix / git 日付形式) |
--risk-formula {weighted\|percentile} | weighted | 複合スコアの式 |
--emit-author-details | オフ | SHA-256 でハッシュ化した正規作者 ID を出力 |
--author-hash-key <KEY> | 未設定 | 出力される作者ダイジェストを鍵付き HMAC に強化する(作者詳細のプライバシーを参照)。--emit-author-details が必要 |
--include-deleted | オフ | 対象 ref で削除済みのファイルもランク付けする |
--no-cache | オフ | 永続履歴キャッシュをスキップする(常に新規に走査) |
--clear-cache | オフ | 実行前にこのリポジトリのキャッシュ済み履歴を消去する |
--cache-dir <DIR> | プラットフォーム標準のキャッシュ | キャッシュディレクトリを上書きする |
キャッシュ
ランキングが再走査するのは長期ウィンドウ内の履歴部分だけですが、大規模で活発なリポジトリではそれでも支配的なコストです — そして CI では、実行間で意味のある差分は前回以降にプッシュされたコミットだけです。そのため bca vcs は、解決済みの HEAD SHA とリポジトリの同一性をキーとして、各走査の永続キャッシュを保持します。
- 変更のないツリーでは前回の結果が再生され、履歴走査は行われません。
HEADが前進した場合、走査は新しいコミットのみを訪れ、それをキャッシュ済み履歴に継ぎ足します。- フォースプッシュ(キャッシュされた head が新しい head の祖先でなくなった状態)では、完全走査にフォールバックします。
このキャッシュは純粋な最適化です。キャッシュヒットは新規の走査とビット単位で同一であり、時間ウィンドウは実行のたびに「現在」時刻に対して再計算されるため、キャッシュされた結果が古くなることはありません。スキーマ、スコア式のバージョン、または「走査に影響する」オプションが異なる場合、エントリは無視され履歴が再計算されます。特に、ウィンドウを変更すると新規の走査が強制されます。(--risk-formula、--emit-author-details、--author-hash-key、--include-deleted のような最終化のみのノブは再生時に適用されるため、同じキャッシュ済み走査を再利用します — キャッシュ済み走査は、「異なる」作者ハッシュ鍵の下でも再走査なしで再最終化されます。)
デフォルトでは、キャッシュは $XDG_CACHE_HOME/big-code-analysis/vcs の下に置かれます(Windows では %LOCALAPPDATA%、それ以外では ~/.cache)。作者 ID は SHA-256 ダイジェストとしてのみ保存され、平文では決して保存されないため、キャッシュに生の作者メールアドレスは含まれません。これは匿名化ではなく仮名化である点に注意してください。ダイジェストは候補となるメールアドレス集合に対して復元可能です(--emit-author-details を参照)。同じキャッシュは bca metrics --vcs と bca report --vcs も透過的に高速化します。
# 最初の実行がキャッシュを準備し、2 回目はそれを再生します。
bca vcs --paths .
bca vcs --paths . # 前回の作業を再利用
bca vcs --no-cache --paths . # この実行ではキャッシュを無視
bca vcs --clear-cache --paths . # ゼロから再構築
bca vcs --cache-dir /tmp/bca-cache --paths .
REST(POST /v1/vcs)と Python(vcs.rank)の各インターフェースは、省略可能な no_cache / cache_dir パラメータを通じて同じ挙動を提供します。
このキャッシュはファイルランキング専用です。trend と commit の各サブコマンド — および /v1/vcs/trend と /v1/vcs/jit エンドポイント — はキャッシュを使用しないため、キャッシュ関連のフラグはそこでは適用されません。サブコマンドと併せて --no-cache / --cache-dir を渡すと使用方法エラーになり、trend エンドポイントは no_cache / cache_dir フィールドを黙って無視するのではなく拒否します(issue #961)。
bca metrics での利用
bca metrics --vcs を渡すと、各ファイルの metrics に vcs ブロック(およびファイルの cyclomatic 合計から計算される hotspot_score)が付加されます:
$ bca metrics --vcs --paths src/parser.rs --format json
{ "name": "src/parser.rs",
"metrics": { "cyclomatic": { ... },
"vcs": { "commits_long": 15, "churn_recent": 211,
"risk_score": 3.7, "hotspot_score": 7596.0, ... } } }
bca metrics --vcs はデフォルトのウィンドウと重み付き計算式を使用します。ウィンドウや計算式を調整したい場合は bca vcs を使用してください。
関数単位の帰属
bca metrics --vcs-per-function(--vcs を暗黙に含みます)は、さらに入れ子になったすべての関数・メソッド・クラスのスペースに vcs ブロックを付加します。各ファイルを git blame で一度だけ blame し、現存する行を AST の関数スパンに振り分けるため、リスクの高いファイルの中のリスクの高い「関数」をランク付けできます:
$ bca metrics --vcs-per-function --paths src/parser.rs --format json
{ "name": "src/parser.rs",
"metrics": { "vcs": { "risk_score": 3.7, ... } }, // ファイルレベルのブロック
"spaces": [
{ "name": "parse", "kind": "function",
"metrics": { "vcs": { "commits_long": 4, "churn_recent": 12,
"risk_score": 2.1, "hotspot_score": 144.0 } } } ] }
関数単位のブロックは現時点の blame のスナップショットであり、ファイルレベルのブロックと直接比較「できません」。その churn は、最終変更がウィンドウ内に収まる現存行を数えるものであり(過去の追加+削除のチャーンではありません)、所有権は変更したコミットごとに加算されます。ウィンドウ内で誰も変更していない関数のカウントはゼロと報告されます。最終変更が長期ウィンドウより前の行は、関数のサイズには寄与しますが、ウィンドウ付きのどのカウントにも寄与しません。
制限事項。 blame はファイルのリネームを追跡します(そのため旧パスでの編集も帰属します)が、「関数間を移動した」行は現在の位置にのみ帰属します。2 つに分割された関数には分割前のアイデンティティの記録はなく、削除後に再作成された関数は再作成したコミットに帰属します。ファイルが blame できない場合 — 未追跡の場合や、病的に反復的な内容に対するまれな gix-blame の失敗の場合 — その関数単位のブロックは単に省略され、ファイルレベルのブロック(と AST メトリクス)は引き続き出力されます。
ジャストインタイム(コミットレベル)スコアリング
ここまでの機能がリファレンス時点の「ファイル」をランク付けするのに対し、bca vcs commit <commit> は単一の「コミット」の欠陥誘発リスクをスコアリングします — これは CI ゲートがチェックイン時にレビューする単位です。(このサブコマンドは 2.0 で bca vcs jit から改名されました。旧称の jit は 1 リリースサイクルの間、隠しエイリアスとして引き続き動作します。「ジャストインタイム(JIT)」は以下でも文献上の用語として残ります。)これは静的なルールベースのスコアラーで(学習済みモデルを使わないため、プロジェクトが古くなっても何もドリフトしません)、特徴量グループと符号はジャストインタイム欠陥予測の文献から採られています:Kamei et al., A Large-Scale Empirical Study of Just-in-Time Quality Assurance, IEEE TSE 2013、およびそのオープンな追試である Commit Guru(FSE 2015)と McIntosh & Kamei, Are Fix-Inducing Changes a Moving Target?(IEEE TSE 2018)です。
$ bca vcs commit HEAD --pretty
{
"jit_schema_version": 3,
"jit_score_version": 1,
"source": "commit",
"risk_score": 4.40,
"commit": { "id": "5176d3e…", "parent_count": 1, "is_merge": false,
"purpose": { "is_fix": true, "is_security_fix": false,
"is_revert": false } },
"features": {
"size": { "lines_added": 942, "lines_deleted": 60,
"files_touched": 19, "hunks": 78 },
"diffusion": { "subsystems": 5, "directories": 8, "entropy": 3.48 },
"history": { "prior_changes": 275, "prior_distinct_authors": 1,
"prior_bug_fix_commits": 237,
"prior_security_fix_commits": 21,
"file_risk_max": 10.97, "file_risk_mean": 3.87,
"new_files": 2 },
"experience": { "author_prior_commits": 962,
"author_recent_commits": 962 }
},
"contributions": { "size": 2.74, "diffusion": 0.97, "history": 1.57,
"purpose": 0.15, "experience": -1.03 }
}
5 つの特徴量グループと、それぞれがスコアをどう動かすか:
| グループ | 特徴量 | 方向 |
|---|---|---|
| サイズ | 追加 / 削除行数、変更ファイル数、diff ハンク数 | 大きいほど ⇒ 高リスク |
| 拡散度 | 個別のサブシステム数とディレクトリ数、コミット内の変更エントロピー | 分散しているほど ⇒ 高リスク |
| 履歴 | 変更対象ファイルの事前値 — 過去の変更回数、個別の作者数、バグ修正・セキュリティ修正の回数、および複合 risk_score — をコミット「以前」の履歴から測定 | 荒れたファイル履歴 ⇒ よりリスキー |
| 経験 | 作者の過去のコミット数(長期・直近) | 経験が多いほど ⇒ リスクは低下(このグループは減算) |
| 目的 | メッセージの修正 / セキュリティ修正 / リバート分類 | 修正は加算、リバートは減衰 |
contributions ブロックは、順序尺度である risk_score への各グループの符号付き寄与を報告するため、コミットが「なぜ」その順位になったのかを利用側が確認できます。ファイルレベルの risk_score と同様、このスコアは順序尺度です。スコアでコミットをランク付けしたり、リポジトリ自身の分布とコミットを比較したりできますが、その大きさを確率として読み取らないでください。計算式が変更されると jit_score_version が上がります(ファイルレベルの risk_score_version とは別です)。
コミットは第一親に対してスコアリングされます。*「マージ」*コミットにはフラグが立てられ(is_merge、parent_count ≥ 2)、その第一親に対してスコアリングされます。ルートコミットと新規ファイルは、構造上、事前値がゼロになります — その場合スコアはサイズと作者の経験に依存します。これはファイル履歴のない変更に対して文献が定めるとおりの挙動です。
ウィンドウ / --ref / ボット / マージ / リネームの各フラグは親コマンドの bca vcs と共有されます。commit 専用のフラグは、位置引数の <commit>(デフォルト HEAD)、--format json|yaml|toml|cbor(デフォルト json)、--output、--pretty、および次のとおりです:
# CI ゲート:コミットのスコアがしきい値以上のとき exit 2 で終了する。
bca vcs commit HEAD --fail-above 6.0
--fail-above は終了コード 2 を使用します(bca check と同じ「メトリクスゲート」の慣例です。終了コード 1 はツールエラー用に予約されたままです)。スコアは順序尺度なので、しきい値は絶対値として扱うのではなく、リポジトリ自身のコミットスコア分布に対して調整してください。
任意の diff のスコアリング(--diff)
bca vcs commit --diff <file> はコミットの代わりに git diff をスコアリングします(標準入力から diff を読むには --diff - を使用します)。変更がまだコミットされておらず diff としてのみ存在する pre-commit フックやコードレビューボットで便利です。
git diff --cached | bca vcs commit --diff - --pretty
入力は、git diff や git format-patch が生成する、diff --git ファイルヘッダーを持つ git 形式の unified diff でなければなりません。素の diff -u / diff -ru の出力(---/+++ ヘッダー行はあるが diff --git ヘッダーがないもの)は 0 ファイルとしてパースされ、combined / マージ diff(@@@ ハンクヘッダーを持つ git diff --cc)は不正な diff として拒否されます — 代わりに通常の 2-way git diff をパイプしてください。
素の diff には作者・親・ファイル履歴の情報が一切ないため、計算できるのは sizeとdiffusion のグループだけです。したがって出力は意図的に「部分的な」レポートであり、コミットレポートとは異なる形をしています:
$ git diff | bca vcs commit --diff - --pretty
{
"jit_schema_version": 3,
"jit_score_version": 1,
"source": "diff",
"partial_risk_score": 1.83,
"size": { "lines_added": 42, "lines_deleted": 8,
"files_touched": 3, "hunks": 6 },
"diffusion": { "subsystems": 2, "directories": 3, "entropy": 1.46 },
"contributions": { "size": 1.18, "diffusion": 0.65 }
}
source フィールドは恒久的な "diff" マーカーであり、history / experience / purpose の各グループはレポートから完全に欠落します — ゼロとして現れるのでは「ありません」。ゼロは実際の値です(本当に事前履歴のないコミットは、これらのグループをゼロとしてスコアリングします)。欠落したグループは「利用不可」を意味するため、利用側がスコアリングされていないグループを「低リスク」と取り違えることはありません。同じ理由で、スコアのフィールド名は risk_score ではなく partial_risk_score です。
diff のみのスコアはコミットスコアと比較できません。 部分スコアは size + diffusion のみを合計するため、同じ変更に対する完全なコミットスコア(history、experience、purpose も織り込まれます)より常に低くなります。diff は「他の diff」 に対してランク付けし、決してコミットスコアと比較しないでください。
--diffと位置引数の<commit>は同時に指定できません。--fail-aboveは両方のモードで機能します(diff モードのしきい値は自身の diff スコア分布に対して調整してください)。
パーサーは git のデフォルトである C 形式のパス引用(core.quotePath=true)を理解するため、非 ASCII 文字や空白を含む名前のファイル(git は "a/na\303\257ve.txt" のように出力します)に触れる diff は、diffusion の特徴量では引用付きの生文字列ではなく、デコードされたパスの下にグループ化されます。
REST と Python のパリティ
JIT スコアは CLI 以外からも利用できます:
- REST:
POST /v1/vcs/jitに{ "id", "repo_path", "commit" }を渡すとコミットのJitReportJSON が返り、{ "id", "diff" }を渡すと部分的な diff レポートが返ります。REST API の利用を参照してください。 - Python:
vcs.commit(repo_path, commit=...)はコミットレポートをdictとして返し、vcs.score_diff(diff)は部分的な diff レポートを返します。変更履歴(VCS)メトリクスを参照してください。
ML ベースの JIT モデルとサーバーサイドフック統合は、引き続きスコープ外です。
履歴トレンド(時系列)
単発の bca vcs 実行は 「いま何がリスキーか」 に答えます。bca vcs trend は、複数の時点でメトリクスをサンプリングしてファイルごとの時系列を出力することで、「良くなっているのか悪くなっているのか」 — 技術的負債プログラムにとって行動につながる問い — に答えます。
$ bca vcs --top 20 trend --points 12 --span 24mo --pretty
{
"trend_schema_version": 1,
"vcs_schema_version": 2,
"risk_score_version": 2,
"long_window_days": 365,
"recent_window_days": 90,
"truncated_shallow_clone": false,
"as_of_points": [ 1700000000, 1705259520, ... ],
"files": {
"src/parser.rs": [
null, // 最古の時点では存在しなかった
{ "as_of": 1705259520, "vcs": { "risk_score": 4.1, ... } },
{ "as_of": 1710519040, "vcs": { "risk_score": 6.8, ... } }
]
},
"deltas": {
"improved": [ { "path": "src/old.rs", "delta": -3.2, ... } ],
"regressed": [ { "path": "src/parser.rs", "delta": 2.7, ... } ]
}
}
--points N 個の等間隔サンプル(両端点を含む)が --span DURATION をカバーし、--as-of(指定がなければ現在時刻)で終わります。as_of_points はサンプルのタイムスタンプを古い順に列挙し、各ファイルの配列はそれと 1:1 で対応します。null 要素は、その時点でファイルがまだ存在しなかったことを示します。deltas は、各ファイルの最初と最後に存在した時点の間で risk_score が最も下がったファイル(improved)と最も上がったファイル(regressed)をランク付けします。--top-deltas で各リストを切り詰められます。
重要なのは、各時点がその瞬間かそれ以前に存在したメインラインの先端に再アンカーすることです — 今日の HEAD ツリーのウィンドウを引き直すだけではありません。これにより、後から生まれたファイルは古い時点で null として表示されます(現在のメトリクスが過去に漏れ出すことはありません)。系列に残されるファイルは、最新サンプルの時点でリスクが最も高い --top 件です。
親コマンド bca vcs から再利用されるフラグ:ウィンドウ(--long-window / --recent-window)、--ref、--file-types、ボット / マージ / リネームのトグル、--as-of(最新のアンカー)、および --top。-O は json(デフォルト)、yaml、cbor を受け付けます。存在しない時点は null としてシリアライズされ、TOML はこれを表現できないため、TOML は除外されています。時点数は、深い履歴でも時点ごとの履歴走査が扱いきれる範囲に収まるよう、2–120 に制限されています。
リネームに関する注意。 リネームは各サンプルの走査「内」では追跡されますが、2 つのサンプルの「間」でリネームされたファイルは、1 本の連続した線ではなく、2 つの別々のパス系列(旧名、次に新名)として現れます。サンプル間のリネームの縫合は後回しのフォローアップです。
バスファクター(ディレクトリ・リポジトリレベル)
ファイルごとの ownership_top_share がファイル「内」の集中度を測るのに対し、バスファクター(別名トラックファクター)は「ファイル集合をまたいで」集中度を測ります。すなわち、その離脱によってディレクトリ内のファイルの半数超が精通した保守者を失うことになる、最小の開発者数です。bca vcs はこれを、ランク付けされた files と並ぶトップレベルの vcs_aggregate オブジェクトとして出力します:
{
"vcs_aggregate": {
"bus_factor": {
"bus_factor_schema_version": 2,
"coverage_threshold": 0.5,
"doa_threshold": 0.75,
"repo": { "bus_factor": 3, "files": 412, "authors": 11 },
"by_directory": [
{ "directory": "src", "bus_factor": 2, "files": 180, "authors": 7 },
{ "directory": "src/vcs", "bus_factor": 1, "files": 24, "authors": 3 }
]
}
}
}
各開発者の各ファイルに対する作者性は、Avelino の Degree-of-Authorship ヒューリスティック(Avelino, Passos, Hora & Valente, A Novel Approach for Estimating Truck Factors, ICPC 2016)でスコアリングされます:
DoA(d, f) = 3.293 + 1.098·FA + 0.164·DL − 0.321·ln(1 + AC)
ここで FA は最初の作者性(d が f を作成したなら 1)、DL は d の f への提供(変更)回数、AC は「他の」開発者による変更回数です。ファイルの最大値で正規化した DoA が 0.75(論文のしきい値)を超えるとき、その開発者は f の作者とみなされます。トラックファクターは貪欲な除去で求めます。すなわち、まだカバーされているファイルを最も多く作者として持つ開発者を除去し、ファイルの --bus-factor-threshold(デフォルト 0.5、Avelino に準拠)超が孤児になるまで繰り返し、除去した人数を報告します。by_directory は各トップレベルディレクトリとその直下のサブディレクトリをカバーし、それぞれ再帰的に配下のすべてのファイルに対して計算されます。
構造上の注意点:
- 大部分が単一作者のファイルからなるリポジトリ(またはディレクトリ)のバスファクターは
1と報告されます — その 1 人の作者を失うと各ファイルが孤児になるからです。これはヒューリスティックが意図どおりに働いているのであってバグではありません。この数値は保証ではなく計画のためのシグナルとして扱ってください。 - ボットのアイデンティティは(ファイルごとのシグナルと同様に)フィルタリングされ、ウィンドウ内に活動のないファイルは作者性を持たず、分母から除外されます。
- 「最初の作者性」は「長期ウィンドウ内で観測された」最古のコミットを意味し、必ずしもファイルの真の作成を意味しません。
この集約はファイル種別スコープ内のリポジトリ全体を反映します(1 回の履歴走査がスコープ内のすべてのファイルをカバーします — デフォルトはメトリクスを持つファイルの集合で、ファイル種別スコープを参照 — したがって --file-types all はバスファクターを追跡対象のすべてのファイルに広げます)。--paths / --include / --exclude はランク付けされたファイルごとのリストのみをスコープし、バスファクターには「適用されません」。サブシステムに注目したい場合は、走査をフィルタリングするのではなく by_directory の該当エントリを読んでください。
--emit-author-details は各グループに key_author_ids リストを追加します — 除去されたキー開発者の SHA-256 ハッシュ化されたアイデンティティを、除去順に並べたものです(平文のアイデンティティがプロセスの外に出ることはありません)。この集約は専用の bca vcs / bca report --vcs レポートと REST / Python エンドポイントに対してのみ計算されます。ファイルごとの bca metrics --vcs 注入パスはこのコストを払いません。
作者詳細のプライバシー
key_author_ids のダイジェストは安定した仮名であり、匿名化ではありません。ハッシュ化により平文のメールアドレスはレポートとキャッシュに含まれず、安易な漏洩は抑止されますが、このハッシュは暗号学的に不可逆では「ありません」。原像はメールアドレス — 低エントロピーで列挙可能 — であり、コミット履歴は公開されているため、候補となるメールアドレスの集合を持つ者なら誰でも、各候補をハッシュ化するか、事前計算されたメール→ハッシュのテーブルを使って、どのダイジェストが誰のものかを割り出せます。これは Gravatar のメールハッシュ化を破ったのと同じ弱点です。
公開された key_author_ids(およびファイルごとの author_ids)は、平文のメールアドレスの出力を避ける仮名化として扱い、執念深い攻撃者によって作者が再識別されないことの保証としては扱わないでください。その保証が必要なら、ダイジェストを公開しないでください。
強化モード:--author-hash-key
より強い耐性が必要な場合は、--author-hash-key <KEY> で秘密鍵を渡します(--emit-author-details が必要です)。出力されるダイジェストは素のハッシュではなく HMAC-SHA256(key, SHA-256(email)) になります。鍵を持たない攻撃者は、候補のメールアドレスをハッシュ化してダイジェストを見分けることも、事前計算されたメール→ハッシュのテーブルを使うこともできなくなります — どちらの攻撃にも秘密鍵が必要です。高エントロピーの鍵を選び、秘密に保ってください。鍵を知った者は誰でも列挙をやり直せます。
鍵は安定しています。同じ鍵なら、すべてのレポートにわたって、また永続キャッシュのリプレイをまたいでも同じダイジェストが得られるため、レポート間の相関付けとキャッシュは引き続き機能します。異なる鍵は無関係なダイジェストを生成するため、履歴を共有する 2 つのチームは、鍵を共有しない限り作者を相互にひも付けることはできません。
フラグよりも環境変数 BCA_AUTHOR_HASH_KEY を優先してください — コマンドラインに置かれた鍵はプロセスリスト(ps)経由で他のローカルユーザーから見え、シェル履歴にも保存されます。両方が設定されている場合はフラグが優先されます:
export BCA_AUTHOR_HASH_KEY="$(cat ~/.config/bca/author-key)"
bca vcs --emit-author-details
鍵がカバーしないもの:ディスク上の履歴キャッシュ(issue #334)は意図的に「鍵なしの」内側の SHA-256 ダイジェストを保存します。鍵は確定処理の段階で適用されるため、キャッシュ済みの走査は再走査なしに任意の鍵で再確定できるからです。キャッシュはローカル限定で公開されることはありませんが、脅威モデルにローカルのキャッシュディレクトリを読む攻撃者が含まれる場合は、キャッシュを無効化(--no-cache)するかクリア(--clear-cache)してください。同じ鍵オプションは REST エンドポイント(author_hash_key)と Python(vcs.Options(author_hash_key=…))でも利用できます。
このリポジトリでのドッグフーディング
このプロジェクトは自身のソースに対して bca vcs を実行しています。make vcs はランク付けされたテーブルを表示します(パス選択と .bcaignore の除外セットは、make self-scan と make report が使うのと同じ、リポジトリルートの bca.toml マニフェストから取得されます。BCA_VCS_TOP で行数の上限を上書きできます)。マニフェストの [vcs] file_types キーがデフォルトのスコープを設定します(--file-types CLI フラグが指定された場合はそちらで置き換えられます)。main へのプッシュのたびに、Pages CI ジョブがレンダリング済みランキングをフラッグシップレポート — bca report html --vcs / report markdown --vcs — に織り込むため、公開される reports/index.html には変更履歴リスクのセクションが AST ホットスポットと並んで表示され、さらにツール向けに完全なトップ 100 ランキングが reports/vcs-report.json として公開されます。
REST と Python
- REST: JSON ボディ
{ "id": "...", "repo_path": "/path/to/repo", ... }を伴うPOST /v1/vcsはランク付けされたレポートを返し、POST /v1/vcs/trend(同じフィールドに加えてpoints/span/top_deltas)は履歴の時系列を返します。REST API の利用を参照してください。 - Python:
big_code_analysis.vcs.rank(repo_path, …)はランク付けされたレポートを dict として返し、vcs.trend(repo_path, points=…, span=…, …)は時系列を返します。またanalyze(path, vcs=True)は単一ファイルのメトリクスにvcsブロックを付加します。
POST /v1/vcs と vcs.rank()(vcs.Options 経由)のどちらも、ランク付け対象のファイルをスコープする省略可能な file_types("metrics" / "all" / "rs,py")を受け付けます。これは CLI の --file-types に対応します。
どちらも結果に vcs_aggregate のバスファクターを含み、カバレッジの割合を調整する bus_factor_threshold(範囲は (0, 1))を受け付けます。