Notes
同じデータの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 — コロケーションしていないと不要フィールドに気づきにくいので、ついでにファイル配置もコンポーネントに寄せる
ドメインガード関数で不変条件を一元管理する
複数のフィールドの組み合わせでパネルや状態を決定するUIで、ドラッグ&ドロップなどの操作を追加すると矛盾した状態が生まれやすい。パネル判定ロジックが複数箇所に散らばっていたのが原因だった。
解決策として、不変条件をドメインガード関数に集約し、パネル判定も単一の関数に一元化した。
構造:
domain/
item-input.ts ← ドメインガード(不変条件のバリデーション)
item-panel-grouping.ts ← パネル判定の唯一の情報源
ui/
multi-panel-screen.tsx ← ガード関数を呼ぶだけ
ドメインガードの例:
// isArchived=true と activeGroupId!=null は共存できない
function validateItemInput(input: ItemInput) {
if (input.isArchived && input.activeGroupId != null) {
throw new Error("Archived items cannot belong to an active group");
}
}
パネル判定の一元化:
// グルーピングと同じロジックで単一アイテムのパネルも判定する
function determinePanelForItem(item: Item): PanelType {
return groupItemsByPanel([item])[0].panel;
}
// DnDハンドラは source × target のマトリクスで明示的に許可
function handleDragEnd(source: PanelType, target: PanelType) {
const allowed = { Backlog: ["Archive"], Archive: ["Backlog"] };
if (!allowed[source]?.includes(target)) return; // 無効な遷移は無視
// ...
}
なぜこうしたか:
- UI側にパネル判定の逆ロジックを持つと、グルーピング関数との間で不整合が起きる。判定を1箇所にすることで「UIが見ているパネル」と「ドメインが認識するパネル」のズレがなくなる
- ドメインガードを永続化層の手前に置くと、APIからもUIからも同じ不変条件が強制される
- DnDの遷移をマトリクスで管理すると、許可されない遷移を明示的にドキュメントできる
Cloudflare Access applicationを再作成するとaudが変わる
Cloudflare Accessで保護したアプリのAccess applicationを削除→再作成すると、JWTのaud(Audience Tag)が新しい値に変わる。Worker側でaudを検証している場合、環境変数を更新しないと全APIが401になる。
症状がわかりにくいのがハマりポイント。Cloudflare Accessのログイン自体は成功するので「認証は通っている」ように見えるが、Worker側のJWT検証でaud不一致になりAPIが全滅する。フロントエンドは「session expired」的な表示になるため、Access側の問題だと気づきにくい。
// Worker側のJWT検証コード(簡略化)
const payload = await verify(jwt, publicKey);
if (payload.aud !== env.ACCESS_AUD) {
// Access applicationを再作成すると、ここで落ちる
return new Response('Unauthorized', { status: 401 });
}
調査の勘所:
- ブラウザでAccess認証は通るのにAPIが401 →
audの不一致を疑う curl -vでレスポンスヘッダを確認し、Access自体は通過していることを確認する- JWTをデコードして
audの値を現在のAccess applicationのAudience Tagと突き合わせる
ポイント:
- Access applicationを再作成・置換したら、Worker側の
ACCESS_AUD環境変数を必ず更新する - IaCで管理しているなら、applicationリソースの
aud出力をWorker設定に自動連携しておくと安全
MCPツールは1機能1PRで積み上げると安定して進む
MCPサーバーにCRUD系のツールを追加するとき、一度に全部作ろうとせず1ツール1PRで積み上げるアプローチが効果的だった。1日で get / create / update / comment の4ツール+関連情報取得を安定してリリースできた。
構造:
mcp-server/
tools/
get-resource.ts # PR #1
create-resource.ts # PR #2 (get-resourceの存在を前提に)
update-resource.ts # PR #3
create-comment.ts # PR #4
get-workspace.ts # PR #5
なぜこうしたか:
- 依存の方向が自然に決まる — getを先に作り、create/updateはgetの動作を前提にできる。テストもgetの結果を使って検証できる
- CIが毎回グリーンになる — 差分が小さいのでレビューもCI修正も局所的で済む
- ロールバックが容易 — 問題が出ても1ツール単位で切り戻せる
- 途中で方針変更しやすい — 3つ目を作る頃にはパターンが固まり、最初の2つをリファクタする判断もしやすい
代替案として「全ツールを1つの大きなPRで出す」方法もあるが、コンフリクトやCI修正が複雑になりがちで、途中で手が止まるリスクが高い。
GitHub ActionsでPR本文に必須セクションを強制する
PR本文にADR(Architecture Decision Records)セクションを含めるルールを作ったが、AIエージェントがルールを守らないことがあった。CLAUDE.mdに書いてもスキップされるケースがあるため、CIで機械的にブロックする方が確実。
Before(手動):
レビュー時に「ADRセクションがない」と指摘して差し戻し。見落としも発生する。
After(自動):
# .github/workflows/pr-body-check.yml
name: PR Body Check
on:
pull_request:
types: [opened, edited]
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check required sections
uses: actions/github-script@v7
with:
script: |
const body = context.payload.pull_request.body || '';
const required = ['## Related ADRs'];
const missing = required.filter(s => !body.includes(s));
if (missing.length > 0) {
core.setFailed(`PR本文に必須セクションがありません: ${missing.join(', ')}`);
}
ポイント:
types: [opened, edited]で作成時と編集時の両方をチェックする- AIエージェントに「ルールを守れ」と書くより、CIで落とすほうが確実に機能する
- 必須セクションが増えたら
required配列に追加するだけで拡張できる
macOS標準のbash 3は連想配列が使えない
macOSに標準搭載されている bash は 3.x 系で、declare -A (連想配列)が使えない。bash 4以降の機能。
# bash 4+: 動く
declare -A counts
counts["key"]="value"
# bash 3 (macOS default): syntax error
Homebrew で bash 5 を入れていれば使えるが、CI環境やチームメンバーの環境では入っていない前提で書くほうが安全。
回避策:
- 連想配列の代わりに
grep+ 一時ファイルやawkでキーバリューを管理する /bin/bashではなく/usr/bin/env bashを使い、PATH上の新しい bash を使う手もあるが、全環境に bash 4+ がある保証はない- 複雑なデータ構造が必要ならシェルスクリプトをやめて Python や jq に切り替える
ポイント:
#!/bin/bashで書くスクリプトはmacOSのbash 3互換を意識するdeclare -A以外にも、${var,,}(小文字変換)、|&(パイプのstderrリダイレクト)なども bash 4+ 限定
