geofeed-tuner について
geofeed-tunerは、グローバルにルーティング可能なIPアドレス空間の位置情報を示すCSVフィードを作成・改善するスキルです。RFC 8805の形式とデータ品質を確認し、FastahのMCPツールで調整用データを取得してHTMLレポートを生成します。
主な機能
- 01RFC 8805に沿ってCSV構造を確認
- 02公開IPプレフィックスを検証
- 03データ品質とプライバシー上の問題を検出
- 04Fastahの検索データを使って調整
- 05HTML分析レポートを生成
活用シーン
- 01公開用の位置情報フィードを準備
- 02既存フィードの形式エラーを確認
- 03IP位置情報の改善案を確認
MCP 対応
調整データの検索段階でFastahのMCPツールを使い、位置情報の精度向上に役立つデータを取得します。READMEにはMCPクライアント向けに公開するツールの説明はありません。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ github/ awesome-copilot --skill geofeed-tunerインストール後、Claude Code で/geofeed-tunerを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/iso3166-1.jsoniso3166-2.jsonsmall-territories.jsonassets/example/01-user-input-rfc8805-feed.csvreferences/rfc8805.txtsnippets-python3.mdscripts/templates/index.html詳細
Geofeed Tuner – より優れた IP ジオロケーションフィードを作成する
このスキルは、次のことを通じて、CSV 形式の IP ジオロケーションフィードの作成と改善を支援します。
- CSV の形式と一貫性を確保する
- RFC 8805(業界標準)との整合性を確認する
- 実際の運用から得られた独自のベストプラクティスを適用する
- 精度、網羅性、プライバシーの向上を提案する
このスキルを使用する場面
- ユーザーが CSV 形式の IP ジオロケーションフィードファイルの作成、改善、公開について支援を求めた場合に、このスキルを使用します。
- CSV ジオロケーションフィードの調整やトラブルシューティングに使用します。エラーを検出し、改善を提案して、RFC への準拠にとどまらない実運用での使いやすさを確保します。
- 対象ユーザー:
- 公開ルーティング可能な IP アドレス空間を担当するネットワークオペレーター、管理者、エンジニア
- ISP、モバイル通信事業者、クラウドプロバイダー、ホスティング事業者やコロケーション事業者、インターネットエクスチェンジの運営者、衛星インターネットプロバイダーなどの組織
- このスキルは、プライベート IP アドレスや内部 IP アドレスの管理には使用しないでください。対象は公開ルーティング可能な IP アドレスのみです。
前提条件
- Python 3 が必要です。
ディレクトリ構成とファイル管理
このスキルでは、配布ファイル(読み取り専用)と作業ファイル(実行時に生成)の領域を明確に分けています。
読み取り専用ディレクトリ(変更禁止)
次のディレクトリには静的な配布用アセットが含まれています。これらのディレクトリ内でファイルを作成、変更、削除しないでください。
| ディレクトリ | 用途 |
|---|---|
assets/ | 静的データファイル(ISO コード、サンプル) |
references/ | 参照用の RFC 仕様とコードスニペット |
scripts/ | レポート用の実行可能コードと HTML テンプレートファイル |
作業ディレクトリ(生成コンテンツ)
生成ファイル、一時ファイル、出力ファイルはすべて、次のディレクトリに格納します。
| ディレクトリ | 用途 |
|---|---|
run/ | エージェントが生成するすべてのコンテンツの作業ディレクトリ |
run/data/ | リモート URL からダウンロードした CSV ファイル |
run/report/ | 生成された HTML 調整レポート |
ファイル管理ルール
- **
assets/、references/、scripts/には書き込まないでください。**これらはスキルの配布物の一部であり、変更せずに保つ必要があります。 - リモート URL からダウンロードした入力ファイルはすべて、
./run/data/に保存してください。 - 生成された HTML レポートはすべて、
./run/report/に保存してください。 - 生成された Python スクリプトはすべて、
./run/に保存してください。 run/ディレクトリはセッション間で消去される場合があります。永続データを保存しないでください。- 実行時の作業ディレクトリ:
./run/内で生成したスクリプトはすべて、スキルのルートディレクトリ(SKILL.mdを含むディレクトリ)をカレントディレクトリとして実行する必要があります。これにより、assets/iso3166-1.jsonや./run/data/report-data.jsonなどの相対パスが正しく解決されます。スクリプトの実行前に./run/にcdしないでください。
処理パイプライン:フェーズの順次実行
すべてのフェーズを、フェーズ 1 からフェーズ 6 まで順番に実行する必要があります。各フェーズは、前のフェーズが正常に完了していることを前提とします。たとえば、構造チェックを完了してからでなければ、品質分析を実行できません。
各フェーズの概要を以下に示します。エージェントは、各フェーズのセクションで後述する詳細な手順に従う必要があります。
| フェーズ | 名前 | 説明 |
|---|---|---|
| 1 | 標準を理解する | 自己公開型 IP ジオロケーションフィードに関する RFC 8805 の主要要件を確認する |
| 2 | 入力を収集する | ローカルファイルまたはリモート URL から IP サブネットデータを収集する |
| 3 | チェックと提案 | CSV の構造を検証し、IP プレフィックスを分析して、データ品質を確認する |
| 4 | チューニングデータの検索 | Fastah の MCP ツールを使用して、ジオロケーションの精度を高めるためのチューニングデータを取得する |
| 5 | チューニングレポートを生成する | 分析と提案をまとめた HTML レポートを作成する |
| 6 | 最終確認 | レポートデータの整合性と完全性を確認する |
フェーズを省略しないでください。 各フェーズでは、後続の段階で必要となる重要なチェックやデータ変換を行います。
実行計画のルール
各フェーズを実行する前に、エージェントは確認可能な TODO チェックリストを必ず生成する必要があります。
計画は次の要件を満たす必要があります。
- フェーズの最初に表示する
- すべての手順を順番に記載する
- チェックボックス形式を使用する
- 手順の完了に合わせて随時更新する
フェーズ 1:標準を理解する
このスキルで適用する RFC 8805 の主要要件を以下にまとめます。この要約を作業時の参考資料として使用してください。 エッジケース、あいまいな状況、またはここに記載されていない規格に関する質問をユーザーから受けた場合に限り、RFC 8805 text の全文を参照してください。
RFC 8805 の要点
目的: 自己公開型 IP ジオロケーションフィードを使うと、ネットワーク事業者は自らが管理する IP アドレス空間の権威ある位置情報をシンプルな CSV 形式で公開でき、ジオロケーションプロバイダーは事業者が提供する修正情報を取り込めます。
CSV の列順(セクション 2.1.1.1~2.1.1.5):
| 列 | フィールド | 必須 | 備考 |
|---|---|---|---|
| 1 | ip_prefix | はい | CIDR 表記。IPv4 または IPv6。ネットワークアドレスである必要があります |
| 2 | alpha2code | いいえ | ISO 3166-1 alpha-2 の国コード。空欄または「ZZ」はジオロケーションを適用しないことを示します |
| 3 | region | いいえ | ISO 3166-2 の下位区分コード(例:US-CA) |
| 4 | city | いいえ | 自由記述の市区町村名。権威ある検証用データセットはありません |
| 5 | postal_code | いいえ | 非推奨 — 空欄にするか、列自体を省略する必要があります |
構造上のルール:
- ファイルには、
#で始まるコメント行を含めることができます(ヘッダーがある場合はヘッダーも含みます)。 - ヘッダー行は任意です。存在する場合、
#で始まるとコメントとして扱われます。 - ファイルは UTF-8 でエンコードする必要があります。
- サブネットのホストビットを設定してはいけません(つまり、
192.168.1.1/24は無効です。192.168.1.0/24を使用してください)。 - 対象となるのはグローバルにルーティング可能なユニキャストアドレスのみです。プライベート、ループバック、リンクローカル、マルチキャストのアドレス空間は対象外です。
ジオロケーションを適用しない場合: alpha2code が空欄、または大文字小文字を区別せず ZZ のエントリーは(region/city の値にかかわらず)、そのプレフィックスにジオロケーションを適用しないことを示す明示的なシグナルです。
郵便番号は非推奨(セクション 2.1.1.5): 5 列目に郵便番号や ZIP コードを含めてはいけません。IP 範囲のマッピングには細かすぎるうえ、プライバシー上の懸念があるためです。
フェーズ 2:入力の収集
-
ユーザーが IP サブネットまたは範囲(
inetnumまたはinet6numと呼ばれることもあります)のリストをまだ提供していない場合は、提供するよう求めてください。受け付ける入力形式:- チャットに貼り付けたテキスト
- ローカルの CSV ファイル
- CSV ファイルを指すリモート URL
-
入力がリモート URLの場合:
- 処理を開始する前に、CSV ファイルを
./run/data/にダウンロードしてみてください。 - HTTP エラー(4xx、5xx、タイムアウト、リダイレクトループ)が発生した場合は、直ちに中止し、ユーザーに次の内容を報告してください:
Feed URL is not reachable: HTTP {status_code}. Please verify the URL is publicly accessible. - 不完全なダウンロードや空のダウンロードのまま、フェーズ 3 に進まないでください。
- 処理を開始する前に、CSV ファイルを
-
入力がローカルファイルの場合は、ダウンロードせずにそのまま処理してください。
-
エンコーディングの検出と正規化:
- まず、ファイルを UTF-8 として読み込んでみてください。
UnicodeDecodeErrorが発生した場合は、utf-8-sig(BOM 付き UTF-8)、次にlatin-1を試してください。- 正常にデコードできたら、再エンコードして、作業用コピーを UTF-8 で書き込んでください。
- どのエンコーディングでも読み込めない場合は中止し、次の内容を報告してください:
Unable to decode input file. Please save it as UTF-8 and try again.
フェーズ 3:チェックと提案
実行ルール
- このフェーズ用のスクリプトを生成してください。
- このフェーズを他のフェーズと統合しないでください。
- 後続フェーズのデータを事前計算しないでください。
- 出力を JSON ファイルとして
./run/data/report-data.jsonに保存してください。
スキーマ定義
以下の JSON 構造は、フェーズ 3 では変更不可です。フェーズ 4 では、後に Entries 内の各オブジェクトに TunedEntry オブジェクトが追加されます。これは許可される唯一のスキーマ拡張であり、別のフェーズで行われます。
JSON のキーは、{{.CountryCode}}、{{.HasError}} などのテンプレートプレースホルダーに直接対応します。
{
"InputFile": "",
"Timestamp": 0,
"TotalEntries": 0,
"IpV4Entries": 0,
"IpV6Entries": 0,
"InvalidEntries": 0,
"Errors": 0,
"Warnings": 0,
"OK": 0,
"Suggestions": 0,
"CityLevelAccuracy": 0,
"RegionLevelAccuracy": 0,
"CountryLevelAccuracy": 0,
"DoNotGeolocate": 0,
"Entries": [
{
"Line": 0,
"IPPrefix": "",
"CountryCode": "",
"RegionCode": "",
"City": "",
"Status": "",
"IPVersion": "",
"Messages": [
{
"ID": "",
"Type": "",
"Text": "",
"Checked": false
}
],
"HasError": false,
"HasWarning": false,
"HasSuggestion": false,
"DoNotGeolocate": false,
"GeocodingHint": "",
"Tunable": false
}
]
}
フィールド定義:
トップレベルのメタデータ:
InputFile:元の入力ソース。ローカルのファイル名またはリモート URL です。Timestamp:チューニングを実行した時点の Unix エポックからの経過時間(ミリ秒)です。TotalEntries:処理したデータ行の合計数(コメント行と空行を除く)です。IpV4Entries:IPv4 サブネットであるエントリの数です。IpV6Entries:IPv6 サブネットであるエントリの数です。InvalidEntries:IP プレフィックスの解析および CSV の解析に失敗したエントリの数です。Errors:StatusがERRORのエントリの合計数です。Warnings:StatusがWARNINGのエントリの合計数です。OK:StatusがOKのエントリの合計数です。Suggestions:StatusがSUGGESTIONのエントリの合計数です。CityLevelAccuracy:Cityが空でない有効なエントリの数です。RegionLevelAccuracy:RegionCodeが空でなく、Cityが空の有効なエントリの数です。CountryLevelAccuracy:CountryCodeが空でなく、RegionCodeとCityが空の有効なエントリの数です。DoNotGeolocate(メタデータ):CountryCode、RegionCode、Cityがすべて空の有効なエントリの数です。
エントリのフィールド:
Entries:データ行ごとに 1 つのオブジェクトを格納する配列です。各エントリには次のフィールドがあります。Line:元の CSV における 1 始まりの行番号(コメント行と空行を含むすべての行を数えます)。IPPrefix:CIDR のスラッシュ表記に正規化された IP プレフィックスです。CountryCode:ISO 3166-1 alpha-2 の国コード、または空文字列です。RegionCode:ISO 3166-2 の地域コード(例:US-CA)、または空文字列です。City:都市名、または空文字列です。Status:割り当てられた最も高い重大度です。ERROR>WARNING>SUGGESTION>OKの順です。IPVersion:解析された IP プレフィックスに基づく"IPv4"または"IPv6"です。Messages:メッセージオブジェクトの配列です。各オブジェクトには次のフィールドがあります。ID:以下の検証ルールリファレンス表に記載されている文字列識別子(例:"1101"、"3301")です。Type:重大度の種類です。"ERROR"、"WARNING"、または"SUGGESTION"です。Text:人が読める形式の検証メッセージ文字列です。Checked:検証ルールが自動チューニング可能な場合(リファレンス表でTunable: trueの場合)はtrue、それ以外はfalseです。レポート内のチェックボックスがcheckedまたはdisabledのどちらになるかを制御します。
HasError:いずれかのメッセージのTypeが"ERROR"の場合はtrueです。HasWarning:いずれかのメッセージのTypeが"WARNING"の場合はtrueです。HasSuggestion:いずれかのメッセージのTypeが"SUGGESTION"の場合はtrueです。DoNotGeolocate(エントリ):CountryCodeが空または"ZZ"の場合はtrueです。このエントリは、ジオロケーションを明示的に行わないよう指定しています。GeocodingHint:フェーズ 3 では常に空文字列""です。将来の使用に備えて予約されています。Tunable:エントリ内のいずれかのメッセージにChecked: trueがある場合はtrueです。すべてのメッセージのChecked値に対する論理和として計算されます。このフラグにより、レポート内の「Tune」ボタンの表示が制御されます。
検証ルールのリファレンス
エントリにメッセージを追加するときは、この表の ID、Type、Text、Checked の値を使用します。
| ID | Type | Text | Checked | Condition Reference |
|---|---|---|---|---|
1101 | ERROR | IP プレフィックスが空です | false | IP Prefix Analysis: empty |
1102 | ERROR | 無効な IP プレフィックスです。IPv4 または IPv6 ネットワークとして解析できません | false | IP Prefix Analysis: invalid syntax |
1103 | ERROR | RFC 8805 フィードでは、パブリックではない IP 範囲は許可されません | false | IP Prefix Analysis: non-public |
3101 | SUGGESTION | IPv4 プレフィックスが通常より大きく、タイプミスの可能性があります | false | IP Prefix Analysis: IPv4 < /22 |
3102 | SUGGESTION | IPv6 プレフィックスが通常より大きく、タイプミスの可能性があります | false | IP Prefix Analysis: IPv6 < /64 |
1201 | ERROR | 無効な国コードです。有効な ISO 3166-1 alpha-2 の値ではありません | true | Country Code Analysis: invalid |
1301 | ERROR | 無効な地域形式です。COUNTRY-SUBDIVISION(例:US-CA)が必要です | true | Region Code Analysis: bad format |
1302 | ERROR | 無効な地域コードです。有効な ISO 3166-2 の区分ではありません | true | Region Code Analysis: unknown code |
1303 | ERROR | 地域コードが指定された国コードと一致しません | true | Region Code Analysis: mismatch |
1401 | ERROR | 無効な都市名です。プレースホルダー値は使用できません | false | City Name Analysis: placeholder |
1402 | ERROR | 無効な都市名です。省略形またはコード形式の値が検出されました | true | City Name Analysis: abbreviation |
2401 | WARNING | 都市名の書式が一貫していません。値の正規化を検討してください | true | City Name Analysis: formatting |
1501 | ERROR | RFC 8805 では郵便番号が非推奨であり、プライバシー上の理由から削除する必要があります | true | Postal Code Check |
3301 | SUGGESTION | 小規模な地域では通常、地域情報は不要です。地域の値を削除することを検討してください | true | Tuning: small territory region |
3402 | SUGGESTION | 小規模な地域では通常、市区町村レベルの詳細情報は不要です。市区町村の値を削除することを検討してください | true | Tuning: small territory city |
3303 | SUGGESTION | 都市を指定する場合は地域コードが推奨されます。ドロップダウンから地域を選択してください | true | Tuning: missing region with city |
3104 | SUGGESTION | このサブネットが意図的にジオロケーション対象外として指定されているのか、位置情報が欠落しているのかを確認してください | true | Tuning: unspecified geolocation |
メッセージの設定
検証チェックに該当した場合は、リファレンス表の値を使用してエントリの Messages 配列にメッセージを追加します。
entry["Messages"].append({
"ID": "1201", # From the table
"Type": "ERROR", # From the table
"Text": "Invalid country code: not a valid ISO 3166-1 alpha-2 value", # From the table
"Checked": True # From the table (True = tunable)
})
エントリのすべてのメッセージを設定した後、エントリレベルのフラグを導出します。
entry["HasError"] = any(m["Type"] == "ERROR" for m in entry["Messages"])
entry["HasWarning"] = any(m["Type"] == "WARNING" for m in entry["Messages"])
entry["HasSuggestion"] = any(m["Type"] == "SUGGESTION" for m in entry["Messages"])
entry["Tunable"] = any(m["Checked"] for m in entry["Messages"])
精度レベルのカウント規則
精度レベルは相互に排他的です。有効な(ERROR ではなく、無効でもない)各エントリを、最も詳細な空でない地理フィールドに基づいて、必ず1つのバケットに割り当てます。
| 条件 | バケット |
|---|---|
City が空でない | CityLevelAccuracy |
RegionCode が空でなく、かつ City が空 | RegionLevelAccuracy |
CountryCode が空でなく、RegionCode と City が空 | CountryLevelAccuracy |
DoNotGeolocate(エントリ)が true | DoNotGeolocate(メタデータ) |
HasError: true のエントリや InvalidEntries 内のエントリは、どの精度バケットにもカウントしないでください。
エージェントは以下を行ってはいけません。
- フィールド名を変更する
- フィールドを追加または削除する
- データ型を変更する
- キーの順序を変更する
- ネスト構造を変更する
- オブジェクトをラップする
- 複数のファイルに分割する
値が不明な場合は、空のままにしてください。データを捏造してはいけません。
構造と形式の確認
このフェーズでは、フィードの形式が正しく、解析可能であることを確認します。チューナーがジオロケーションの品質を分析する前に、重大な構造エラーを解決する必要があります。
CSV 構造
このサブセクションでは、IP ジオロケーションフィードに使用するCSV 形式の入力ファイルの規則を定義します。 目標は、ファイルを確実に解析し、一貫した内部表現に正規化できるようにすることです。
-
CSV 構造のチェック
-
pandasが利用可能な場合は、CSV の解析に使用します。 -
それ以外の場合は、Python 標準の
csvモジュールにフォールバックします。 -
CSV に論理列がちょうど4列または5列含まれていることを確認します。
-
コメント行は使用できます。
-
ヘッダー行は、あってもなくてもかまいません。
-
ヘッダー行がない場合は、次の列順を暗黙の順序として使用します。
ip_prefix, alpha2code, region, city, postal code (deprecated) -
入力ファイルの例を参照してください。
assets/example/01-user-input-rfc8805-feed.csv
-
-
CSV のクレンジングと正規化
-
次の操作と同等の Python ロジックを使用して、CSV をクレンジングし、正規化します。
- 先頭から5列のみを選択し、5列目以降の列を削除します。
- 出力ファイルを UTF-8 BOM 付きで書き込みます。
-
コメント
- 1列目が
#で始まるコメント行を削除します。 - これにより、
#で始まるヘッダー行も削除されます。 - 1始まりの行番号をキー、元の行全体を値として、コメントのマップを作成します。空行も保存します。
- このマップを JSON ファイル
./run/data/comments.jsonに保存します。 - 例:
{ "4": "# It's OK for small city states to leave state ISO2 code unspecified" }
- 1列目が
-
-
注意
pandasと標準のcsvのどちらを使用する場合も、UTF-8 BOM が含まれるように、utf-8-sigエンコーディングで出力を書き込む必要があります。
IP プレフィックスの分析
-
各エントリに
IPPrefixフィールドが存在し、空でないことを確認します。 -
エントリ間で
IPPrefix値が重複していないか確認します。 -
重複が見つかった場合はスキルを停止し、次のメッセージをユーザーに報告します:
Duplicate IP prefix detected: {ip_prefix_value} appears on lines {line_numbers} -
重複が見つからない場合は分析を続行します。
-
チェック
- 各サブネットが、
references/フォルダー内のコードスニペットを使って IPv4 または IPv6 ネットワークとして正しく解析できる必要があります。 - サブネットは正規化し、CIDR スラッシュ表記で表示する必要があります。
- 単一ホストの IPv4 サブネットは
/32で表す必要があります。 - 単一ホストの IPv6 サブネットは
/128で表す必要があります。
- 単一ホストの IPv4 サブネットは
- 各サブネットが、
-
ERROR
-
次の条件を ERROR として報告します。
-
無効なサブネット構文
- メッセージ ID:
1102
- メッセージ ID:
-
非パブリックアドレス空間
- プライベート、ループバック、リンクローカル、マルチキャスト、またはその他の非パブリックなサブネットに適用されます。
- Python では、
./referencesに示されているように、is_privateおよび関連するアドレスプロパティを使って非パブリック範囲を検出します。
- Python では、
- メッセージ ID:
1103
- プライベート、ループバック、リンクローカル、マルチキャスト、またはその他の非パブリックなサブネットに適用されます。
-
-
SUGGESTION
-
次の条件を SUGGESTION として報告します。
-
IPv6 サブネットが大きすぎる
/64より短いプレフィックス- メッセージ ID:
3102
-
IPv4 サブネットが大きすぎる
/22より短いプレフィックス- メッセージ ID:
3101
-
ジオロケーション品質チェック
ジオロケーションデータの正確性と一貫性を分析します。
- 国コード
- 地域コード
- 市区町村名
- 非推奨フィールド
このフェーズは構造チェックの通過後に実行されます。
国コードの分析
-
チェックには、ローカルで利用可能なデータテーブル
ISO3166-1を使用します。- ISO コードを含む国と地域の JSON 配列
- 各オブジェクトには次の項目が含まれます。
alpha_2:2 文字の国コードname:短い国名flag:国旗の絵文字
- このファイルは、RFC 8805 CSV で有効な
CountryCode値の上位集合を表します。
-
エントリの
CountryCode(RFC 8805 セクション 2.1.1.2、列alpha2code)をalpha_2属性と照合します。 -
サンプルコードは
references/ディレクトリにあります。 -
国が
assets/small-territories.jsonにある場合、そのエントリを内部的に小規模地域としてマークします。このフラグは後続のチェックと提案で使用しますが、出力 JSON には保存されません(一時的な検証状態です)。 -
注:
small-territories.jsonには、iso3166-1.jsonには含まれていない歴史的/係争中のコード(AN、CS、XK)が含まれています。これらをCountryCodeとして使用するエントリは、小規模地域として一致する場合でも、国コード検証で ERROR になります。国コードの ERROR が優先されるため、小規模地域フラグを理由にこのエラーを抑制しないでください。 -
ERROR
- 次の条件を ERROR として報告します。
- 無効な国コード
- 条件:
CountryCodeが存在するものの、alpha_2の集合に見つからない - メッセージ ID:
1201
- 条件:
-
SUGGESTION
-
次の条件を SUGGESTION として報告します。
-
サブネットのジオロケーションが未指定
- 条件:サブネットのすべての地理フィールド(
CountryCode、RegionCode、City)が空である。 - アクション:
- エントリの
DoNotGeolocateをtrueに設定します。 - エントリの
CountryCodeをZZに設定します。
- エントリの
- メッセージ ID:
3104
- 条件:サブネットのすべての地理フィールド(
-
地域コードの分析
-
確認には、ローカルで利用可能なデータテーブル
ISO3166-2を使用します。- ISO が割り当てたコードを持つ、国の行政区画の JSON 配列です
- 各オブジェクトには次の項目が含まれます。
code:国コードを前置した行政区画コード(例:US-CA)name:行政区画の短縮名
- このファイルは、RFC 8805 CSV で有効な
RegionCode値の上位集合を表します。
-
RegionCode値が指定されている場合(RFC 8805 Section 2.1.1.3):- 形式が
{COUNTRY}-{SUBDIVISION}(例:US-CA、AU-NSW)に一致することを確認します。 - 国コードを前置した
code属性と値を照合します。
- 形式が
-
小規模な領域に関する例外: エントリーが小規模な領域であり、かつ
RegionCode値がエントリーのCountryCodeと等しい場合(例:シンガポールの国コードと地域コードの両方にSGを使用)、地域を許容可能として扱い、このエントリーの地域に関する検証をすべて省略します。小規模な領域は実質的に都市国家であり、意味のある ISO 3166-2 の行政区画を持ちません。 -
ERROR
- 次の条件を ERROR として報告します。
- 無効な地域形式
- 条件:
RegionCodeが{COUNTRY}-{SUBDIVISION}に一致せず、かつ小規模な領域に関する例外が適用されない - メッセージ ID:
1301
- 条件:
- 不明な地域コード
- 条件:
RegionCode値がcodeの集合に含まれず、かつ小規模な領域に関する例外が適用されない - メッセージ ID:
1302
- 条件:
- 国と地域の不一致
- 条件:
RegionCodeの国部分がCountryCodeと一致しない - メッセージ ID:
1303
- 条件:
都市名の分析
-
都市名はヒューリスティックなチェックのみで検証します。
-
現時点では、都市名の検証に利用できる権威あるデータセットはありません。
-
ERROR
-
次の条件を ERROR として報告します。
-
プレースホルダーまたは意味のない値
- 条件:次の値を含む、プレースホルダーまたは意味のない値:
undefinedPlease selectnullN/ATBDunknown
- メッセージ ID:
1401
- 条件:次の値を含む、プレースホルダーまたは意味のない値:
-
途中で切れた名前、省略形、または空港コード
- 条件:有効な都市名を表していない、途中で切れた名前、省略形、または空港コード:
LAFrftsin01LHRSINMAA
- メッセージ ID:
1402
- 条件:有効な都市名を表していない、途中で切れた名前、省略形、または空港コード:
-
-
WARNING
- 次の条件を WARNING として報告します。
- 大文字・小文字または書式の不統一
- 条件:データ品質を低下させる可能性のある、都市名の大文字・小文字、スペース、または書式の不統一。例:
HongKongとHong Kong- 大文字・小文字の混在、または想定外の文字体系の使用
- メッセージ ID:
2401
- 条件:データ品質を低下させる可能性のある、都市名の大文字・小文字、スペース、または書式の不統一。例:
郵便番号のチェック
-
RFC 8805 Section 2.1.1.5 では、郵便番号または ZIP コードは明示的に非推奨とされています。
-
郵便番号は非常に小さな人口を表す場合があり、IP アドレス範囲のマッピングには統計的な性質があるため、プライバシー保護上安全とは見なされません。
-
ERROR
- 次の条件を ERROR として報告します。
- 郵便番号が指定されている
- 条件:郵便番号/ZIP コード欄に空でない値が指定されている。
- メッセージ ID:
1501
調整と推奨事項
このフェーズでは、RFC 8805 の範囲を超え、実際のジオフィード導入から得られた精度と使いやすさを高めるための独自の推奨事項を適用します。
- SUGGESTION
-
次の条件を SUGGESTION として報告します。
-
小規模な領域に地域または都市が指定されている
- 条件:
- エントリーが小規模な領域である
RegionCodeが空でない またはCityが空でない。
- メッセージ ID:地域の場合は
3301、都市の場合は3402
- 条件:
-
都市が指定されているが地域コードがない
- 条件:
Cityが空でないRegionCodeが空である- エントリーが小規模な領域ではない
- メッセージ ID:
3303
- 条件:
-
フェーズ 4:調整データの参照
目的
Fastah の rfc8805-row-place-search ツールを使用して、すべての Entries を検索します。
実行ルール
- ペイロードの生成のみを目的とした新しいスクリプトを作成します(データセットを読み込み、1 つ以上のペイロード JSON ファイルを書き出します。このスクリプトから MCP を呼び出してはいけません)。
- サーバーが受け付けるエントリ数は 1 リクエストあたり 1000 件までです。1000 件を超える場合は、複数のリクエストに分割してください。
- エージェントは生成されたペイロードファイルを読み込み、そこからリクエストを構築して、1 バッチあたり最大 1000 件で MCP サーバーに送信する必要があります。
- MCP の失敗時: MCP サーバーに接続できない場合、エラーが返された場合、またはバッチの結果が返されない場合は、警告を記録して Phase 5 に進んでください。影響を受けるすべてのエントリに
TunedEntry: {}を設定してください。レポート生成を妨げてはいけません。次の内容をユーザーにはっきりと通知してください:Tuning data lookup unavailable; the report will show validation results only. - 提案は参考情報のみです。自動入力してはいけません。
ステップ 1:重複排除を行い、検索用ペイロードを作成する
データセットを次の場所から読み込みます:./run/data/report-data.json
Entries配列を読み込みます。各エントリを使用して、MCP 検索用ペイロードを作成します。
同一のエントリを重複排除して、サーバーへのリクエスト数を減らします。
Entriesの各エントリについて、コンテンツハッシュ(CountryCode+RegionCode+Cityのハッシュ)を計算します。- 重複排除マップを作成します:
{ contentHash -> { rowKey, payload, entryIndices: [] } }。rowKey は、レスポンスとの照合のために MCP サーバーへ送信する UUID です。 - エントリのハッシュがすでに存在する場合は、その重複排除エントリの
entryIndices配列に、Entries内での0 始まりの配列インデックスを追加します。 - ハッシュが新しい場合は、**UUID(rowKey)**を生成し、新しい重複排除エントリを作成します。
リクエストバッチを作成します。
- マップから重複排除済みの一意なエントリを、重複排除された順序を保って取り出します。
- 最大 1000 件のリクエストバッチを作成します。
- 各バッチについて、レスポンスを rowKey で照合するために、
[{ rowKey, payload, entryIndices }, ...]のようなインメモリ構造を保持します。 - MCP ペイロードファイルを書き込む際は、各ペイロードオブジェクトに
rowKeyフィールドを含めます。
[
{"rowKey": "550e8400-e29b-41d4-a716-446655440000", "countryCode":"CA","regionCode":"CA-ON","cityName":"Toronto"},
{"rowKey": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "countryCode":"IN","regionCode":"IN-KA","cityName":"Bangalore"},
{"rowKey": "6ba7b811-9dad-11d1-80b4-00c04fd430c8", "countryCode":"IN","regionCode":"IN-KA"}
]
- レスポンスを読み込む際は、各レスポンスの
rowKeyフィールドを対応する重複排除エントリに照合し、関連付けられたすべてのentryIndicesを取得します。
ルール:
- ペイロードの書き込み先:
./run/data/mcp-server-payload.json - ペイロードを書き込んだら、スクリプトを終了します。
ステップ 2:Fastah MCP ツールを呼び出す
- Fastah MCP サーバーの
mcp.json形式の設定例は次のとおりです。
"fastah-ip-geofeed": {
"type": "http",
"url": "https://mcp.fastah.ai/mcp"
}
-
サーバー:
https://mcp.fastah.ai/mcp -
ツールとそのスキーマ:最初の
tools/callの前に、エージェントはtools/listリクエストを送信して、rfc8805-row-place-searchの入力および出力スキーマを読み取る必要があります。 検出したスキーマを、フィールド名、型、制約の正式な情報源として使用してください。 -
以下は参考例にすぎません。必ず
tools/listから返されたスキーマを優先してください。[ {"rowKey": "550e8400-...", "countryCode":"CA", ...}, {"rowKey": "690e9301-...", "countryCode":"ZZ", ...} ] -
Open
./run/data/mcp-server-payload.jsonand send all deduplicated entries with their rowKeys. -
If there are more than 1000 deduplicated entries after deduplication, split into multiple requests of 1000 entries each.
-
The server will respond with the same
rowKeyfield in each response for mapping back. -
Do NOT use local data.
Step 3: Attach Tuned Data to Entries
- Generate a new script for attaching tuned data.
- Load both
./run/data/report-data.jsonand the deduplication map (held in memory from Step 1, or re-derived from the payload file). - For each response from the MCP server:
- Extract the
rowKeyfrom the response. - Look up the
entryIndicesarray associated with thatrowKeyfrom the deduplication map. - For each index in
entryIndices, attach the best match toEntries[index].
- Extract the
- Use the first (best) match from the response when available.
Create the field on each affected entry if it does not exist. Remap the MCP API response keys to Go struct field names:
"TunedEntry": {
"Name": "",
"CountryCode": "",
"RegionCode": "",
"PlaceType": "",
"H3Cells": [],
"BoundingBox": []
}
TunedEntry フィールドは単一のオブジェクト(配列ではありません)です。MCP サーバーからの最適な一致結果を格納します。
MCP API レスポンスキー → JSON キーのマッピング:
| MCP API レスポンスキー | JSON キー |
|---|---|
placeName | Name |
countryCode | CountryCode |
stateCode | RegionCode |
placeType | PlaceType |
h3Cells | H3Cells |
boundingBox | BoundingBox |
UUID が一致しないエントリ(つまり、MCP サーバーから UUID に対するレスポンスが返されなかったエントリ)には、空の TunedEntry: {} オブジェクトを設定してください。このフィールドを省略してはいけません。
- データセットを次の場所に書き戻します:
./run/data/report-data.json - ルール:
- 既存のすべての検証フラグを維持してください。
- 追加の中間ファイルを作成しないでください。
フェーズ 5:チューニングレポートを生成する
./scripts/templates/index.html のテンプレートに、./run/data/report-data.json および ./run/data/comments.json のデータを適用して、自己完結型の HTML レポートを生成します。
完成したレポートを ./run/report/geofeed-report.html に書き込みます。生成後、システムの既定のブラウザーで開くことを試みてください(例:webbrowser.open())。ヘッドレス環境、CI パイプライン、またはブラウザーを利用できないリモートコンテナーで実行している場合は、ブラウザーで開く手順を省略し、ユーザーが開いたりダウンロードしたりできるよう、ファイルパスを提示してください。
テンプレートでは Go の html/template 構文({{.Field}}、{{range}}、{{if eq}} など)が使用されています。テンプレートを読み込み、JSON データファイルからレンダリング用のコンテキストを構築し、テンプレートのプレースホルダーを処理して最終的な HTML を生成する Python スクリプトを作成してください。テンプレートファイル自体は変更せず、すべての処理はレンダリング時に Python スクリプトで行ってください。
ステップ 1:メタデータのプレースホルダーを置き換える
テンプレート内の各 {{.Metadata.X}} プレースホルダーを、report-data.json の対応する値に置き換えます。JSON キーはテンプレートのプレースホルダーと一致するため、対応付けは直接行えます。たとえば、{{.Metadata.InputFile}} は InputFile JSON キーに対応します。
| テンプレートのプレースホルダー | JSON キー(report-data.json) |
|---|---|
{{.Metadata.InputFile}} | InputFile |
{{.Metadata.Timestamp}} | Timestamp |
{{.Metadata.TotalEntries}} | TotalEntries |
{{.Metadata.IpV4Entries}} | IpV4Entries |
{{.Metadata.IpV6Entries}} | IpV6Entries |
{{.Metadata.InvalidEntries}} | InvalidEntries |
{{.Metadata.Errors}} | Errors |
{{.Metadata.Warnings}} | Warnings |
{{.Metadata.Suggestions}} | Suggestions |
{{.Metadata.OK}} | OK |
{{.Metadata.CityLevelAccuracy}} | CityLevelAccuracy |
{{.Metadata.RegionLevelAccuracy}} | RegionLevelAccuracy |
{{.Metadata.CountryLevelAccuracy}} | CountryLevelAccuracy |
{{.Metadata.DoNotGeolocate}} | DoNotGeolocate(メタデータ) |
{{.Metadata.Timestamp}} に関する注意: このプレースホルダーは JavaScript の new Date(...) 呼び出し内にあります。<script> 内の数値リテラルであるため、HTML エスケープせずに整数値をそのまま置き換えてください。その他のメタデータ値は、HTML 要素内のテキストとして表示されるため、HTML エスケープしてください。
ステップ 2:コメントマップのプレースホルダーを置き換える
テンプレート内で次のパターンを探します。
const commentMap = {{.Comments}};
{{.Comments}} を ./run/data/comments.json のシリアライズされた JSON オブジェクトに置き換えます。JSON は JavaScript オブジェクトリテラルとして直接埋め込まれるため(文字列の中ではありません)、追加のエスケープは不要です。
comments_json = json.dumps(comments)
template = template.replace("{{.Comments}}", comments_json)
ステップ 3:Entries の範囲ブロックを展開する
テンプレートには、<tbody id="entriesTableBody"> 内に {{range .Entries}}...{{end}} ブロックが含まれています。次の手順で処理します。
- 正規表現を使って範囲ブロックの本体を抽出します。重要: このブロックには、
{{if eq .Status ...}}、{{if .Checked}}、{{range .Messages}}に由来する入れ子の{{end}}タグが含まれています。\{\{range \.Entries\}\}(.*?)\{\{end\}\}のような単純な非貪欲マッチでは、最初の内側の{{end}}にマッチし、ブロックが途中で切れてしまいます。代わりに、その後に続く</tbody>を基準にして外側の{{end}}を特定します。
これにより、3 つすべてのm = re.search( r'\{\{range \.Entries\}\}(.*?)\{\{end\}\}\s*</tbody>', template, re.DOTALL, ) entry_body = m.group(1) # 1 つのエントリーの反復に使うテンプレートテキスト<tr>行と、入れ子になった{{range .Messages}}...{{end}}を含むブロック本体全体を取得できます。 report-data.jsonのEntries配列内にある各エントリーを反復処理します。- 各エントリーについて、以下の処理順序でブロック本体を展開します。
{{range .Entries}}から</tbody>までのマッチ全体を、展開した HTML を連結したものと、その後に続く</tbody>で置き換えます。
各エントリーの処理順序({{end}} の混同を避けるため、最も内側の構造から処理します):
{{if eq .Status ...}}...{{end}}の条件分岐(ステータスバッジのクラスとアイコン)を評価します。{{if .Checked}}...{{end}}の条件分岐(メッセージのチェックボックス)を評価します。- 内側の
{{range .Messages}}...{{end}}を展開します。 - 単純な
{{.Field}}プレースホルダーを置き換えます。
エントリーフィールドのマッピング
範囲ブロックの本文内で、各エントリーについて次のプレースホルダーを置き換えます。JSON キーはテンプレートのプレースホルダーと一致するため、テンプレートのプレースホルダー {{.X}} は JSON キー X に直接対応します。
| テンプレートのプレースホルダー | JSON キー(Entries[]) | 備考 |
|---|---|---|
{{.Line}} | Line | 整数値をそのまま使用 |
{{.IPPrefix}} | IPPrefix | HTML エスケープ済み |
{{.CountryCode}} | CountryCode | HTML エスケープ済み |
{{.RegionCode}} | RegionCode | HTML エスケープ済み |
{{.City}} | City | HTML エスケープ済み |
{{.Status}} | Status | HTML エスケープ済み |
{{.HasError}} | HasError | 小文字の文字列:"true" または "false" |
{{.HasWarning}} | HasWarning | 小文字の文字列:"true" または "false" |
{{.HasSuggestion}} | HasSuggestion | 小文字の文字列:"true" または "false" |
{{.GeocodingHint}} | GeocodingHint | 空文字列 "" |
{{.DoNotGeolocate}} | DoNotGeolocate | "true" または "false" |
{{.Tunable}} | Tunable | "true" または "false" |
{{.TunedEntry.CountryCode}} | TunedEntry.CountryCode | TunedEntry が空の {} の場合は "" |
{{.TunedEntry.RegionCode}} | TunedEntry.RegionCode | TunedEntry が空の {} の場合は "" |
{{.TunedEntry.Name}} | TunedEntry.Name | TunedEntry が空の {} の場合は "" |
{{.TunedEntry.H3Cells}} | TunedEntry.H3Cells | 角括弧で囲んだスペース区切りの値。空の場合は "[]"(形式は下記参照) |
{{.TunedEntry.BoundingBox}} | TunedEntry.BoundingBox | 角括弧で囲んだスペース区切りの値。空の場合は "[]"(形式は下記参照) |
data-h3-cells と data-bounding-box の形式: これらは JSON 配列ではありません。角括弧で囲んだスペース区切りの値です。JSON シリアライズは使用しないでください(文字列要素を引用符で囲まず、数値の間にカンマを入れません)。例:
[836752fffffffff 836755fffffffff]— 正しい["836752fffffffff","836755fffffffff"]— 誤り。引用符が解析を妨げます[-71.70 10.73 -71.52 10.55]— 正しい[]— 空の場合は正しい
ステータス条件の評価
単純な {{.Field}} プレースホルダーを置き換える前にこれらを処理してください。そうしないと、{{end}} マーカーが消費され、正規表現が一致しなくなります。
テンプレートでは、ステータスバッジの CSS クラスとアイコンに {{if eq .Status "..."}} 条件を使用しています。エントリーの status 値を確認し、一致する分岐のテキストだけを残して評価します。
ステータスバッジの行には、CSS クラス用とアイコン用の {{if eq .Status ...}}...{{end}} ブロックが 2 つ、1 行に含まれています。コールバックを使って re.sub ですべての出現箇所を解決してください。
STATUS_CSS = {"ERROR": "error", "WARNING": "warning", "SUGGESTION": "suggestion", "OK": "ok"}
STATUS_ICON = {
"ERROR": "bi-x-circle-fill",
"WARNING": "bi-exclamation-triangle-fill",
"SUGGESTION": "bi-lightbulb-fill",
"OK": "bi-check-circle-fill",
}
def resolve_status_if(match_obj, status):
"""Pick the branch matching `status` from a {{if eq .Status ...}}...{{end}} block."""
block = match_obj.group(0)
# Try each branch: {{if eq .Status "X"}}val{{else if ...}}val{{else}}val{{end}}
for st, val in [("ERROR",), ("WARNING",), ("SUGGESTION",)]:
# not needed to parse generically — just map from the known patterns
...
より簡単な方法として、既知のパターンが正確に 2 つだけなので、リテラル文字列として置き換える方法があります。
css_class = STATUS_CSS.get(status, "ok")
icon_class = STATUS_ICON.get(status, "bi-check-circle-fill")
body = body.replace(
'{{if eq .Status "ERROR"}}error{{else if eq .Status "WARNING"}}warning{{else if eq .Status "SUGGESTION"}}suggestion{{else}}ok{{end}}',
css_class,
)
body = body.replace(
'{{if eq .Status "ERROR"}}bi-x-circle-fill{{else if eq .Status "WARNING"}}bi-exclamation-triangle-fill{{else if eq .Status "SUGGESTION"}}bi-lightbulb-fill{{else}}bi-check-circle-fill{{end}}',
icon_class,
)
テンプレート内にこれらの完全一致する文字列がそのまま含まれているため、この方法では正規表現を使わず、安全に処理できます。
ステップ 4:ネストされたメッセージ範囲を展開する
{{range .Messages}}...{{end}} ブロックには、ネストされた {{if .Checked}} checked{{else}} disabled{{end}} 条件式が含まれているため、内側の {{end}} によって単純な非貪欲正規表現では早くマッチしすぎます。メッセージ範囲の閉じ {{end}} の直後にあるタグ </td> を正規表現のアンカーにして、ブロック本体全体をキャプチャします。
msg_match = re.search(
r'\{\{range \.Messages\}\}(.*?)\{\{end\}\}\s*(?=</td>)',
body, re.DOTALL
)
先読み (?=</td>) によって、チェックボックス条件式の {{end}}(後ろに > が続き、</td> は続きません)をスキップし、範囲を閉じる {{end}}(後ろに空白と </td> が続きます)にのみマッチします。
エントリーの Messages 配列内の各メッセージについて、キャプチャしたブロック本体を複製して展開します。
-
メッセージごとにチェックボックス条件式を解決します(ネストされた
{{end}}を削除するため、単純なプレースホルダー置換より先に行う必要があります)。if msg.get("Checked"): msg_body = msg_body.replace( '{{if .Checked}} checked{{else}} disabled{{end}}', ' checked' ) else: msg_body = msg_body.replace( '{{if .Checked}} checked{{else}} disabled{{end}}', ' disabled' ) -
メッセージフィールドのプレースホルダーを置換します。
テンプレートのプレースホルダー ソース 備考 {{.ID}}Messages[i].IDJSON からの直接の文字列値 {{.Text}}Messages[i].TextHTML エスケープ済み -
展開したすべてのメッセージブロックを連結し、元の
{{range .Messages}}...{{end}}マッチ(msg_match.group(0))をその結果で置換します。body = body[:msg_match.start()] + "".join(expanded_msgs) + body[msg_match.end():]
Messages が空の場合は、マッチした領域全体を空文字列に置換します(メッセージの div は出力されず、問題のヘッダーだけが残ります)。
出力の保証
- レポートは、テンプレート内にすでにある CDN リンク(
leaflet、h3-js、bootstrap-icons、Raleway フォント)以外のネットワーク依存関係なしで、最新のブラウザーで読み取れる必要があります。 - HTML に埋め込むすべての値は、表示上の問題を防ぐため HTML エスケープ(
<、>、&、")する必要があります。 commentMapは文字列の中ではなく、JavaScript のオブジェクトリテラルとして直接埋め込まれるため、JavaScript の文字列エスケープは不要です。有効な JSON を出力するだけで構いません。- すべての値は、ヒューリスティックに再計算せず、分析結果のみから導出する必要があります。
フェーズ 6:最終確認
結果をユーザーに提示する前に、具体的で検証可能なアサーションを使って最終確認を行います。
確認 1 — エントリー数の整合性
- 元の入力 CSV に含まれる、コメントでも空行でもないデータ行の数を数えます。
- アサーション:
len(entries) in report-data.json == data_row_count - 失敗した場合:
Row count mismatch: input has {N} data rows but report contains {M} entries.
確認 2 — サマリーカウンターの整合性
- これらのカウンターでは、ブール値フラグに基づく相互排他を使用します。これは、最も重大度の高い
Statusフィールドに対応します。HasError: trueとHasWarning: trueの両方に該当するエントリーはErrorsにのみカウントし、Warningsにはカウントしません。これは、エントリーのStatusフィールドでカウントするのと同じです。 - 以下をすべてアサーションし、失敗したものはレポートを生成する前に修正します。
Errors == sum(1 for e in Entries if e['HasError'])Warnings == sum(1 for e in Entries if e['HasWarning'] and not e['HasError'])Suggestions == sum(1 for e in Entries if e['HasSuggestion'] and not e['HasError'] and not e['HasWarning'])OK == sum(1 for e in Entries if not e['HasError'] and not e['HasWarning'] and not e['HasSuggestion'])Errors + Warnings + Suggestions + OK == TotalEntries - InvalidEntries
確認 3 — 精度分類の整合性
- アサーション:
CityLevelAccuracy + RegionLevelAccuracy + CountryLevelAccuracy + DoNotGeolocate == TotalEntries - InvalidEntries - 注: フェーズ 3 で定義されている精度分類では「
HasError: trueのエントリーはカウントしない」とされていますが、上記の確認 3 の式ではTotalEntries - InvalidEntriesを使用しています(これには ERROR エントリーが含まれます)。つまり、ERROR エントリー(有効な IP として解析されたものの、検証に失敗したエントリー)は、地理情報フィールドの有無に基づいて精度分類にカウントされます。除外されるのはInvalidEntries(解析できない IP プレフィックス)のみです。正式なルールとして、確認 3 の式に従ってください。 - 失敗した場合は、処理を続行する前に分類ロジックを追跡して修正します。
確認 4 — 行番号の重複なし
- アサーション:
Entries内のすべてのLine値が一意であること。 - 失敗した場合は、重複している行番号をユーザーに報告します。
確認 5 — TunedEntry の完全性
- アサーション:
Entries内のすべてのオブジェクトにTunedEntryキーがあること(値が{}の場合も含みます)。 - 失敗した場合は、キーがないエントリーに
"TunedEntry": {}を追加してから、report-data.jsonを再保存します。
確認 6 — レポートファイルの存在とサイズ
./run/report/geofeed-report.htmlが書き込まれており、ファイルサイズが 0 バイトを超えていることを確認します。- 失敗した場合は、ユーザーに提示する前にレポートを再生成します。