#!/usr/bin/env python3"""変更点の一覧から、自分の実装に触れる行だけを残す。 python3 relevance_filter.py inventory.tsv changes.txt終了コード(エージェント側の分岐に使う): 0 … 自分に関係する変化はなかった(報告しない) 10 … 関係する変化があった(報告する) 2 … 入力が読めなかった(設定の間違い。黙って 0 を返さない)"""import reimport sysfrom pathlib import Path# 一般語と衝突する断片は単独キーにしない。誤検出の主因はここでした。GENERIC = { "internet", "wake_lock", "vibrate", "camera", "storage", "notification", "notifications", "service", "activity", "manager",}def keys_for(symbol: str) -> set: """android.app.WallpaperManager -> 全体 / app.wallpapermanager / wallpapermanager""" parts = [p for p in re.split(r"[.\s]", symbol) if p] out = {symbol.lower()} if parts: last = parts[-1].lower() if last not in GENERIC and len(last) > 3: out.add(last) if len(parts) >= 2: out.add(f"{parts[-2]}.{parts[-1]}".lower()) return outdef load_inventory(path: Path) -> dict: table = {} for line in path.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue kind, _, symbol = line.partition("\t") if not symbol: continue for key in keys_for(symbol): if len(key) > 3: table.setdefault(key, f"{kind}:{symbol}") return tabledef main() -> int: if len(sys.argv) != 3: print(__doc__, file=sys.stderr) return 2 inv_path, changes_path = Path(sys.argv[1]), Path(sys.argv[2]) if not inv_path.is_file() or not changes_path.is_file(): print(f"入力が読めません: {inv_path} / {changes_path}", file=sys.stderr) return 2 table = load_inventory(inv_path) if not table: print("インベントリが空です。抽出側が壊れている可能性があります。", file=sys.stderr) return 2 hits = [] for lineno, raw in enumerate(changes_path.read_text(encoding="utf-8").splitlines(), 1): low = raw.lower() why = sorted({w for key, w in table.items() if key in low}) if why: hits.append((lineno, raw.strip(), why)) if not hits: print("関係する変化はありません。") return 0 for lineno, text, why in hits: print(f"L{lineno}: {text}") for w in why: print(f" ← {w}") print(f"\n該当 {len(hits)} 件 / 監視対象 {len(table)} キー") return 10if __name__ == "__main__": sys.exit(main())
五行の変更点リストを通すと、残るのは二行だけでした。
L1: Behavior change: WallpaperManager.setBitmap now throws when the caller is in the background.
← import:android.app.WallpaperManager
L4: Foreground services with type dataSync are limited to 6 hours per day.
← service-type:dataSync
該当 2 件 / 監視対象 18 キー
L1: Chrome now blocks mixed content over the internet.
← permission:android.permission.INTERNET
L2: Android 17 tightens INTERNET-less loopback access.
← permission:android.permission.INTERNET
android.permission.INTERNET から internet という単語をキーにしていたため、英文に自然に出てくる「インターネット」がすべて一致していました。ほとんどのアプリが宣言している権限が、ほとんどの文章に一致するキーを持っている。この組み合わせは、フィルタとしては最悪の性質です。関連性フィルタを書くときの典型的な落とし穴で、回避の仕方そのものは単純でした。一般語と衝突する断片を集合として先に列挙し、単独のキーには使わないと決めるだけです。
ですから、検知の精度を上げる努力と同じくらい、漏れた前提の受け皿に手をかけています。私の場合は Google Play の段階公開がその役割です。小さな割合から配信を始め、クラッシュ率と ANR の推移を見てから広げる。本番環境で異常が出たときに配信を止められる状態を保っておけば、検知の取りこぼしは事故ではなく手戻りで済みます。この手順は Android 17 に限らず使っている運用ですが、外部から与えられる検証の区切りが消えたぶん、比重が上がりました。