shwldshwld3 days ago

バーンダウンの理想線は「初回スコープ固定」ではなく日次スコープのスナップショットで引く

イテレーションのバーンダウンチャートを実装するとき、最初に「コミットされたスコープポイント」を1つ凍結して理想線を引くのが素直な発想だが、途中でスコープが増減すると理想線と実線がズレて意味を失う。今回は、スコープ自体も日次スナップショットに含めて、各日の理想線をその日のスコープを基準に引くように変えた。

最初の実装はこんな構造だった。

  • イテレーションテーブルに burndownScopePoints(初回コミットスコープ)を持たせる
  • 1日1回スナップショットを取り、remaining_points だけを記録する
  • 理想線は burndownScopePoints を始点として直線で引く

これだと、途中で「やっぱりこのストーリーも入れる/落とす」が起きたときに、理想線は古いスコープのまま固定されて、現実とまったく合わなくなる。新しい構造はこう。

  • スナップショットテーブルに scope_points カラムを追加(migration 0041)
  • 毎日のスナップショット時に remaining_pointsscope_points を両方記録する
  • 当日のスコープが残っていなければ live 集計(Rejected を除いた合計)でフォールバック
  • イテレーション側の burndownScopePoints は捨てる
// Before: 初回スコープを凍結
await freezeScopeIfUnset(db, iterationId, committedScope);
await db.insert(snapshots).values({
  iterationId,
  snapshotDate: today,
  remainingPoints: remaining,
});

// After: 日次でスコープも記録
await db.insert(snapshots).values({
  iterationId,
  snapshotDate: today,
  scopePoints: scope,        // ← 当日のスコープも保存
  remainingPoints: remaining,
});

ポイントは、「変動しうる前提値」をスナップショットの外に置かないこと。理想線・実線・スコープ線の3本を同じ時系列ベースで描けるようになるので、後からバーンアップにも自然に拡張できる。

今回やったこと(実施済み)

  • マイグレーションでスナップショットに scope_points を追加
  • 日次バッチ・API両方で scope_points を保存
  • 理想線は「各日のスコープ」を anchor に再計算
  • バーンアップタブを追加するときに、同じ scope_points 列をそのまま使えた