9月14日、という日付をしばらく眺めておりました。iOS 27 と iPadOS 27 の配信日です。対応は iPhone 11 以降。手元のカレンダーで数えると、あと四日でした。
個人開発でアプリを出しておりますと、この四日間の使い方で秋の忙しさがずいぶん変わります。以前の私は、まずソースコードを開いておりました。@available の分岐を数え、デプロイメントターゲットを確かめ、実機ごとの場合分けを見直すところまでを、毎年の手順にしておりました。そこまでやれば準備は済んだつもりでいたのです。
結果は芳しくありませんでした。配信の翌週、エージェントに書かせた新しい画面のコードに、また古い可用性の分岐が入っていたのです。私が直し忘れていたわけではありません。直した場所と、エージェントが読んでいた場所が違っていたのでした。
版数の文字列は、二つのゾーンに散らばっています
版数がリポジトリに文字列として残る場所は、性質の違う二つに分かれます。
| ゾーン | 読むもの | ファイルの例 | 間違いに気づく仕組み |
|---|---|---|---|
| A | 人間とコンパイラ | .swift / .h / .xcconfig / .plist / build.gradle | ビルド警告・コードレビュー・CI |
| B | エージェント | AGENTS.md / rules.json / agents.json / skills.json / .antigravity/ 配下 | ありません |
右端の列が要点です。ゾーン A の古い版数はコンパイラが教えてくれます。ゾーン B の古い版数は、誰も教えてくれません。文章として正しく、構文としても壊れていないからです。ただ、そこに書いてある前提のとおりにコードが生成されるだけなのです。
しかもゾーン B は増える方向にあります。Antigravity 2.11.0 で、プロジェクトのサブディレクトリに置いた skills.json / agents.json / rules.json も探索の対象に入りました。あわせて AGENTS.md とカスタムルールファイルの中で @path/to/file 記法が使えるようになり、外部のファイルをその場に展開できます。さらに 2.12.2 では、Markdown で定義したカスタムエージェントが周囲のスキル・ルール・サブエージェントを既定で継承するようになりました。
つまり、リポジトリの根元にある AGENTS.md だけを見て「確認しました」と言える状態では、もうないのです。
二つのゾーンを別々に数えます
そこで、配信日が決まった日に走らせる棚卸しのスクリプトを用意しました。ゾーンを分けて数えることだけを目的にしています。
#!/usr/bin/env bash
# os-literals.sh — 古い OS 版数がリポジトリのどこに文字列として残っているかを数えます。
# 使い方: bash os-literals.sh <リポジトリのパス> <古い版数> [新しい版数]
# 例: bash os-literals.sh . 26 27
set -uo pipefail
ROOT="${1:-.}"; OLD="${2:?古い版数を指定してください(例: 26)}"; NEW="${3:-}"
# 版数の直前に来る語だけを拾います。単独の "26" は年号にも座標にも当たるためです。
PAT="(iOS|iPadOS|macOS|watchOS|tvOS|visionOS|Xcode|SDK|DEPLOYMENT_TARGET|available)[^0-9A-Za-z]{0,12}${OLD}([.][0-9]+)?"
list_a() { # 人間とコンパイラが読むもの
find "$ROOT" -type f \( -name '*.swift' -o -name '*.h' -o -name '*.m' -o -name '*.kt' \
-o -name '*.xcconfig' -o -name '*.plist' -o -name '*.gradle' -o -name '*.pbxproj' \) \
-not -path '*/.git/*' -not -path '*/node_modules/*' -print0 2>/dev/null
}
list_b() { # エージェントだけが読むもの
find "$ROOT" -type f \( -name 'AGENTS.md' -o -name '*.agent.md' -o -name 'skills.json' \
-o -name 'agents.json' -o -name 'rules.json' -o -name '.antigravityrules' \
-o -path '*/.antigravity/*' \) \
-not -path '*/.git/*' -not -path '*/node_modules/*' -print0 2>/dev/null
}
report() { # report <ラベル> <ファイル列挙関数>
local label="$1" hits n
hits=$("$2" | xargs -0 -r grep -InE "$PAT" 2>/dev/null)
n=$(printf '%s' "$hits" | grep -c . )
printf '%s: %d 件\n' "$label" "$n"
[ "$n" -gt 0 ] && printf '%s\n' "$hits" | sed "s|^${ROOT}/||; s/^/ /"
return 0
}
echo "== ゾーン A: 人間とコンパイラが読むもの =="
report "ソース・ビルド設定" list_a
echo
echo "== ゾーン B: エージェントだけが読むもの =="
report "エージェント設定" list_b
echo
echo "== @ 記法で取り込まれている参照先 =="
list_b | xargs -0 -r grep -hoE '@[A-Za-z0-9_./-]+\.(md|json|ya?ml)' 2>/dev/null \
| sed 's/^@//' | sort -u | while read -r ref; do
if [ -f "$ROOT/$ref" ]; then
c=$(grep -cE "$PAT" "$ROOT/$ref" 2>/dev/null || true)
printf ' %-40s %s 件\n' "$ref" "${c:-0}"
else
printf ' %-40s 参照先が見つかりません\n' "$ref"
fi
done小さな検証用のリポジトリで走らせた出力が、こちらです。
$ bash os-literals.sh ./fixture 26 27
== ゾーン A: 人間とコンパイラが読むもの ==
ソース・ビルド設定: 2 件
ios/Config/Base.xcconfig:1:IPHONEOS_DEPLOYMENT_TARGET = 26.0
src/Views/HomeView.swift:4: if #available(iOS 26.0, *) { Text("new") } else { Text("old") }
== ゾーン B: エージェントだけが読むもの ==
エージェント設定: 3 件
packages/mobile/rules.json:1:{ "rules": ["Assume Xcode 26 toolchain", "minimum iOS 26.0"] }
.antigravity/rules/swiftui.md:1:Prefer the iOS 26 glass material APIs.
AGENTS.md:3:Target platform: iOS 26 and later. Use `@available(iOS 26, *)` when adding new APIs.
== @ 記法で取り込まれている参照先 ==
.antigravity/rules/swiftui.md 1 件ゾーン B のほうが件数が多いことが分かります。私が最初にこの分け方で数えたとき、素直に肩の力が抜けました。直すべき場所は多いのですが、どこを見ればよいかがはっきりしたからです。
最後の節も見落とせません。.antigravity/rules/swiftui.md は AGENTS.md から @ 記法で取り込まれているファイルです。ルールを分割して整理している方ほど、根元のファイルだけを開いて安心してしまいます。取り込み先を辿る一手間が要ります。
keyword では拾えない一行が残ります
この棚卸しには、あえて拾わないようにしている行があります。iOS や Xcode のような語を伴わない、裸の版数です。
// ios/Config/DefineManager.h
#define kMinOS 26.026.0 だけを検索の対象にすると、座標や年号や無関係な定数まで一緒に釣れてしまいます。ですから第一のパスでは外し、目視前提の第二のパスとして分けて走らせています。
# 第二のパス。件数は少なく、そのぶん一件ずつ目で見ます。
grep -rnE '(^|[^0-9A-Za-z.])26\.0([^0-9]|$)' . \
--include='*.h' --include='*.xcconfig' --include='*.json'これを分けたのは、一度に全部を拾おうとして失敗したからです。最初のうち、私は網を広げすぎておりました。出力が数百行になり、目が滑り、結局どれも直さないまま配信日を迎えてしまったのです。いまは「keyword つきは機械が、裸の数値は自分が」という分け方に落ち着いております。
そのうえで、書き手として引いた線がもうひとつあります。版数は、それが何の版数かを示す語と並べて書いておきます。 #define kMinOS 26.0 ではなく #define kMinOS_iOS 26.0 と書いてあれば、来年の私も、エージェントも、一度の検索で見つけられます。検索できる形で書き残すことは、それ自体が保守なのだと感じています。
直す順番は、影響の大きい側から
件数が出たあとの順番も決めてあります。
- ゾーン B のうち
@で取り込まれているファイル — 展開されて複数のエージェントに効くため、一箇所の修正で効き目がいちばん広く届きます - ゾーン B の残り(
AGENTS.md・各rules.json)— 継承が既定になった以上、狭い文脈のつもりで書いたルールも周囲へ伝わります - ゾーン A のビルド設定 —
.xcconfigと.plist。ここはビルドが教えてくれるので、慌てなくても大丈夫です - ゾーン A の可用性分岐 — 数が多いぶん時間がかかります。新しい版が降りてから、実機の挙動を見つつ順に
ゾーン B を先に直すのは、ここを放っておくとゾーン A の修正が上書きされていくからです。古い前提の残ったルールを読んだエージェントは、せっかく直した分岐を、次の生成でまた古い形に戻してしまいます。
なお、審査側の準備は別立てで進めておくと楽になります。2026年9月から、新規提出・アップデート・代替配信向けの公証申請のいずれにも年齢レーティングの質問票への回答が必要になりました。この答えをリポジトリ側の宣言から組み立てる方法は、年齢レーティングの質問票に、記憶ではなくリポジトリの宣言で答えるに書きました。配信直後は提出が混み合いますので、四日のうちに済ませておくと呼吸が楽になります。アプリ側の初期化と権利の復元についてはiOS 27 の RC から正式リリースまでの5日間のほうにまとめてあります。
四日のうちに、一度だけ走らせてください
今日できることは一つだけで足ります。手元のリポジトリで os-literals.sh を一度走らせ、ゾーン B の件数を見てください。ゼロならそのまま配信日を迎えられますし、一件でもあれば、そこがあなたのエージェントの読んでいる前提です。
私自身、この棚卸しを始めてから、新しい OS が降りてくる週の胃の痛みがだいぶ減りました。準備が完璧になったからではありません。どこを見ていないかが分かっているという、それだけの違いなのです。お読みいただきありがとうございました。