Claude Codeでsemverの破壊的変更を判定 — api-extractor差分を読ませる
TypeScriptライブラリの次のバージョンがmajorかminorかを、api-extractorの.api.md差分とClaude Codeで判定する手順。変更を分類させ、上げ幅は機械で決めます。
TypeScriptライブラリの次の版がmajorかminorかは、公開APIの差分を読めば決まります。api-extractorが出す.api.mdの差分をClaude Codeに読ませ、変更を「削除」「シグネチャ変更」「追加」に仕分けさせると、判断の根拠をCHANGELOGの見出しまで一続きで残せます。ここで任せるのは分類までです。バージョン番号は分類結果から機械的に決めます。
なぜコミットの型ではなくAPI差分から決めるのか
semver(セマンティックバージョニング)の規則自体は短く、破壊的変更ならMAJOR、後方互換の機能追加ならMINOR、後方互換のバグ修正ならPATCHを上げます。難しいのは「どの変更が破壊的か」の判定のほうです。
コミットの型から版を決める方式は、型を書く人の申告に依存します。feat:と書かれたコミットが実は既存関数の引数を必須に変えていた場合、申告が誤っているので、破壊的変更として扱われません。この方式の運用はClaude Codeでsemantic-releaseを回す手順にまとめています。
一方、公開APIの形そのものを差分で見る方式なら、申告は要りません。ライブラリが外に見せている宣言だけが比較対象になるからです。TypeScriptではその役を、api-extractorのAPIレポートが担えます。
api-extractorのレポートに載るもの
api-extractorはTypeScriptコンパイラを使って、パッケージが公開している宣言を取り出します。結果は.api.mdというMarkdownファイルに、シグネチャを要約した疑似コードとして書き出されます。デフォルトの置き場は<projectFolder>/etc/で、パッケージ名が@microsoft/sp-core-libraryならetc/sp-core-library.api.mdになります。
レポートを差分レビューに使うために、載せる範囲は絞られています。
- 関数のシグネチャは載るが、関数本体は載らない
- 公開された宣言は載るが、公開されていない宣言や
privateメンバーは載らない // (undocumented)のようにドキュメントの有無は載るが、説明文が変わっても差分にならない@internal、@alpha、@beta、@publicのリリース区分は載る
つまりAPI上の意味のある変化があったときだけ差分が出るよう設計されたファイルです。この性質が、LLMに読ませる入力として都合がよい点です。ソース全体の差分を渡すより、読む量が小さく、ノイズが少なくなります。
前提: api-extractorの導入
手順1に入る前に、api-extractorをプロジェクトに入れ、.api.mdが出る状態にしておきます。
npm install --save-dev @microsoft/api-extractor
npx api-extractor initinitが作るapi-extractor.jsonで、apiReport.enabledをtrueにし、レポートの置き場をapiReport.reportFolderで決めます。1つのパッケージなら、既定のetc/のままで足ります。入力にするのはパッケージのエントリポイントの.d.ts(例: lib/index.d.ts)なので、tscでビルドして宣言ファイルを先に出力しておきます。生成された.api.mdはGitで追跡します。差分としてPRに出ることが、この手順の前提です。
全体の流れ
作る仕組みは4段です。前半2つはapi-extractor、後半2つがClaude Codeとシェルの担当です。
API差分から版を決める4段
- 1
レポートを再生成する
ローカルビルドで
.api.mdを更新し、Gitで追跡します。 - 2
前回のリリースタグと比べる
最後に公開した版のタグと、現在のレポートの差分を取ります。
- 3
Claude Codeに変更を分類させる
差分の各項目に、
major/minor/patch/noneの水準と理由をJSONで付けさせます。 - 4
上げ幅はシェルで決め、CHANGELOGに落とす
水準の最大値が次の版の上げ幅になります。分類結果は見出しの下の箇条書きになります。
レポートの更新と差分の取り方
api-extractorのrunは、--localを付けるとローカルビルドとして動き、変更されたレポートを自動で所定の場所へ書き戻します。付けない本番ビルドでは自動では書き戻されません。公開APIのシグネチャが変わっていると、temp/に出力されたレポートをetc/へコピーするか、ローカルビルドを実行するよう求める警告が出ます。リポジトリのPRチェックで本番ビルドを回しておけば、API変更を含むPRが.api.mdの更新を忘れたとき検知できます。
npx api-extractor run --local
git diff v1.4.0 -- etc/awesome-widgets.api.md比較の基準にするのは、直前のコミットではなく最後に公開した版のタグです。版番号はリリース単位で決まるので、間に何本のPRが入っていても、公開済みの状態と比べた累積差分で判断します。タグ名は例です。自分のリポジトリのものに読み替えてください。
Claude Codeに差分を分類させる
Claude Codeは-pを付けると非対話で動き、標準入力で渡した内容をそのまま材料にできます。公式の例では、git diff mainをパイプで渡す形でタイプミスの検出に使っています。差分をパイプで渡せば、Claudeにシェルの実行権限を与えなくて済みます。
出力はJSONスキーマで縛ります。--output-format jsonと--json-schemaを併用すると、スキーマに沿った結果がstructured_outputに入ります。
git diff v1.4.0 -- etc/awesome-widgets.api.md | claude -p \
"この.api.mdの差分を、公開APIの変更ごとに分類して。\
判定基準はCLAUDE.mdのsemver節に従うこと。" \
--output-format json \
--json-schema '{"type":"object","properties":{"changes":{"type":"array","items":{"type":"object","properties":{"symbol":{"type":"string"},"kind":{"type":"string","enum":["removed","signature","added","release-tag","deprecated","docs-only"]},"level":{"type":"string","enum":["major","minor","patch","none"]},"reason":{"type":"string"}},"required":["symbol","kind","level","reason"]}}},"required":["changes"]}' \
> classified.jsonkindは差分の種類、levelは上げ幅です。2つを分けておくと、分類の誤りを後から見つけやすくなります。たとえばkindがremovedなのにlevelがminorなら、判定基準に反しているとすぐ分かります。
判定基準はCLAUDE.mdに書いておく
semver本体は、公開APIに後方互換でない変更があればMAJOR、新しい互換機能の追加か、公開機能の非推奨化ならMINORと定めています。型レベルの細かい扱い(引数を増やしたらどうか、戻り値の型を変えたらどうか)までは規定していません。そこはプロジェクトごとの取り決めになるので、判定表をCLAUDE.mdに置いて、毎回同じ基準で読ませます。
## semver 判定基準(etc/*.api.md の差分を読むとき)
- エクスポートされた宣言の削除・改名: major
- 既存関数に必須引数を追加、引数や戻り値の型を狭める/無関係な型に変える: major
- 既存の型に必須プロパティを追加: major(利用側が値を作る型の場合)
- 任意引数・任意プロパティの追加、新しいエクスポートの追加: minor
- @deprecated の付与: minor(削除はしない)
- @beta/@alpha の宣言の変更: none(利用側に安定を約束していない)
- @beta から @public への昇格: minor / @public から @beta への降格: major
- `// (undocumented)` の増減や、コメントだけの差分: none
- 迷うものは level を一段重いほうにして、reason に迷った点を書く最後の1行が大事です。LLMの誤判定には、軽く見積もるものと重く見積もるものの2種類があります。リリースの事故になりやすいのは、破壊的変更を軽く見積もる側です。迷ったときは重いほうに分類させる、という方向を明示しておくと、見逃しが減ります。
@betaの扱いは、api-extractorが想定する使い方に沿っています。公式の説明では、まだ固まっていないAPIを@alpha、@betaとして公開しておき、成熟したら@publicへ昇格させる運用を挙げています。進化のたびにmajorを上げ続けないための仕組みです。この前提のライブラリなら、@betaの変更はnoneで構いません。
上げ幅はシェルで決める
分類結果のlevelを集めて、最も重いものを次の上げ幅にします。ここはLLMに任せず、jqで数えます。同じ入力なら必ず同じ答えになり、レビューでも説明しやすくなります。
jq -r '
[.structured_output.changes[].level] as $l
| if ($l | index("major")) then "major"
elif ($l | index("minor")) then "minor"
elif ($l | index("patch")) then "patch"
else "none" end' classified.jsonstructured_outputは、Claudeが依頼を断ったなどで有効な構造化出力が得られなかったとき、レスポンスに含まれません。スクリプトでは、フィールドの有無を先に調べてから使います。無かったら、版を決めずに失敗させるのが安全です。
jq -e '.structured_output.changes' classified.json > /dev/null \
|| { echo "分類結果なし。手動で確認すること" >&2; exit 1; }semverには例外があります。0.y.zの間は初期開発で、いつ何が変わってもよい扱いです。1.0.0未満のパッケージでは「major」の意味が変わるため、0.x系のままminorを上げるのか、1.0.0へ上げるのかは別に決めます。判定表の側に、現行バージョンの区分も書き添えてください。
CHANGELOGの見出しに落とす
水準が決まれば、見出しは機械的に作れます。分類結果をkindとlevelで並べ替え、majorのものを「破壊的変更」、minorを「追加」の下に並べます。Claude Codeに文章化させるときは、元のJSONを渡して「reasonをそのまま使い、事実を足さない」と指示します。
## 2.0.0
### 破壊的変更
- `AwesomeButton.draw()` の第2引数 `style: DrawStyle` を必須にした
- `DrawHelper` のエクスポートを削除した
### 追加
- `AwesomeButton.drawAsync()` を追加見出しの版番号は、上のシェルが返した上げ幅を現在の版に適用して作ります。ここもClaudeに計算させません。
差分を読ませるときの落とし穴
.api.mdの差分は、LLMにとって読みやすい形ですが、それでも見落としは起きます。仕組みから予想できる穴を挙げておきます。
落とし穴
関数の中身の変更はレポートに出ない
レポートに載るのはシグネチャだけで、関数本体は対象外です。引数の型が同じまま戻り値の意味が変わったり、例外を投げる条件が変わったりした変更は、差分が空になります。振る舞いの変更はレポートの外にあるため、CHANGELOGのレビューか、テストの差分で拾う必要があります。分類結果がnoneのときも、「APIの形は変わっていない」と読んでください。
もう1つは、レポートに混ざる警告です。api-extractorはaddToApiReportFileを有効にすると、ae-forgotten-exportのような警告をコンソールではなく.api.mdの中へ書き込みます。この行が増減しても、公開APIが変わったわけではありません。判定基準に「// Warning:で始まる行の差分はnone」と一行足しておくと、誤判定が減ります。
最後に、レポートそのものはClaudeに編集させません。.api.mdの冒頭に「Do not edit this file」とあるとおり、これは生成物です。直したいときは元のソースを直して、api-extractor run --localで作り直します。権限の面でも、パイプ入力にしておけば書き込み権限が要らず、編集される余地が最初からありません。
CIに組み込む
PRの段階で「このPRが入ったら次の版は何か」を出しておくと、レビューで破壊的変更に気づけます。非対話の呼び出しが終了コードで成否を返すので、スクリプトの分岐に使えます。
LAST=$(git describe --tags --abbrev=0)
git diff "$LAST" -- etc/awesome-widgets.api.md > api.diff
if [ -s api.diff ]; then
claude -p "$(cat prompt.txt)" --output-format json \
--json-schema "$(cat schema.json)" < api.diff > classified.json
fi人がいないCIでは、権限確認が出ても答える人がいません。そのためのフラグ--permission-prompts noneが用意されています。承認が要る操作は拒否され、Claudeには「承認できない」と伝わるので、再試行せず先へ進みます。このフラグはClaude Code v2.1.259以降で使えます。上のスクリプトは差分を標準入力で渡し、ツールを使わせない構成なので、このフラグは付けていません。Claudeにファイルの読み取りなどを許す構成にしたときに足します。
差分が空ならClaudeは呼びません。.api.mdの変更が無いPRでは、API上の変化がないと分かっているからです。コストも抑えられます。
まとめ
APIレポートの差分は、LLMに渡す入力としてサイズも密度もちょうどよく、「何を公開しているか」だけが残っています。そこで分類だけをClaude Codeに任せ、上げ幅はjqで決める分担にすれば、判断の流れが後から追えます。判定表を育てる際は、実際にmajorを見逃した事例を1つずつ行にしていくのが近道です。タグ運用や自動公開まで含める場合は、Changesetsの運用のように意図をファイルに残す方式と、どちらが自分のチームに合うかを見比べてください。利用側でmajorアップを受ける手順は、Jest 30へのアップグレードが一例になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。