Skip to content

[Bug] next-spec-id: is-ancestor判定でbaseに追従していないブランチが除外され、ID衝突を見逃す #196

Description

@BlueEventHorizon

概要

forge:next-spec-idskills/next-spec-id/scripts/scan_spec_ids.py)が算出する次の連番IDが、実際には他ブランチで既に使用済みだった。ADR/DES共有採番で DES-007 を採番したが、feature/14-pyyaml-migration ブランチに既に ADR-007_pyyaml_adoption.md が存在しており衝突した(利用先: DocAdvisorリポジトリ)。

原因

get_scan_branches() が、ベースブランチ(develop/main)以外の各ブランチをスキャン対象に含めるかどうかを次の条件で判定している:

proc = subprocess.run(
    ['git', 'merge-base', '--is-ancestor', base_branch, branch],
    ...
)
if proc.returncode == 0:
    scan.append(branch)

つまり「そのブランチが base_branch に追従済み(base の全コミットを含む)」場合のみスキャン対象にする。しかし、アクティブな feature ブランチが base 上の後続コミット(例: バージョンバンプ等)を取り込んでいない状態は日常的に起こり、その場合 is-ancestor は false になり、そのブランチが静かにスキャン対象から除外される

再現手順(DocAdvisorリポジトリで確認済み):

git merge-base --is-ancestor develop feature/14-pyyaml-migration
echo $?  # => 1 (false)

develop 側に feature/14-pyyaml-migration 分岐後の新規コミット(chore: bump to 0.4.5 等)があったため、上記が false になり、feature/14-pyyaml-migration 上の ADR-007next-spec-id のスキャン対象から漏れた。結果、branches_scanned は "成功した" 件数を報告するため、利用側が気づけないまま誤った ID を採番してしまう。

あるべき挙動

ID衝突検出の目的では「base に追従しているか」は無関係で、未マージの全アクティブブランチrefs/heads/ + refs/remotes/origin/ の全て)を無条件でスキャンすべき。むしろ最も衝突リスクが高いのは「base から分岐したまま長期間追従していないブランチ」であり、現行ロジックはまさにそれを除外してしまう逆効果な設計になっている。

修正案

get_scan_branches()is-ancestor フィルタを撤廃し、refs/heads/ + refs/remotes/origin/ から取得した全ブランチ(HEAD除く)を無条件でスキャン対象にする。マージ済みで内容が同一なブランチが重複スキャンされても、find_duplicates() 側で「同一パスの複数ブランチ出現は正常」として除外されるため副作用はないはず。

影響

CLAUDE.md 等で「next-spec-id の出力を鵜呑みにせず自分でも横断確認する」という運用回避策を各プロジェクトが個別に講じる必要が生じており、スキル本来の「安全に採番できる」という価値が損なわれている。

実装計画

関連ドキュメント

ルール文書

種類 ドキュメント
実装ガイドライン implementation_guidelines.md(テスト必須)

参考 PR(実装パターン)

PR 参考ポイント
#185 scan_spec_ids の duplicates を共有採番モードに追随 同一スクリプトの軽量バグ修正の前例(テスト mock パターン・実測検証の書き方)

実装スコープ

レイヤー 実装内容 ファイルパス
スクリプト get_scan_branches()is-ancestor フィルタを撤廃し、全ブランチを無条件対象にする scan_spec_ids.py
テスト get_scan_branches() の単体テスト更新・追加 test_scan_spec_ids.py

スコープ外

項目 理由 担当
スキャン対象ブランチ数増加によるパフォーマンス最適化(並列化等) Issue の指摘は正確性の欠陥であり、性能要件は現時点で未報告 -

TODO

着手前確認

  • 仕様書の確認完了(該当なし)
  • 実装ルールの確認完了
  • 参考 PR のパターン確認完了

実装する

  • get_scan_branches() から git merge-base --is-ancestor フィルタを削除し、for-each-ref 取得の全ブランチ(HEAD 除く)を無条件でスキャン対象にする

テストを書く

  • get_scan_branches: base に追従していないブランチも scan 対象に含まれることを確認するテストを追加
  • 既存の get_scan_branches 関連テスト(もしあれば is-ancestor 前提のもの)を新仕様に合わせて更新

確認・レビュー準備を行う

  • テストを実行する(python3 -m unittest discover -s tests -p 'test_*.py'
  • dprint check を実行する(Markdown 等変更があれば)

完了する

  • PR を作成する
  • レビューに対応する

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions