Notes
PRメタデータ検証は「空でない」だけでなく許容値を固定する
PR本文の必須セクションをCIで検証するとき、単に「空欄ではない」を条件にすると TBD や曖昧な記載が通ってしまう。有効なID か None のような明示的センチネルだけを許可すると、運用が安定する。
- 弱い検証 — セクションの存在と非空だけを確認する
- 強い検証 — 正規表現で許容値を限定する(例:
ADR-0001形式 orNone) - 転用ルール — 人間が読むメタ情報でも、CIにかけるなら「自由記述」ではなく「限定語彙」を設計する
# 例: ID形式または None のみ許可
grep -Eqi 'ADR-[0-9]{4}|^[[:space:]-]*None[[:space:]]*$'
楽観更新の並び替えは部分パッチで直し、次はorder外部化を試す
今回実際にやったのは、並び替え時の楽観更新を「表示中データで全体を置換する方式」から「既存キャッシュに対する部分パッチ方式」へ変えたこと。これで、未表示要素の順序や存在が壊れる症状を抑えられた。
- 変更前の問題 — 表示中の配列をそのまま全体へ反映してしまい、フィルタ/ページング外の要素に副作用が出る
// NG: visibleRows だけで全体を再構築
setQueryData(key, visibleRows)
- 実際に入れた修正 — id 一致の既存要素だけ差し替える。未表示要素は保持する
const updates = new Map(visibleRows.map((row) => [row.id, row]))
setQueryData(key, (current) =>
current.map((row) => updates.get(row.id) ?? row)
)
今回の学び — 一覧UIの optimistic update は、全体同期ではなく「既存キャッシュへの局所変更」として扱うと崩れにくい
次に試す改善案(未実施) — order だけを外部化して、実体データと順序責務を分離する
// entities と order を分離して管理するイメージ
state = {
entitiesById: Record<string, Item>,
orderedIdsByView: Record<ViewKey, string[]>,
}
この形にすると、DnD の楽観更新は orderedIdsByView だけを更新すればよく、未表示要素や別ビューへの副作用を抑えやすい。次回はこの設計で比較する。
同じデータの2つのstateを同期するより、片方を派生値にする
Reactコンポーネントで「全アイテムのフラットリスト」と「カテゴリ別に分類したリスト」を別々のstateとして持っていた。更新時に片方だけ更新してもう片方を忘れるバグが頻発した。
// Before: 2つの独立したstate
const [items, setItems] = useState<Item[]>([])
const [categorizedItems, setCategorizedItems] = useState<Record<string, Item[]>>({})
// 更新時に両方を手動で同期する必要がある
const handleUpdate = (updated: Item) => {
setItems(prev => prev.map(i => i.id === updated.id ? updated : i))
// ↑ ここだけ更新して setCategorizedItems を忘れると画面に反映されない
}
修正: categorizedItems を正本にして、フラットリストは useMemo で派生させる。
// After: stateは1つ、もう片方は派生値
const [categorizedItems, setCategorizedItems] = useState<Record<string, Item[]>>({})
const allItems = useMemo(
() => Object.values(categorizedItems).flat(),
[categorizedItems]
)
この構造なら setCategorizedItems を更新するだけで allItems も自動的に最新になる。更新ハンドラが増えても「どっちのstateを更新すべきか」で迷わない。
判断基準:
- 2つのstateが同じデータの異なるビューなら、片方を派生値にできないか検討する
- 「更新のたびに2箇所のsetStateを呼ぶ必要がある」コードは、同期漏れのバグを待っている状態
- どちらを正本にするかは「更新の起点がどちらの形式か」で決める。パネルやタブ別に更新するならカテゴリ別を正本にする
agent skillの実行中に判明した課題や改善点はfeedback.log的なファイルに追記してリポジトリに永続化するのが良さそう
定期的にその内容をスキル本体に取り込む習慣を自動化すると良い
パネルUIは一括取得より各パネル独立ロードにする
カンバン風の複数パネルUI(Done / Current / Backlog / Icebox等)で、全データを一括取得→クライアント側で振り分けていた構成を、パネルごとに独立してAPIを叩く方式にリファクタした。
もともとの構成だと、全件取得してからクライアントでフィルタリングしていたため、データ量が増えると初期表示が遅くなり、特定パネルだけページネーションしたいケースに対応できなかった。
リファクタ後の構成:
panelStories: Record<PanelType, Item[]>
panelLoading: Record<PanelType, boolean>
panelErrors: Record<PanelType, string | null>
panelPagination: Record<PanelType, { page: number; hasNext: boolean }>
- パネルごとにステートを分離 — loading / error / pagination を
Record<PanelType, T>で管理。あるパネルのロード中でも他パネルは操作可能 - パネル別のページサイズ — Doneは10件、他は20件のように、パネルの性質に応じたページサイズを設定できる
- フィルタ変更時の全パネルリロード — オーナーやラベルのフィルタが変わったら全パネルをpage=1からリロード。共通クエリパラメータを
buildSharedQuery()で組み立てて各パネルのリクエストに載せる
トレードオフとして、APIリクエスト数は増える(1回→パネル数分)。ただしパネルごとに並列リクエストできるので体感速度はむしろ改善する。パネル間でアイテムを移動した際のローカルステート同期は mergePanelStories() で全パネルの配列をフラットに結合して保持する形にした。
MCPリソースにデフォルトフィルタを入れてエージェントの焦点を絞る
AIエージェントがMCPリソースから作業項目を取得するとき、全件返すとノイズが多く、エージェントが「今やるべきこと」の判断に時間を使ってしまう。リソース側で「今のスプリントの作業だけ」「優先度順に上位N件だけ」を返すようにデフォルトを絞った。
Before:
エージェント → MCPリソース「タスク一覧」 → 全50件返る
→ エージェントが自分で「今のスプリントは…優先度が高いのは…」と判断
→ やっと実装開始
After:
エージェント → MCPリソース「タスク一覧(スプリント=現在, 上位3件)」
→ 3件だけ返る → 先頭から実装開始
やってみてどうだったか:
- エージェントが「どれをやるか」で迷う時間が消え、タスク開始までのレイテンシが明確に改善した
- 「どの作業を優先するか」の判断ロジックがMCPサーバー側に集約されるので、エージェントのスキル定義はシンプルに「一番上をやれ」だけで済む
- 優先度の計算方法を変えたくなったとき、MCPサーバー側だけ変えればよく、エージェント側のプロンプトを修正しなくてよい
- ポイントは「エージェントに判断させない」こと。判断の余地を減らすほどエージェントの行動が安定する
GraphQL Fragment変更時は「overfetchしてたら直して」と伝える
GraphQL Fragmentを変更するPRで、AIアシスタントに「overfetchしていたら直して」と指示するだけで不要フィールドが見つかる。人間が目視で全フィールドの使用箇所を追うのは面倒だが、コード解析はAIの得意領域。
見つかりやすいパターン:
- 使われていないunion型メンバー(
... on DeletedEntity { id }等) - propsとFragmentの両方で取得している重複フィールド
- コンポーネントと離れた場所に定義されたFragment — コロケーションしていないと不要フィールドに気づきにくいので、ついでにファイル配置もコンポーネントに寄せる
