「検出0件」という結果ほど、疑ってかかるべきものはありませんでした。
Google Play が 2026年7月15日のポリシー告知で、ポリシー審査用のオープンソースツールを案内しています。LLM を Play のポリシー文書に接地させて、IDE や CLI の中でコードを評価させる、という趣旨のものです。名前は Play Policy Insights。提出してから差し戻される回数を減らせるなら、個人開発で6本のアプリを並行して抱えている身としては、試さない理由がありません。
手元に Expo 構成の最小プロジェクトを組んで走らせました。1回目、android/app を指定。検出0件。2回目、リポジトリのルートを指定。データ収集が5カテゴリ検出されました。同じコード、同じ権限、同じ依存です。
差はディレクトリの指定だけでした。
Play Policy Insights が実際に見ているもの
このスキルは android/skills リポジトリ で配布されています。監査対象は3ドメイン、Permissions and APIs Hygiene(権限と API の衛生)、User Account and Identity(アカウントと本人性)、Data Safety and Privacy(データセーフティとプライバシー)です。
構成は2フェーズです。第1フェーズで orchestrator.py init <app_dir> を実行し、静的解析でコードベースを地図化して、必要な監査ゴールを判定します。第2フェーズで、判定されたゴールごとにプロンプトが生成され、AI アシスタント側がそれを実行して JSON を書き出します。最後に generate_report.py が Markdown のレポートにまとめます。
フェーズ 実行するもの 成果物
Phase 1 orchestrator.py init <app_dir>activated_goals・静的解析結果 JSON・ゴール別プロンプト
Phase 2 ゴール別プロンプトの実行 → orchestrator.py aggregate aggregated_findings.json
Finalize generate_report.py <temp_dir>compliance_report.md
重要なのは、精度のすべてが第1フェーズの静的解析に乗っているという点です。ここで拾えなかったものは、後段の AI がどれだけ賢くても復元されません。スキル自身も「Phase 1 の自動監査が真実の源泉であり、失敗したら即座に停止せよ。手動監査へのフォールバックは禁じる」と明記しています。裏を返せば、第1フェーズが静かに空振りしたときが一番危ない ということです。
条件1: 走査の入口がリポジトリのルートでないと、ハイブリッド判定が起きません
scanner.py は、指定されたディレクトリとその親ディレクトリ だけを見て、package.json か pubspec.yaml があるかを確認します。あれば「ハイブリッドアプリ」と判定して、走査のルートをそこまで広げます。
Expo プロジェクトのルートは package.json があるリポジトリ直下です。android/app を指定すると、親は android であって、そこには package.json がありません。ハイブリッド判定は起きず、JS/TS のコードは1行も走査されません。
手元で確認した差がこれです。
指定したディレクトリ activated_goals 検出されたデータカテゴリ
リポジトリのルート data_safety_part_1, permissions_and_apis5件
android/app(空) 0件
Android アプリの監査だと言われたら、android の下を指したくなります。私自身、最初にそうしました。ところが Expo / React Native では、権限を要求している実体は JS 側にあります。ネイティブ側だけを見に行った瞬間、監査は成立しなくなります。
init の標準エラー出力に Hybrid app detected. Expanding scan root to: ... という1行が出るかどうかが、成否の見分け方です。この行が出ていなければ、そのレポートは読む価値がありません。
条件2: Expo prebuild が出す Gradle の書き方だと、targetSdk が読めません
Google Play は 2026年8月31日から、対象 API レベル36 以上を要求します。同じ7月15日の告知に「Reminders」として並記されている、毎年恒例の更新です。提出前チェックで真っ先に見たいのはここのはずです。
ところが、Expo prebuild が生成する android/app/build.gradle の既定の書き方では、この値が読み取れませんでした。parse_application_modules に4通りの記法を直接食わせて確かめた結果です。
build.gradle の書き方 applicationId targetSdk
Expo prebuild 既定(targetSdkVersion rootProject.ext.targetSdkVersion) None None
Groovy + リテラル + 単一引用符の applicationId None 36
Groovy + リテラル + 二重引用符の applicationId None 36
Kotlin DSL(applicationId = "..." / targetSdk = 36) com.example.wallpaper 36
原因は、SDK バージョンを rootProject.ext 経由の変数参照で書いている点にあります。スキル側は gradle.properties とバージョンカタログを解決対象に持っていますが、android/build.gradle の ext { } ブロックに置かれた値までは辿りません。結果として manifest_details.json の target_sdk が null のまま後段に渡ります。
applicationId のほうも、Groovy DSL では4通り中3通りで解決に失敗しました。解決できなかった場合、package_name には package.json の name フィールドが入ります。私の再現プロジェクトでは wallpaper-demo という文字列がパッケージ名として記録されました。これはパッケージ名ではありません。
期限まで日数がない状況で、この2つが null のままレポートに乗ると、確認したつもりが確認できていない状態になります。走らせる前に、実効値を自分で出しておくのが確実です。
#!/usr/bin/env bash
# preflight.sh — Play Policy Insights を走らせる前の入口チェック
# 使い方: ./preflight.sh /path/to/expo-project
set -euo pipefail
ROOT = " ${1 :? プロジェクトのルートを指定してください } "
# 1) 走査ルートの妥当性: package.json がここにあるか
if [ ! -f " $ROOT /package.json" ]; then
echo "NG: $ROOT に package.json がありません。ハイブリッド判定が起きず JS が走査されません。"
echo " → リポジトリのルートを指定してください。"
exit 1
fi
echo "OK: 走査ルート = $ROOT "
# 2) targetSdk の実効値: 変数参照のままだとスキルが読めない
GRADLE = " $ROOT /android/app/build.gradle"
if [ -f " $GRADLE " ]; then
LINE = $( grep -E '^\s*targetSdk(Version)?\s' " $GRADLE " || true )
if echo " $LINE " | grep -qE 'targetSdk(Version)?\s+[0-9]+' ; then
echo "OK: targetSdk はリテラル指定です -> ${ LINE // [[ : space : ]] / }"
else
# ext ブロックから実値を引く(Expo prebuild の既定形)
EXT = $( grep -E 'targetSdkVersion\s*=\s*[0-9]+' " $ROOT /android/build.gradle" 2> /dev/null | head -1 || true )
echo "注意: targetSdk が変数参照です。スキルは null と判定します。"
echo " ext 側の実値: ${ EXT :- 見つかりません}"
fi
else
echo "注意: android/ がありません(managed workflow)。targetSdk は app.json 側で確認してください。"
fi
android/ を持たない managed workflow のプロジェクトでは、Gradle ファイル自体が存在しません。この場合スキルは対象 API レベルについて何も言いません。ビルダーが出したものをそのまま提出する運用ほど、この沈黙を「問題なし」と読んでしまいます。実効値の判定については、以前に書いたtargetSdkVersion 36 を通すまでに直した3か所 のほうに、宣言値ではなくビルドが読む値を出す手順をまとめてあります。
条件3: node_modules は走査対象外です
scanner_config.json の ignored_directories に node_modules が入っています。.git や build と同列の扱いです。走査時間を考えれば妥当な設計ですが、Expo / Rork 構成では意味が変わってきます。
Data safety の申告で実際に問題になるのは、自分で書いたコードよりも、依存が勝手に集めているもののほうです。私が運用している Android 2本と iOS 4本で申告項目を見直したときも、追加が必要になったのは AdMob の広告 ID と、Crashlytics が送るクラッシュログでした。どちらも自分のソースには getAdvertisingId() のような呼び出しが1行も出てきません。SDK を初期化した時点で発生します。
つまり、静的走査の網の外側に、申告義務の主要部分が置かれています。ここはツールに任せず、依存の一覧から手で棚卸しするしかありません。
# 収集系 SDK の棚卸し(申告漏れの候補を依存から機械的に列挙)
node -e '
const pkg = require("./package.json");
const deps = Object.keys({...pkg.dependencies, ...pkg.devDependencies});
const rules = [
[/google-mobile-ads|admob|applovin|unity-ads|inmobi|liftoff/i, "広告ID・おおよその位置情報"],
[/crashlytics|sentry|bugsnag/i, "クラッシュログ・診断情報"],
[/analytics|amplitude|mixpanel|firebase\/app$/i, "アプリの操作・診断情報"],
[/expo-location|geolocation/i, "位置情報"],
[/image-picker|expo-media-library|camera/i, "写真・動画"],
[/purchases|revenuecat|iap|billing/i, "購入履歴"],
];
const hits = [];
for (const d of deps) for (const [re, label] of rules) if (re.test(d)) hits.push([d, label]);
if (!hits.length) { console.log("収集系の依存は検出されませんでした"); process.exit(0); }
console.log("Data safety で申告が要る可能性のある依存:");
for (const [d, label] of hits) console.log(` ${d.padEnd(42)} -> ${label}`);
'
この出力はあくまで候補です。実際に何を送っているかは各 SDK のドキュメントで確認する必要があります。それでも、「依存に入っているのに申告に無い」ものを機械的に炙り出せるだけで、見落としはかなり減ります。
逆に、拾いすぎる側の誤検出もあります
走査が届いた場合でも、出力をそのまま Play Console に転記してはいけません。最小プロジェクトでの検出結果がこちらです。
検出カテゴリ 根拠として示されたもの 妥当か
PRECISE_LOCATION app/index.tsx(Pattern: latitude)妥当
FILES_AND_DOCS app/index.tsx(Pattern: AsyncStorage)要判断
PHOTOS / VIDEOS package.json(Pattern: expo-image-picker)誤り(呼び出し無し)
MUSIC app/index.tsx(Pattern: track)誤り
MUSIC が立った理由は、コード中の解析エンドポイント https://api.example.com/v1/track に含まれる track という文字列でした。設定ファイル側で MUSIC の判定語に track が登録されているためです。解析イベントを送る関数を trackEvent と名付けている人は、ほぼ全員がこれを踏みます。
PHOTOS と VIDEOS は、package.json に expo-image-picker が入っているというだけで立ちました。コードからは一度も呼ばれていません。ビルダーが生成したプロジェクトは、使っていない依存を初期状態で抱えていることが多いので、この過剰検出は日常的に出ます。
過剰と不足が同時に起きる、というのが静的走査の素直な性質です。私はこの出力を「申告の下書き」ではなく「確認すべき箇所のリスト」として扱っています。1件ずつ、根拠に挙がったファイルを開いて判断する。手間はかかりますが、Data safety の記載は差し戻しの理由になりますし、後から直すほうが高くつきます。データセーフティの記載そのものでつまずいた場合の直し方は、Google Play のデータセーフティ項目の修正 のほうにまとめています。
提出前に回している順番
6本を並行で見ていると、チェックの順番を固定しておかないと、どのアプリでどこまでやったかが分からなくなります。今はこの並びに落ち着いています。
preflight.sh を走らせ、走査ルートと targetSdk の実効値を確定させる
リポジトリのルートを指して orchestrator.py init を実行し、標準エラーに Hybrid app detected が出ることを確認する
activated_goals が空でないことを確認する。空なら入口が間違っている
依存の棚卸しスクリプトを走らせ、SDK 由来の収集項目を手で足す
検出結果を1件ずつ根拠ファイルで確認し、誤検出を落とす
Play Console の Data safety と突き合わせ、差分だけを修正する
3番を明示的な確認項目にしているのは、今回それで一度だまされたからです。空の activated_goals は「クリーン」ではなく「走っていない」を意味します。エラーも警告も出ません。JSON に空の配列が入るだけです。
段階公開に入ってからの監視は別の話ですが、こちらは Crash-free users と ANR のしきい値を先に決めておいて、下回ったら次の段に進めない、という運用にしています。ポリシー審査を通ることと、通った先で壊れないことは、別々に設計しておくほうが結局は早いというのが、個人開発を続けてきた中で身についた感覚です。
明日からできること
まず orchestrator.py init をリポジトリのルートで1回だけ走らせて、標準エラーに Hybrid app detected の行が出るかどうかを見てください。出ていれば入口は正しく、出ていなければ、これまでのチェックは何も見ていなかったことになります。
対象 API レベル36 の期限まで、あと12日です。走査が届いているかどうかの確認だけなら、数分で終わります。
なお、ここに載せた検出結果は、Expo 構成を最小限に再現したプロジェクトでの実行結果です。皆さんのプロジェクトでは依存もコードも違いますから、数字がそのまま一致することはありません。確認していただきたいのは値ではなく、走査が届く条件のほう です。
私自身まだ運用に組み込んでいる途中で、Phase 2 まで含めた精度は評価しきれていません。分かったことがあれば、また書きます。お読みいただきありがとうございました。