AIタスク #10469
未完了エピック #10493: [WEBサイト自動販売機] 操作可能MVP:ヒアリングからサイト生成・プレビューまで
LLM対策診断ツール実装(website-vending-machine 診断コンテンツ機能)
100%
## 受入基準(検収条件)
server.js/crawlability.js/scoring.js/db.js/Dockerfile/package.jsonが仕様通りに実装されていること。safeFetchが本文fetch/robots.txt/llms.txtの全てを必ず経由していること。異常系が全て200+error_reasonで返っていること(500にならない)。既存/sessions系の動作を壊さないこと。total_scoreが上記重み式(direct_answer0.30+authority0.25+original_info0.20+schema0.10+freshness0.10+llms_txt0.05)で算出されていること。diagnosis_resultsにscore_llms_txt列が存在すること。rate_limit_ipテーブルが存在し、24時間ウィンドウでリセットされ、attempt_count>=3でGeminiに切り替わること。diagnosis_resultsにllm_provider列が存在し、成功時に'claude'または'gemini'が記録されること。Gemini容量制限時のerror_reasonが`llm:gemini_capacity`で固定されていること。
説明
LLM対策診断ツール実装 詳細仕様¶
背景¶
集客ページとして、URL入力→AI検索(LLM)に引用されやすいかを自動診断するツールをvend01(/root/website-vending-machine/vend01)に追加する。
スコアリング方針¶
ゲート判定(クロール可否)→本体スコア(重み付け加重平均)の2段階。重みはAIクローラー(GPTBot/PerplexityBot/ClaudeBot/Google-Extended)の実挙動とGEO研究ベースの確度で設定(表示速度・UXは除外、llms.txtは重み最小)。
ファイル構成(既存vend01の少ファイル主義を尊重、2ファイルに限定)¶
- crawlability.js: safeFetch(url)単一実装(本文fetchとrobots.txt取得の両方が必ずこれを経由)、gate_crawlable/gate_robots_blocked判定
- scoring.js: スコア集計(機械的判定+LLM呼び出し)
SSRF対策(safeFetch必須仕様)¶
- スキームはhttp/httpsのみ許可
- redirect: 'manual'を指定、3xx応答を自前でハンドルしLocationを再度safeFetchに通して各ホップごとにIP検証(最大リダイレクト3回)
- DNS rebinding対策: dns.lookup()で名前解決したIPを検証後、undiciのカスタムdispatcherでconnect先IPを固定し同一接続内で再解決させない(ただしTLSのSNI/証明書検証は元ホスト名ベースを使うこと)
- プライベートレンジ拒否(IPv4): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, 0.0.0.0/8, 100.64.0.0/10
- プライベートレンジ拒否(IPv6): ::1, fe80::/10, fc00::/7, ::ffff:127.0.0.1等
- proxy-network内コンテナ名解決も拒否、AbortSignal.timeout(8000)をsafeFetch内に内蔵
判定方式の分離¶
機械的判定: gate_crawlable, gate_robots_blocked, score_schema(cheerioでJSON-LD解析), score_freshness(sitemap lastmod), score_authority(外部リンクユニークドメイン数、nofollow除外)
LLM判定: 抽出済み本文をLLMに渡しscore_direct_answerとscore_original_infoを1回の呼び出しでJSON一括出力
LLM接続先(重要)¶
既存vend01のcallClaude(Anthropic API直呼び)は使わず、VPS-root内部APIを使用:
- 通常時: http://mcp-gateway-v2:8000/v1/chat/completions (proxy-network内、OpenAI Chat Completions互換)。modelに
claude-3-sonnetを渡すと内部でsonnetエイリアスにマッピングされ常に最新Sonnetに自動追従。認証はX-API-Keyヘッダ(.envのCLAUDE_API_KEYS)。注意: 内部でClaude Code CLIをサブプロセス起動する重い処理でレイテンシ数秒〜。messagesはフラット化されるため単発呼び出し前提の実装にすること。 - 同一IPから3回以上のトライがあった場合: Gemini APIの無料枠に切り替える(IP別試行回数カウントが必要)。Gemini無料枠の容量制限に到達した場合はエラーメッセージとして「システムが混雑しています。日を改めてご利用ください」を表示する。
- error_reasonはgate:/llm:の接頭辞で区別し、llm:claude / llm:geminiのようにどちらで判定したかも記録する。
LLM失敗フォールバック¶
タイムアウト/例外/JSONパース失敗はすべてfetch失敗と同じパターン(total_score=null, error_reason記録, エンドポイントは200)に合流させる。
DBスキーマ(既存単一DBファイルにdb.js内でCREATE TABLE IF NOT EXISTSで追記、別ファイルにしない)¶
CREATE TABLE IF NOT EXISTS diagnosis_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
gate_crawlable INTEGER,
gate_robots_blocked INTEGER,
score_direct_answer REAL,
score_authority REAL,
score_original_info REAL,
score_schema REAL,
score_freshness REAL,
total_score REAL,
error_reason TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
UNIQUE制約なしで同一URLの再診断は毎回新規行として履歴保持。raw_html本文はDB保存しない。
APIエンドポイント¶
- POST /api/diagnosis body:{url} → {gate_crawlable, gate_robots_blocked, breakdown:{...}, total_score, error_reason}
- GET /api/diagnosis/:id → 過去診断結果の再取得
- 既存/sessions系と同じプレフィックスなし規約(Nginx側で/vend/01/api/を付与する前提)
Dockerfile/package.jsonの明示変更¶
- Dockerfile COPY行に crawlability.js, scoring.js を明示追記
- package.jsonに cheerio を追加(robots.txtパースは自前実装、依存追加不要)
受入基準(検収条件)¶
server.js/crawlability.js/scoring.js/db.js/Dockerfile/package.jsonが仕様通りに実装されていること。safeFetchが本文fetch/robots.txt/llms.txtの全てを必ず経由していること。異常系が全て200+error_reasonで返っていること(500にならない)。既存/sessions系の動作を壊さないこと。total_scoreが上記重み式(direct_answer0.30+authority0.25+original_info0.20+schema0.10+freshness0.10+llms_txt0.05)で算出されていること。diagnosis_resultsにscore_llms_txt列が存在すること。rate_limit_ipテーブルが存在し、24時間ウィンドウでリセットされ、attempt_count>=3でGeminiに切り替わること。diagnosis_resultsにllm_provider列が存在し、成功時に'claude'または'gemini'が記録されること。Gemini容量制限時のerror_reasonがllm:gemini_capacityで固定されていること。
レビュー反映(2回目)¶
- rate_limit_ipテーブル新規: CREATE TABLE IF NOT EXISTS rate_limit_ip (ip TEXT PRIMARY KEY, attempt_count INTEGER NOT NULL DEFAULT 0, window_start TEXT NOT NULL);
- リセットは日次(24時間)ウィンドウ。window_startから24h経過でattempt_count=1・window_startリセット。未経過ならインクリメント。attempt_count>3でGemini切替。
- diagnosis_resultsにllm_provider TEXT列追加('claude'/'gemini')。
- Gemini容量制限時のerror_reasonは
llm:gemini_capacityで固定。 - Gemini APIキーは.envのGEMINI_API_KEY。
レビュー反映(3回目)¶
- 重み確定: total_score = direct_answer0.30 + authority0.25 + original_info0.20 + schema0.10 + freshness0.10 + llms_txt0.05(各score_*は0〜100、合計重み100)。
- llms_txt実装: crawlability.jsのsafeFetchで/llms.txtを取得、2xxで取得できればscore_llms_txt=100、それ以外(404含む)は0。diagnosis_resultsテーブルにscore_llms_txt REAL列を追加。
- Gemini切替え閾値を統一: attempt_count>=3でGemini切替え(本文の「3回以上」に合わせ、以前の「attempt_count>3」記述を上書き訂正)。
スコープ確定宣言(これ以上の仕様化は行わない)¶
以下は確定事項(必須実装): total_score重み(direct_answer0.30/authority0.25/original_info0.20/schema0.10/freshness0.10/llms_txt0.05)、diagnosis_resultsのscore_llms_txt列とllm_provider列、rate_limit_ipテーブル(attempt_count>=3でGemini切替え、24時間ウィンドウ)、LLM呼び出し失敗時のtotal_score=null+error_reason+200応答。DBスキーマのCREATE TABLE本体ブロックにこれら全列を統合して実装すること(追記別ブロックではなく本体の1つのCREATE TABLE文にまとめる)。
以下はCodexの裁量に委ねる(レビューで大きく外れていなければ差し戻さないこと): score_authority/score_freshness/score_schemaの0〜100正規化の具体的な閾値・分割点、および機械判定系(freshness/schema/authority)の取得失敗時の細部挿作は実装者(Codex)自身の合理的な判断に委ねる(根拠はコードコメントに残す)。異常系は必ず200+error_reasonで返すという大方針のみ遵守すればよい。
Redmine Admin さんが14日前に更新
LLM対策診断ツール実装 詳細仕様¶
背景¶
集客ページとして、URL入力→AI検索(LLM)に引用されやすいかを自動診断するツールをvend01(/root/website-vending-machine/vend01)に追加する。
スコアリング方針¶
ゲート判定(クロール可否)→本体スコア(重み付け加重平均)の2段階。重みはAIクローラー(GPTBot/PerplexityBot/ClaudeBot/Google-Extended)の実挙動とGEO研究ベースの確度で設定(表示速度・UXは除外、llms.txtは重み最小)。
ファイル構成(既存vend01の少ファイル主義を尊重、2ファイルに限定)¶
- crawlability.js: safeFetch(url)単一実装(本文fetchとrobots.txt取得の両方が必ずこれを経由)、gate_crawlable/gate_robots_blocked判定
- scoring.js: スコア集計(機械的判定+LLM呼び出し)
SSRF対策(safeFetch必須仕様)¶
- スキームはhttp/httpsのみ許可
- redirect: 'manual'を指定、3xx応答を自前でハンドルしLocationを再度safeFetchに通して各ホップごとにIP検証(最大リダイレクト3回)
- DNS rebinding対策: dns.lookup()で名前解決したIPを検証後、undiciのカスタムdispatcherでconnect先IPを固定し同一接続内で再解決させない(ただしTLSのSNI/証明書検証は元ホスト名ベースを使うこと)
- プライベートレンジ拒否(IPv4): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, 0.0.0.0/8, 100.64.0.0/10
- プライベートレンジ拒否(IPv6): ::1, fe80::/10, fc00::/7, ::ffff:127.0.0.1等
- proxy-network内コンテナ名解決も拒否、AbortSignal.timeout(8000)をsafeFetch内に内蔵
判定方式の分離¶
機械的判定: gate_crawlable, gate_robots_blocked, score_schema(cheerioでJSON-LD解析), score_freshness(sitemap lastmod), score_authority(外部リンクユニークドメイン数、nofollow除外)
LLM判定: 抽出済み本文をLLMに渡しscore_direct_answerとscore_original_infoを1回の呼び出しでJSON一括出力
LLM接続先(重要)¶
既存vend01のcallClaude(Anthropic API直呼び)は使わず、VPS-root内部APIを使用:
- 通常時: http://mcp-gateway-v2:8000/v1/chat/completions (proxy-network内、OpenAI Chat Completions互換)。modelに
claude-3-sonnetを渡すと内部でsonnetエイリアスにマッピングされ常に最新Sonnetに自動追従。認証はX-API-Keyヘッダ(.envのCLAUDE_API_KEYS)。注意: 内部でClaude Code CLIをサブプロセス起動する重い処理でレイテンシ数秒〜。messagesはフラット化されるため単発呼び出し前提の実装にすること。 - 同一IPから3回以上のトライがあった場合: Gemini APIの無料枠に切り替える(IP別試行回数カウントが必要)。Gemini無料枠の容量制限に到達した場合はエラーメッセージとして「システムが混雑しています。日を改めてご利用ください」を表示する。
- error_reasonはgate:/llm:の接頭辞で区別し、llm:claude / llm:geminiのようにどちらで判定したかも記録する。
LLM失敗フォールバック¶
タイムアウト/例外/JSONパース失敗はすべてfetch失敗と同じパターン(total_score=null, error_reason記録, エンドポイントは200)に合流させる。
DBスキーマ(既存単一DBファイルにdb.js内でCREATE TABLE IF NOT EXISTSで追記、別ファイルにしない)¶
CREATE TABLE IF NOT EXISTS diagnosis_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
gate_crawlable INTEGER,
gate_robots_blocked INTEGER,
score_direct_answer REAL,
score_authority REAL,
score_original_info REAL,
score_schema REAL,
score_freshness REAL,
total_score REAL,
error_reason TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
UNIQUE制約なしで同一URLの再診断は毎回新規行として履歴保持。raw_html本文はDB保存しない。
APIエンドポイント¶
- POST /api/diagnosis body:{url} → {gate_crawlable, gate_robots_blocked, breakdown:{...}, total_score, error_reason}
- GET /api/diagnosis/:id → 過去診断結果の再取得
- 既存/sessions系と同じプレフィックスなし規約(Nginx側で/vend/01/api/を付与する前提)
Dockerfile/package.jsonの明示変更¶
- Dockerfile COPY行に crawlability.js, scoring.js を明示追記
- package.jsonに cheerio を追加(robots.txtパースは自前実装、依存追加不要)
受入基準(検収条件)¶
- server.js/crawlability.js/scoring.js/db.js/Dockerfile/package.jsonが上記仕様通りに実装されていること
- safeFetchが本文fetchとrobots.txt取得の両方を必ず経由していること
- 異常系(fetch失敗/タイムアウト/LLM失敗)がすべて200+error_reasonで返っていること(500にならない)
- 既存/sessions系エンドポイントの動作を壊さないこと
- 同一IP3回以上でGemini無料枠へのフォールバック、容量制限到達時のエラーメッセージ表示が実装されていること
Redmine Admin さんが14日前に更新
[クロスレビュー] レビュアー種別: claude
VERDICT: needs_revision
クロスレビュー結果(#10469)¶
観点1: 起票意図に対して正しく答えているか
チケット本文は「詳細仕様は#10442のコメントで後続入力予定、先に骨格で起票」としているが、この計画はorchestrateの実行機構と噛み合っていない。
- submit_issue/Workerが参照するのはこのチケット(#10469)自身のdescriptionであり、別チケット#10442のコメントに書いた内容は自動的にはこちらへ反映されない。「#10442のコメントに書けば後で拾われる」という前提自体が誤解であり、誰が・いつ・どうやって#10442の内容を#10469の本文またはcf16に転記するのかがチケット内に一切書かれていない。
- #10442と#10469の依存関係(先に#10442で仕様確定 → #10469が着手可能)を系で表現するなら、precedes/follows + chain_root_id(cf20)で連鎖を組むのが本来の作法(3章)だが、この関連付けが行われていない。フリーテキストでの言及だけでは自動実行パイプラインはこの依存を認識できない。
観点2: 回答漏れ・誤解
- 件名が空。記録としても実行トリガーとしても、何のチケットか一見して判別できない。
- 自動実行に乗せる必須条件(1章)のうち、担当者(bot_user_ids割当)が起票内容に見当たらず、cf3(自動実行フラグ)もNoneのまま。骨格起票であっても「このチケットは今回自動実行に乗せない(記録目的)」のか「#10442の詳細が揃ったら自動実行に乗せる予定」なのかが本文から読み取れず、担当者・レビュアーとも次に何をすべきか判断できない。
- 対象プロジェクト(vend01 / website-vending-machine)が#9980のtarget_projects・workdir_by_projectに登録済みかどうか本文からは確認できない。骨格起票の段階でも、後続の自動実行を意図するなら最低限の対象確認は必要。
上記は「後で詳細を書く」こと自体を問題視しているのではなく、その詳細をどうやってこのチケットの実行内容に反映させるかの導線が起票時点で欠落している点が本質的な不備。
VERDICT: needs_revision
Redmine Admin さんが14日前に更新
このチケットの受入基準はコード実装(server.js/crawlability.js/scoring.js/db.js/Dockerfile/package.json)のみ。docker compose build/upによる本番反映は含まず、別チケット(human_gate)で実施する
Redmine Admin さんが14日前に更新
- 次のチケットに先行 AIタスク #10470: LLM対策診断ツール 本番反映(docker compose build/up) を追加
Redmine Admin さんが14日前に更新
[クロスレビュー] レビュアー種別: claude
VERDICT: needs_revision
クロスレビュー結果(チケット #10469)¶
観点1: 起票意図に対する回答の正確性¶
実装対象ファイル・SSRF対策・スコアリング方式・LLM接続先・フォールバック・DBスキーマ・APIエンドポイント・受入基準まで、実装者が迷わず着手できる粒度で詳細に書かれており、意図に対する回答としては概ね妥当。ただし以下の点で回答が閉じきっていない箇所がある。
観点2: 回答漏れ・誤解の指摘¶
-
IP別試行回数カウンタの永続化設計が欠落
「同一IPから3回以上のトライがあった場合…(IP別試行回数カウントが必要)」と要件自体は明記されているが、その保持先が規定されていない。DBスキーマ節は「既存単一DBファイルにdb.js内でCREATE TABLE IF NOT EXISTSで追記、別ファイルにしない」と唯一の永続化手段を明示しているにもかかわらず、diagnosis_resultsテーブルのみが定義され、IP試行回数を保持するテーブル/カラムが存在しない。インメモリ保持で良いのか(その場合コンテナ再起動やマルチインスタンスで回数がリセットされてよいのか)、DB永続化が必要なのかが未確定のまま。この機能は有料LLM API(Claude)のコスト保護を目的とした無料枠切替ロジックの核心であり、受入基準にも「同一IP3回以上でGemini無料枠へのフォールバック…が実装されていること」と検収項目に挙がっているため、判定不能な曖昧さが残る。 -
カウントのリセット条件が未規定
「3回以上」がURL単位か、セッション単位か、日次リセットか、恒久カウントかが書かれていない。1と合わせて、実装者が任意に決めることになり、検収基準としても検証しづらい。 -
成功時にどちらのLLM(Claude/Gemini)が回答したかを記録する手段がない
error_reasonのllm:claude/llm:gemini接頭辞による記録は失敗時のみ言及されている。正常応答時にどちらのプロバイダが実際にscore_direct_answer/score_original_infoを生成したかを記録するカラムがdiagnosis_resultsに存在せず、フォールバックの発生状況を後から追跡する手段が仕様上ない。 -
Gemini容量制限到達時のエラーメッセージとerror_reasonの対応関係が不明
「エラーメッセージとして『システムが混雑しています。日を改めてご利用ください』を表示する」とあるが、これがAPI応答のerror_reasonフィールドにそのまま格納される文字列なのか、それともフロントエンドがある特定のerror_reasonコード(例:llm:gemini_capacity等)を検知して表示文言に変換する設計なのかが規定されていない。他の失敗パターンでは「error_reasonはgate:/llm:の接頭辞で区別」とルールがあるため、この容量制限ケースがどちらの命名規則に従うか読み取れない。 -
Gemini APIキーの格納先が未記載
Claude接続については「認証はX-API-Keyヘッダ(.envのCLAUDE_API_KEYS)」と明記されているが、Geminiフォールバック用のAPIキーをどの環境変数名で.envに持たせるかの記載がない。
上記1〜3は受入基準に直結する機能(IPベースのフォールバック運用)の実装可否・検証可否に関わる回答漏れであり、着手前に確定させるべき事項と判断する。
VERDICT: needs_revision
Redmine Admin さんが14日前に更新
[クロスレビュー] レビュアー種別: claude
VERDICT: needs_revision
レビュー結果を以下にまとめます(修正案は提示せず、指摘のみ)。
指摘事項¶
1. 加重平均の重み値が本文に一切示されていない(最重要)
「スコアリング方針」では「本体スコア(重み付け加重平均)」「重みはAIクローラーの実挙動とGEO研究ベースの確度で設定」とあるが、各score_*(direct_answer/authority/original_info/schema/freshness)に対する具体的な重み係数(数値)が一切書かれていない。total_scoreの算出式そのものが未定義であり、受入基準「上記仕様通りに実装されていること」を検証する基準がない。方針(何を重視するか)の説明はあるが、実装に必要な具体値という「回答」が欠落している。
2. llms.txtの扱いが本文内で矛盾・未定義
「llms.txtは重み最小」と言及されているが、「判定方式の分離」セクションの機械的判定・LLM判定いずれにもllms.txt関連の項目がなく、DBスキーマ(diagnosis_results)にもllms.txtに対応するscore列が存在しない。llms.txtをgate判定に含めるのか独立スコアにするのか、そもそもどのフィールドに反映するのかが本文中で回答されていない。
3. Gemini切替閾値の矛盾
本文「LLM接続先」および受入基準では「同一IPから3回以上のトライ」「同一IP3回以上でGemini無料枠へのフォールバック」(>=3)と明記されている。一方「レビュー反映(2回目)」の2番では「attempt_count>3でGemini切替」(>3、実質4回目から切替)としており、3回目のリクエストをClaudeで処理するかGeminiで処理するかの境界が一致していない。
4. 受入基準(検収条件)が2回目レビュー反映内容を反映していない
「受入基準」セクションはレビュー前(1回目)の内容のままで、2回目で追加された下記項目が検収条件に含まれていない:
- rate_limit_ipテーブルの存在・スキーマ
- 24時間ウィンドウのリセットロジック
- diagnosis_resultsへのllm_provider列追加
- error_reason=
llm:gemini_capacity固定文字列であること
本文は詳細化されたが、Directorのdone判定対象である受入基準自体が更新されておらず、2回目で追加した仕様が検証対象から漏れている。加えてcf16(acceptance_criteria)も空欄のままで、構造化フィールドと本文の受入基準内容が一致していない。
5. (軽微)件名が空欄
チケットの件名フィールドが空になっている。
VERDICT: needs_revision
Redmine Admin さんが14日前に更新
[クロスレビュー] レビュアー種別: claude
VERDICT: needs_revision
レビュー結果(ticket #10469)¶
指摘1: DBスキーマ本体ブロックが最新のレビュー反映を取り込んでいない¶
「DBスキーマ」節に書かれた CREATE TABLE IF NOT EXISTS diagnosis_results のSQLブロックには score_llms_txt 列と llm_provider 列が存在しない。しかし受入基準および反映(2回目)(3回目)ではこの2列の存在が必須条件として明記されている。担当者がこのSQLブロックをそのままコピーして実装した場合、列が漏れる。同様に rate_limit_ip テーブルも「DBスキーマ」節の本体には記載がなく、反映(2回目)の追記のみに存在する。本体スキーマと受入基準が食い違ったまま起票されており、後から読む担当者が本体だけ見て実装してしまうリスクがある。
指摘2: 機械判定系スコアの異常系フォールバックが未定義¶
「LLM失敗フォールバック」節はLLM呼び出し(score_direct_answer/score_original_info)の失敗時の合流先(total_score=null, error_reason記録, 200維持)を明記しているが、機械的判定側(score_freshness: sitemap.xml不在/lastmod欠如、score_schema: JSON-LD不在/パース失敗、score_authority: 外部リンク抽出不可)についてはエラー時の挙動が一切定義されていない。受入基準には「異常系が全て200+error_reasonで返っていること(500にならない)」とあるが、これを満たすための機械判定側のフォールバックパターン(error_reasonの値、該当スコアの扱い)がスペックに存在しないため、実際に何が「異常」で何を返すべきかを担当者が独自判断することになる。
指摘3: score_authority / score_schema / score_freshness の0〜100正規化ルールが未定義¶
重み付け式は各score_*が0〜100スケールであることを前提にしているが、「外部リンクユニークドメイン数」「sitemap lastmod」「JSON-LD解析結果」からどのように0〜100の点数に変換するかの基準(閾値・上限・0点/満点の条件)が本文に一切記載されていない。受入基準の「total_scoreが上記重み式で算出されていること」を検証するには、各サブスコアの算出ロジックが定義されている必要があるが、現状は加重合算の重みだけが確定していて、合算対象そのものの算出方法が空欄になっている。
以上3点は起票意図(何を実装すべきか)に対する回答漏れであり、追加機能や仕様肥大化の提案ではない。
VERDICT: needs_revision
Redmine Admin さんが14日前に更新
[クロスレビュー] レビュアー種別: claude
VERDICT: needs_revision
クロスレビュー結果(ticket #10469)¶
観点1(起票意図への回答)・観点2(回答漏れ/誤解)に沿って確認しました。3回のレビュー反映を経ており大枠は整合していますが、以下の点は未解決のまま残っています。
1. ゲート判定NG時のレスポンス仕様が未定義(最重要)¶
「スコアリング方針」は「ゲート判定(クロール可否)→本体スコア」の2段階と定義しているが、gate_crawlable=false または gate_robots_blocked=true の場合に total_score / breakdown をどう返すか(LLM失敗時と同様に total_score=null に合流させるのか、0にするのか、breakdown各項目はnullか0か)が本文のどこにも記載がない。
受入基準の「異常系が全て200+error_reasonで返っていること」はエラー・例外系の話であり、ゲートNGは実運用上ごく普通に起きる正常分岐(robots.txtで拒否しているサイトは多い)なので、これを「異常系」に含めてよいのかも判断できない。LLM判定に進む前段の主要な分岐であるにもかかわらず、レスポンス契約が未確定。
2. safeFetchの対象範囲に関する記述の食い違い¶
「ファイル構成」節では crawlability.js の safeFetch について「本文fetchとrobots.txt取得の両方が必ずこれを経由」と2つのみ列挙しているが、レビュー反映(3回目)でllms.txt取得が追加され、受入基準では「safeFetchが本文fetch/robots.txt/llms.txtの全てを必ず経由していること」と3つに増えている。ファイル構成節の記述自体は更新されておらず、実装者が上から読んだ場合にllms.txt取得をsafeFetch経由にし忘れるリスクが残る。
3. error_reasonフィールドの用途が二重定義気味で曖昧¶
「error_reasonはgate:/llm:の接頭辞で区別し、llm:claude / llm:geminiのようにどちらで判定したかも記録する」とあるが、これが成功時にもerror_reasonへプロバイダ名(llm:claude等)を書き込む意図なのか、失敗時のプレフィックスとしてのみ使うのか不明瞭。別途「diagnosis_resultsにllm_provider列が存在し、成功時に'claude'または'gemini'が記録されること」という要件があり、成功時のプロバイダ情報はllm_provider列で扱う設計に見えるため、error_reasonとllm_provider列の役割分担(error_reasonは失敗時のみ設定されるのか、成功時も値が入るのか)が本文からは判断できない。
いずれも解決案は担当者側で検討すべき事項のため、ここでは指摘のみとします。
VERDICT: needs_revision
Redmine Admin さんが14日前に更新
Redmine自動クロスレビューは5回連続で仕様テキストのneeds_revisionが続き、コードが1行も書かれない状態が12時間継続したため、自動パイプラインをバイパスしProducer(chat)からcodex:submitで直接実装を依頼。実装完了後、Claude Codeによる実コードレビューを別途実施しneeds_revisionが1回(IPv6 IPv4互換アドレス表記のSSRF拒否バイパス、実害あり)、Codexが修正対応しapprove。本番場所(/root/website-vending-machine/vend01/app)へコピーしcommit d71afa6 でコード実装完了(.envの実 APIキーはgit履歴汚染回避のため除外、GEMINI_API_KEYは未設定プレースホルダ)。本番反映(docker compose build/up)は#10470(human_gate)でユーザーの最終確認待ち。
Redmine Admin さんが12日前に更新
PM引継ぎ検収(2026-07-26): 実装コミット d71afa6 を確認。vend01-api稼働中、/health=200、/api/diagnosisでClaude成功応答・数値スコア・llm_provider記録を確認済み。SSRFレビュー指摘も修正済み。コード実装チケットとして解決へ更新します。未解決の秘密値ローテーションと運用整備は別チケットへ分離します。