先月、あるアプリで直したはずのクラッシュが、数週間後にまた同じ症状で戻ってきました。コードを見ても、直した箇所はそのままです。原因を掴むのに半日かかりました。別の画面を作り直したとき、その生成が古い実装ごと巻き戻していたのです。
個人開発でいくつものアプリを運用していると、こうした「静かな巻き戻し」は珍しくありません。Rork Max の2クリック申請でリリースの敷居が下がったぶん、私たちは以前より気軽に画面を作り直します。生成の回数が増えれば増えるほど、「いつ・どの生成で・何が変わったのか」は手元から抜け落ちていきます。
生成物に来歴(provenance)を刻んでおけば、不具合が出たときに疑うべき世代を数分で絞り込めます。特別なインフラは要りません。ビルド時の定数ひとつと、テキストの台帳ひとつが出発点です。私自身が実運用で使っている設計を、順に見ていきます。
なぜ git blame では追えないのか
手書きのコードなら、git blame が「この行を最後に触ったコミット」を教えてくれます。ところが AI ビルダーで再生成した画面は、しばしばファイル全体が丸ごと差し替わります。差分は「1行の変更」ではなく「ファイルの置換」として記録され、blame は再生成のコミット1点を指すだけです。
さらに厄介なのは、変更の意図がコミットに残らないことです。「価格表示を直して」と指示した生成が、ついでに離れた場所の状態管理を書き換えていても、コミットメッセージは Update pricing screen のままです。あなたが後から読むのは、実際に起きたことではなく、あなたが起こそうとしたことだけです。
つまり欠けているのは行単位の履歴ではありません。「どの生成イベントが、アプリのどの版に対応し、何を触ったのか」という世代の来歴 です。これを補うのが本記事の主題です。
来歴を3層で残す
来歴は1か所に書こうとすると必ず破綻します。ビルド成果物、ソースツリー、実行中のアプリは、それぞれ寿命も参照する人も違うからです。私は次の3層に分けて考えています。
層 何を残すか いつ役に立つか
ビルドスタンプ 生成 ID・ビルド日時をアプリ内定数に焼き込む 実機やレビュー版で「これはどの世代か」を即答したいとき
生成台帳 各生成の意図・触ったファイル・消費クレジットを1行追記 数週間後に「あの変更はいつだったか」を遡るとき
テレメトリ埋め込み クラッシュ・主要イベントに生成 ID を付与 本番の不具合を世代へ逆引きするとき
3層はそれぞれ独立して価値がありますが、同じ生成 ID で串刺しにすると効果が跳ね上がります。台帳で世代の意図を読み、テレメトリで影響範囲を測り、ビルドスタンプで手元の版を照合する。この往復ができるかどうかが、トリアージの速さを決めます。
ビルドスタンプ — 生成 ID を焼き込む
まず、世代を一意に表す文字列を決めます。私は 世代連番 + 短いラベル の形にしています(例: g047-pricing)。連番だけだと意味を思い出せず、ラベルだけだと重複するためです。
Expo(React Native)の場合、app.config.ts でビルド時の環境変数を extra に流し込みます。
// app.config.ts
export default ({ config }) => ({
... config ,
extra: {
... config.extra,
generationId: process.env. GENERATION_ID ?? "dev-local" ,
builtAt: new Date (). toISOString (),
} ,
}) ;
アプリ側では、この値を単一の定数モジュールから読みます。散らばると更新漏れの温床になるため、参照点はひとつに絞ります。
// src/provenance.ts
import Constants from "expo-constants" ;
const extra = Constants.expoConfig?.extra ?? {};
export const PROVENANCE = {
generationId: extra.generationId ?? "unknown" ,
builtAt: extra.builtAt ?? "unknown" ,
} as const ;
ビルドは環境変数を渡すだけです。GENERATION_ID=g047-pricing eas build --profile production のように、生成のたびに更新します。設定画面の隅に小さく g047-pricing と表示しておくと、QR で配った実機を見た瞬間に世代が分かります。
Rork Max が生成する Swift 側でも考え方は同じです。ビルド設定の User-Defined 設定に値を入れ、Info.plist 経由で読み出します。
enum Provenance {
static let generationId =
Bundle.main.infoDictionary ? [ "GenerationId" ] as? String ?? "unknown"
}
なぜ日時ではなく生成 ID を主キーにするのか。日時は「いつビルドしたか」を表しますが、「どの意図の生成か」は表しません。不具合の調査で本当に知りたいのは後者です。日時は補助情報として添えるにとどめます。
生成台帳 — 1生成につき1行
台帳はリポジトリ直下の GENERATIONS.md に置きます。ツールも要らず、git の履歴とセットで残るのが利点です。1エントリは短く、後から grep できる粒度にします。
## g047-pricing — 2026-07-07
- 意図: 価格表示のロケール対応(通貨記号の位置を修正)
- 触った範囲: PricingScreen, useLocalePrice
- 想定外の副作用の疑い: 状態初期化まわり(要監視)
- 消費クレジット: 12
- ビルド: eas build production(TestFlight 配布なし・QR 実機のみ)
肝は「意図」と「想定外の副作用の疑い」の2行です。生成直後、差分を眺めて「指示していない場所が変わっていないか」を一度だけ確認し、気になった箇所をここに書き留めます。この数十秒の記録が、数週間後の半日を救います。
台帳を人手だけに頼ると、忙しい日ほど書き漏れます。そこで、世代間で実際に変わったファイルを機械的に添える小さなスクリプトを併用します。生成ごとに git tag を打っておけば、直前世代との差分を要約できます。
// scripts/gen-diff.mjs
import { execSync } from "node:child_process" ;
const [ prev , curr ] = process.argv. slice ( 2 );
if ( ! prev || ! curr) {
console. error ( "usage: node scripts/gen-diff.mjs <prevTag> <currTag>" );
process. exit ( 1 );
}
const stat = execSync ( `git diff --stat ${ prev } ${ curr }` , { encoding: "utf8" });
console. log ( `### ${ prev } → ${ curr } \n\n\`\`\`\n ${ stat } \`\`\`\n ` );
node scripts/gen-diff.mjs g046-home g047-pricing を実行すると、変更ファイル数と行数の要約が出ます。これを台帳の該当エントリに貼れば、「意図」と「実際に触れた範囲」が並び、両者のズレが一目で分かります。指示は価格画面だけなのに store/auth.ts が変わっていれば、それが後日の火種です。
テレメトリに世代を付ける
手元の版と台帳がつながっても、本番で起きる不具合は結局ユーザーの端末で起きます。クラッシュレポートと主要イベントに生成 ID を添えておくと、集計画面から世代を逆引きできます。
import * as Sentry from "@sentry/react-native" ;
import { PROVENANCE } from "./provenance" ;
Sentry. setTag ( "generation_id" , PROVENANCE .generationId);
// アナリティクス側にも同じ値を共通プロパティとして渡す
analytics. setSuperProperty ({ generation_id: PROVENANCE .generationId });
こうしておくと、クラッシュ一覧を generation_id で束ねられます。「クラッシュ率が g046 の 0.3% から g047 で 1.1% に上がった」といった比較が、推測ではなく数字で言えるようになります。AdMob の表示回数や課金の転換率を世代で切ると、UI を作り直した生成が収益を静かに下げていた、といった発見にもつながります。
数字が世代を指したら、その世代の台帳エントリを開き、「想定外の副作用の疑い」に書いた行を見ます。多くの場合、犯人はそこに既にメモされています。
不具合が出たときのトリアージ手順
3層がそろうと、調査は決まった順番の作業になります。私自身は次の流れで追っています。
テレメトリのクラッシュや主要指標を generation_id で束ね、悪化が始まった世代を特定します。数字が判断の起点です。
その世代の台帳エントリを開き、「意図」と「想定外の副作用の疑い」を読みます。指示と実際のズレがここに現れます。
gen-diff の差分を見て、指示外に触れたファイルがないかを確認します。多くの場合、本番の不具合の原因はこの一覧の中にいます。
手元のビルドスタンプが同じ世代であれば、その版で再現を試し、直前世代へ切り戻して症状が消えるかを比べます。
この順番の良いところは、勘に頼る場面が最後まで出てこないことです。以前は再現条件を手当たり次第に試していた調査が、世代を軸にした絞り込みへと変わります。
どこまでやるか — 過剰にしない線引き
来歴は保険であって、目的ではありません。全生成を完璧に記録しようとすると、記録そのものが負担になり、結局続きません。私が守っている線引きはこうです。
状況 来歴の残し方
本番に出す生成 3層すべて(ID 焼き込み・台帳1行・テレメトリ)
試作・破棄前提の生成 何もしない。台帳を汚さない
課金・データ層に触れる生成 3層に加え、最小テストで壊れる箇所を1つ守る
課金やデータ層のように「壊れると痛い場所」だけは、来歴に加えて自動テストの薄い網をかけておくと安心です。生成が離れた場所を巻き戻しても、テストが赤くなって気づけます。この考え方は「Rork が生成した画面に最小限の自動テストを足す 」で詳しく触れています。長期の保守全体を俯瞰したい方は「Rork で作った個人アプリを3年以上持たせる保守設計 」も合わせてご覧いただければと思います。
来歴を残す作業は、1生成あたりおおむね1分です。生成が月に十数回あっても、月に十数分。半日の原因調査が一度でも減れば、十分に元が取れます。
まとめ
AI ビルダーは「作り直しやすさ」を最大の武器にしています。その武器を長く安全に振るうには、作り直した事実そのものを記録に残す規律が要ります。ビルドスタンプで手元の版を、台帳で生成の意図を、テレメトリで本番の影響を。この3層を同じ生成 ID で串刺しにするだけで、不具合の調査は「勘」から「逆引き」へ変わります。
次の生成から、GENERATIONS.md に1行だけ書き足してみてください。最初の1行は何の役にも立ちません。効いてくるのは、三度目に不具合が戻ってきた日です。
実運用の中で見つけた小さな工夫ですが、どなたかの調査時間を減らせたなら嬉しく思います。お読みいただきありがとうございました。