mcpField
G
geofeed-tuner

geofeed-tuner

github.com/githubクラウド・DevOps40.5k1.3k 回インストール今日更新
リポジトリ

geofeed-tuner について

geofeed-tunerは、グローバルにルーティング可能なIPアドレス空間の位置情報を示すCSVフィードを作成・改善するスキルです。RFC 8805の形式とデータ品質を確認し、FastahのMCPツールで調整用データを取得してHTMLレポートを生成します。

主な機能

  1. 01RFC 8805に沿ってCSV構造を確認
  2. 02公開IPプレフィックスを検証
  3. 03データ品質とプライバシー上の問題を検出
  4. 04Fastahの検索データを使って調整
  5. 05HTML分析レポートを生成

活用シーン

  1. 01公開用の位置情報フィードを準備
  2. 02既存フィードの形式エラーを確認
  3. 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.json
iso3166-2.json
small-territories.json
assets/example/
01-user-input-rfc8805-feed.csv
references/
rfc8805.txt
snippets-python3.md
scripts/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 調整レポート
ファイル管理ルール
  1. **assets/、references/、scripts/ には書き込まないでください。**これらはスキルの配布物の一部であり、変更せずに保つ必要があります。
  2. リモート URL からダウンロードした入力ファイルはすべて、./run/data/ に保存してください。
  3. 生成された HTML レポートはすべて、./run/report/ に保存してください。
  4. 生成された Python スクリプトはすべて、./run/ に保存してください。
  5. run/ ディレクトリはセッション間で消去される場合があります。永続データを保存しないでください。
  6. 実行時の作業ディレクトリ: ./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):

列フィールド必須備考
1ip_prefixはいCIDR 表記。IPv4 または IPv6。ネットワークアドレスである必要があります
2alpha2codeいいえISO 3166-1 alpha-2 の国コード。空欄または「ZZ」はジオロケーションを適用しないことを示します
3regionいいえISO 3166-2 の下位区分コード(例:US-CA)
4cityいいえ自由記述の市区町村名。権威ある検証用データセットはありません
5postal_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 に進まないでください。
  • 入力がローカルファイルの場合は、ダウンロードせずにそのまま処理してください。

  • エンコーディングの検出と正規化:

    1. まず、ファイルを UTF-8 として読み込んでみてください。
    2. UnicodeDecodeError が発生した場合は、utf-8-sig(BOM 付き UTF-8)、次に latin-1 を試してください。
    3. 正常にデコードできたら、再エンコードして、作業用コピーを UTF-8 で書き込んでください。
    4. どのエンコーディングでも読み込めない場合は中止し、次の内容を報告してください: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 の値を使用します。

IDTypeTextCheckedCondition Reference
1101ERRORIP プレフィックスが空ですfalseIP Prefix Analysis: empty
1102ERROR無効な IP プレフィックスです。IPv4 または IPv6 ネットワークとして解析できませんfalseIP Prefix Analysis: invalid syntax
1103ERRORRFC 8805 フィードでは、パブリックではない IP 範囲は許可されませんfalseIP Prefix Analysis: non-public
3101SUGGESTIONIPv4 プレフィックスが通常より大きく、タイプミスの可能性がありますfalseIP Prefix Analysis: IPv4 < /22
3102SUGGESTIONIPv6 プレフィックスが通常より大きく、タイプミスの可能性がありますfalseIP Prefix Analysis: IPv6 < /64
1201ERROR無効な国コードです。有効な ISO 3166-1 alpha-2 の値ではありませんtrueCountry Code Analysis: invalid
1301ERROR無効な地域形式です。COUNTRY-SUBDIVISION(例:US-CA)が必要ですtrueRegion Code Analysis: bad format
1302ERROR無効な地域コードです。有効な ISO 3166-2 の区分ではありませんtrueRegion Code Analysis: unknown code
1303ERROR地域コードが指定された国コードと一致しませんtrueRegion Code Analysis: mismatch
1401ERROR無効な都市名です。プレースホルダー値は使用できませんfalseCity Name Analysis: placeholder
1402ERROR無効な都市名です。省略形またはコード形式の値が検出されましたtrueCity Name Analysis: abbreviation
2401WARNING都市名の書式が一貫していません。値の正規化を検討してくださいtrueCity Name Analysis: formatting
1501ERRORRFC 8805 では郵便番号が非推奨であり、プライバシー上の理由から削除する必要がありますtruePostal Code Check
3301SUGGESTION小規模な地域では通常、地域情報は不要です。地域の値を削除することを検討してくださいtrueTuning: small territory region
3402SUGGESTION小規模な地域では通常、市区町村レベルの詳細情報は不要です。市区町村の値を削除することを検討してくださいtrueTuning: small territory city
3303SUGGESTION都市を指定する場合は地域コードが推奨されます。ドロップダウンから地域を選択してくださいtrueTuning: missing region with city
3104SUGGESTIONこのサブネットが意図的にジオロケーション対象外として指定されているのか、位置情報が欠落しているのかを確認してくださいtrueTuning: 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(エントリ)が trueDoNotGeolocate(メタデータ)

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" }
  • 注意

    • 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 で表す必要があります。
  • ERROR

    • 次の条件を ERROR として報告します。

    • 無効なサブネット構文

      • メッセージ ID:1102
    • 非パブリックアドレス空間

      • プライベート、ループバック、リンクローカル、マルチキャスト、またはその他の非パブリックなサブネットに適用されます。
        • Python では、./references に示されているように、is_private および関連するアドレスプロパティを使って非パブリック範囲を検出します。
      • メッセージ 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 として報告します。

    • プレースホルダーまたは意味のない値

      • 条件:次の値を含む、プレースホルダーまたは意味のない値:
        • undefined
        • Please select
        • null
        • N/A
        • TBD
        • unknown
      • メッセージ ID:1401
    • 途中で切れた名前、省略形、または空港コード

      • 条件:有効な都市名を表していない、途中で切れた名前、省略形、または空港コード:
        • LA
        • Frft
        • sin01
        • LHR
        • SIN
        • MAA
      • メッセージ 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.json and 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 rowKey field 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.json and 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 rowKey from the response.
    • Look up the entryIndices array associated with that rowKey from the deduplication map.
    • For each index in entryIndices, attach the best match to Entries[index].
  • 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 キー
placeNameName
countryCodeCountryCode
stateCodeRegionCode
placeTypePlaceType
h3CellsH3Cells
boundingBoxBoundingBox

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}} ブロックが含まれています。次の手順で処理します。

  1. 正規表現を使って範囲ブロックの本体を抽出します。重要: このブロックには、{{if eq .Status ...}}、{{if .Checked}}、{{range .Messages}} に由来する入れ子の {{end}} タグが含まれています。 \{\{range \.Entries\}\}(.*?)\{\{end\}\} のような単純な非貪欲マッチでは、最初の内側の {{end}} にマッチし、ブロックが途中で切れてしまいます。代わりに、その後に続く </tbody> を基準にして外側の {{end}} を特定します。
    m = re.search(
        r'\{\{range \.Entries\}\}(.*?)\{\{end\}\}\s*</tbody>',
        template,
        re.DOTALL,
    )
    entry_body = m.group(1)  # 1 つのエントリーの反復に使うテンプレートテキスト
    
    これにより、3 つすべての <tr> 行と、入れ子になった {{range .Messages}}...{{end}} を含むブロック本体全体を取得できます。
  2. report-data.json の Entries 配列内にある各エントリーを反復処理します。
  3. 各エントリーについて、以下の処理順序でブロック本体を展開します。
  4. {{range .Entries}} から </tbody> までのマッチ全体を、展開した HTML を連結したものと、その後に続く </tbody> で置き換えます。

各エントリーの処理順序({{end}} の混同を避けるため、最も内側の構造から処理します):

  1. {{if eq .Status ...}}...{{end}} の条件分岐(ステータスバッジのクラスとアイコン)を評価します。
  2. {{if .Checked}}...{{end}} の条件分岐(メッセージのチェックボックス)を評価します。
  3. 内側の {{range .Messages}}...{{end}} を展開します。
  4. 単純な {{.Field}} プレースホルダーを置き換えます。
エントリーフィールドのマッピング

範囲ブロックの本文内で、各エントリーについて次のプレースホルダーを置き換えます。JSON キーはテンプレートのプレースホルダーと一致するため、テンプレートのプレースホルダー {{.X}} は JSON キー X に直接対応します。

テンプレートのプレースホルダーJSON キー(Entries[])備考
{{.Line}}Line整数値をそのまま使用
{{.IPPrefix}}IPPrefixHTML エスケープ済み
{{.CountryCode}}CountryCodeHTML エスケープ済み
{{.RegionCode}}RegionCodeHTML エスケープ済み
{{.City}}CityHTML エスケープ済み
{{.Status}}StatusHTML エスケープ済み
{{.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.CountryCodeTunedEntry が空の {} の場合は ""
{{.TunedEntry.RegionCode}}TunedEntry.RegionCodeTunedEntry が空の {} の場合は ""
{{.TunedEntry.Name}}TunedEntry.NameTunedEntry が空の {} の場合は ""
{{.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 配列内の各メッセージについて、キャプチャしたブロック本体を複製して展開します。

  1. メッセージごとにチェックボックス条件式を解決します(ネストされた {{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'
        )
    
  2. メッセージフィールドのプレースホルダーを置換します。

    テンプレートのプレースホルダーソース備考
    {{.ID}}Messages[i].IDJSON からの直接の文字列値
    {{.Text}}Messages[i].TextHTML エスケープ済み
  3. 展開したすべてのメッセージブロックを連結し、元の {{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 バイトを超えていることを確認します。
  • 失敗した場合は、ユーザーに提示する前にレポートを再生成します。

コメント