← CLAUDE-PENGIN-TOOLS
DOCUMENT · 22.1 KB

qa/verification-2026-08-18.md

Workspace snapshot · 09/04 13:33

wordpress-migration Skill 検証レポート

  • 実施日: 2026-08-18
  • 担当: PENGIN Tools 事業部 QA
  • 対象: C:\service\agent-venture-studio\agents\claude-pengin-tools\workspace\skills\wordpress-migration
  • 対象コミット: aa914f6 (branch: main, working tree clean)

0. 結論(先に書く)

依頼された「実行による検証」は 1 件も実施できていません。

本セッションではコマンド実行手段(Bash ツール)が無効化されており、 sh -n / bash -n / python -m py_compile / 偽 wp を使った動作検証の いずれも実行不能でした。

したがって本レポートは 静的コードレビューのみ の結果です。 セクション A〜D の全項目は SKIP (実行不能) であり、 「安全ガードが実測で効いている」ことは証明できていません。

公開可否の判断は「実行検証を別環境でやり直すまで保留」を推奨します。


1. 実行環境

項目結果
コマンド実行手段 (Bash ツール)利用不可。Error: No such tool available: Bash. Bash is disabled for this session, in subagents as well as here.
bashバージョン確認不能(実行手段が無いため存在自体も未確認)
sh同上・未確認
python / python3同上・未確認
shellcheck同上・未確認
サブエージェント経由の実行不可(上記エラーメッセージが subagents も対象と明記)

利用できたツール: ファイル読み取り (Read)、パターン検索 (Glob/Grep)、 ファイル書き込み (Write/Edit)。プロセス起動系は一切なし。

外部ネットワークアクセス: 行っていません(WebFetch/WebSearch 未使用)。 対象ディレクトリ外の読み書き: 行っていません(例外は本レポートの出力先のみ)。 一時ファイル: 作成していません(作成手段が無いため)。


2. 検証項目ごとの結果

A. 構文チェック

#目的実行コマンド期待実際判定
A-1preflight.sh の POSIX/bash 構文sh -n scripts/preflight.sh / bash -n ...exit 0実行できずSKIP (実行不能: シェル起動手段なし)
A-2search_replace_plan.sh の構文sh -n scripts/search_replace_plan.sh / bash -n ...exit 0実行できずSKIP (同上)
A-3verify.py のコンパイルpython -m py_compile scripts/verify.pyexit 0実行できずSKIP (同上)
A-4shellcheckshellcheck scripts/*.sh警告確認実行できずSKIP (同上。shellcheck の存在も未確認)

代替として全 3 ファイルを目視で構文レビューしました(結果はセクション 3)。 目視では構文エラーを発見していませんが、これは sh -n の合格を意味しません。 特に以下は目視で正当と判断したものの、実行確認が必須です。

  • search_replace_plan.sh:107,170 / preflight.sh:63,96 のクォート付き ヒアドキュメント(終端 EOF が行頭カラム 0 にあることは確認済み。 本文中の \ やバッククォートはクォート付きなので展開されない)
  • search_replace_plan.sh:371 の $( [ ... ] && printf '...' || printf '...' ) を二重引用符内にネストした形
  • search_replace_plan.sh:288,294 の case ... in *[!a-z0-9.-]*) パターン
  • preflight.sh:278 の grep -E "define\([[:space:]]*['\"]${_name}['\"]"
  • preflight.sh:667 の wp_val eval "... {\$wpdb->posts} ... 'publish' ..."

B. search_replace_plan.sh の安全ガード(最重要)

偽 wp を用意して PATH 先頭に置く検証は 1 件も実施できていません。 呼び出しログが取得できないため、 「DB を書き換える呼び出しが意図したケース以外で発生していないか」の 機械的確認は未実施です。

#ケース期待実際判定
B-1--helpexit 0 / 使い方表示未実行SKIP (実行不能)
B-2引数なしexit 2 / --old と --new は必須未実行SKIP
B-3--old/--new 同一exit 2未実行SKIP
B-4--old 'ex ample.com'exit 2未実行SKIP
B-5偽 wp を PATH から外すexit 2 / WP-CLI 不在未実行SKIP
B-6既定 dry-run(全 search-replace に --dry-run、6 パス、順序、--precise 等)exit 0 / 全件 dry-run未実行SKIP(最重要項目が未検証)
B-7--execute のみexit 2 / DB 非接触未実行SKIP
B-8--execute --backup <存在しないパス>exit 2未実行SKIP
B-9--execute --backup <0 バイト>exit 2未実行SKIP
B-10--execute --backup <ダミー sql> 非対話・--yes なしexit 2未実行SKIP
B-11--execute --yes --backup <ダミー sql>dry-run 完走後に実置換未実行SKIP
B-12--include-bare-host で 7 パス + 警告7 パス / 警告未実行SKIP

コード読解上、B-1〜B-12 に対応する分岐は存在します(該当行はセクション 3 参照)。 ただし「存在する」ことと「宣言どおり効く」ことは別であり、後者は未検証です。

C. preflight.sh

#目的期待実際判定
C-1偽 wp で完走 / レポート出力exit 0未実行SKIP (実行不能)
C-2書き込み系 wp サブコマンドを呼ばないこと呼び出し 0 回未実行(ログ取得手段なし)SKIP
C-3--helpexit 0未実行SKIP

C-2 についてはソース全文の grep による静的確認を行いました(セクション 3.C)。 静的には書き込み系サブコマンドは 1 つも使われていません。 ただし「実行時に呼ばれない」ことの実測ではありません。

D. verify.py

#目的期待実際判定
D-1--helpexit 0未実行SKIP (Python 起動不能)
D-2base_url 無しで起動非 0 終了未実行SKIP
D-3純粋関数への合成データ投入(noindex 検出 / canonical 判定 / 旧ドメイン残存 / 混在コンテンツ)期待どおり判定未実行(import 手段なし)SKIP

D-3 の対象として実物から特定した純粋関数(引数と戻り値の形は読解済み):

  • parse_html(text) -> PageParser (verify.py:228)
  • PageParser(verify.py:121): title / meta_robots / canonical / og_url / base_href / asset_urls / link_urls
  • normalize_domain(value) (verify.py:391)
  • host_of(url) (verify.py:403)
  • domain_matches(host, domain) (verify.py:410)
  • parse_sitemap_xml(text) -> (kind, urls) (verify.py:424)
  • decode_body(body, headers) (verify.py:315)
  • _strip_ns(tag) (verify.py:420)

判定ロジック本体(noindex / canonical 旧ドメイン / 旧ドメイン残存 / 混在コンテンツ)は check_url() (verify.py:537)に集約されており、HTTP 取得と判定が同一関数内で 密結合しています。合成 HTML だけを渡して判定部分だけを単体で叩く公開関数は ありません。ネットワークを使わずに D-3 を実施するには follow() / http_get() をモンキーパッチする必要があります (=ライブラリとしてのテスタビリティが低い、という設計上の指摘)。


3. 静的レビューで検出した事項

実行検証の代替として行った目視レビューの結果です。 いずれも「実行して再現を確認した不具合」ではありません。

3.B search_replace_plan.sh

B-a [要修正候補・中] --include-bare-host が「old と new が同一」ガードを無効化する

search_replace_plan.sh:279

if [ "$OLD_HOST" = "$NEW_HOST" ] && [ "$INCLUDE_BARE_HOST" -eq 0 ]; then

--include-bare-host を付けると同一ホスト判定が丸ごとスキップされます。 --include-bare-host --old a.example.com --new a.example.com は exit 2 にならず、 全 7 パスが「自分自身への置換」として実行されます(実害は薄いが、 依頼書の期待「同一 → exit 2」を満たさないケースが存在する)。 INCLUDE_BARE_HOST と同一性判定に論理的な関係が無く、条件の混入と見えます。

仕様判断が必要なため未修正。 意図がある(スキーム差のみの置換を通したい等)なら その意図は OLD_SCHEME の内側判定(281 行)で既に満たされており、 外側の INCLUDE_BARE_HOST 条件は不要と考えます。

B-b [要修正候補・中] ループ内の wp 呼び出しが PASSFILE を stdin に継承する

search_replace_plan.sh:498-505 / 469 / 472

while IFS="$TAB" read -r _label _old _new; do
    run_pass "$_label" "$_old" "$_new" "$_mode"
done < "$PASSFILE"

run_pass 内の wp は stdin として PASSFILE を継承します。 wp が stdin を消費した場合、残りのパスが黙って飛ばされます。 EXECUTE フェーズで発生すると「一部の形式だけ置換された DB」になり、 しかもエラーが出ません(--dry-run 欠落系の事故ではないが、静かな取りこぼし)。 なお依頼書が指定する「偽 wp を作って検証」の構成でも、偽 wp が cat 等で stdin を読む実装だとこの経路で症状が出ます。

定石の対処は wp 呼び出しに < /dev/null を付けるか、 パスを別 FD で読むこと。実行して回帰を確認できないため未修正。

B-c [要修正候補・低〜中] 一時ファイルパスが予測可能(mktemp 未使用)

search_replace_plan.sh:330,446

PASSFILE="$TMPBASE/wpmig-sr-passes.$$"
OUTFILE_TMP="$TMPBASE/wpmig-sr-out.$$"

: > "$PASSFILE" はシンボリックリンクを追随します。共有 /tmp の環境で 他ユーザーが先に同名シンボリックリンクを置けるため、任意ファイルの truncate、 および(PASSFILE を差し替えられた場合)wp に渡す置換ペアの改変が理論上可能です。 preflight.sh は mktemp -d を正しく使っている(preflight.sh:167-173)ので、 同じ作法に揃えるのが自然です。未修正(挙動変更を伴うため)。

B-d [仕様確認事項] host_of はポートを除去しない(コメントと不一致)

search_replace_plan.sh:249-257 コメントは「スキームと末尾スラッシュ、パス、ポートを落とす」と書いていますが、 sed にポート除去がありません。--old old.example.com:8080 は ホスト名に : が残り、search_replace_plan.sh:287 の文字種チェックで exit 2 になります。安全側に倒れるので危険はありませんが、 ドキュメントと実装が食い違っています。

B-e [設計上の指摘] EXECUTE フェーズで 1 パス失敗しても残りを続行する

run_pass は失敗時に FAILED=1 を立てて return 1 しますが、 iterate_passes の while は break しません(search_replace_plan.sh:501-504)。 DRY-RUN フェーズは終了後に FAILED を見て exit 1 で止まる(519-522)ので 問題ありませんが、EXECUTE フェーズは部分適用のまま先へ進みます。 エラーメッセージで「部分的に置換された状態の可能性」と告知しており、 意図的な設計とも読めます。仕様判断が必要なため未修正、事実として報告。

B-f [軽微] 確認プロンプトで yes 以外を入力したときの終了コードが 0

search_replace_plan.sh:556-559。中止も「正常終了」扱いです。 自動化から中止を検出しにくい。仕様判断事項。

B-g [軽微] --old 等がコマンドラインの最後に来た場合の shift

--old の分岐は shift; OLD_RAW="${1:-}" の後に case 末尾でもう一度 shift します (search_replace_plan.sh:180,207。preflight.sh:112-115 も同型)。 引数が尽きた状態での shift は dash 等でエラーメッセージを出します (set -e 未使用なので中断はしない)。実害は小さいが要実測。

B-h [確認できた良い点(ただし静的確認のみ)]

  • --dry-run の付与は run_pass の _mode = "dry" 分岐のみ(469 行)。 非 dry-run の search-replace(472 行)に到達する経路は iterate_passes "live"(570 行)だけで、その手前に DO_EXECUTE 判定(527 行 / 388 行)、バックアップ検証(389-412 行)、 DRY-RUN フェーズ完走と FAILED チェック(516-522 行)、 対話確認または --yes(550-564 行)が直列に並んでいます。 構造としては依頼書の期待どおりです。
  • 置換パスの追加順(346-354 行)は https → http → プロトコル相対 → JSON エスケープ 3 種 → (任意)ホスト名単体 で、依頼書の期待と一致。
  • SR_COMMON(318-323 行)に --all-tables-with-prefix --precise --report-changed-only が常時、--skip-columns=guid が --replace-guid 未指定時に付与される。
  • JSON エスケープ形の \/\/ は printf '%s' の引数として渡され(339 行)、 read -r で読み戻される(501 行)ため、バックスラッシュは保持されるはず (%s は %b と違いエスケープを解釈しない)。要実測。
  • --backup 検証は 存在 → 通常ファイル → 読取可 → 非 0 バイト の順(396-412 行)で、 すべて exit 2。100KB 未満は WARN のみ(418-420 行)。

3.C preflight.sh

C-a 書き込み系 wp サブコマンドの静的棚卸し(grep による全数確認)

スクリプト中で wp に渡されるサブコマンドは以下のみでした。

core version / eval / option get / config get / config path / plugin list / db size / db tables / cron event list / site list / core verify-checksums

option update / db import / db query / plugin deactivate / plugin install / theme 変更系 / search-replace / rewrite flush / cache flush は一切現れません。wp eval に渡す PHP も get_loaded_extensions() / wp_upload_dir() / wp_load_alloptions() / $wpdb->get_var("SELECT ...") のみで、いずれも読み取りです。 静的には read-only の宣言どおり。実行ログによる裏取りは未実施。

C-b [仕様確認事項] --deep は外部ネットワークへ出る

preflight.sh:838 の wp core verify-checksums は api.wordpress.org へチェックサムを取得しに行きます。 「読み取り専用」の宣言と矛盾はしませんが、 オフライン環境や外部通信が禁止された環境では失敗します。 ドキュメント(preflight.sh:76-78 のヘルプ)に外部通信の記載がありません。

C-c [ドキュメント不一致・軽微] 「exit code は常に 0」ではない

ヘッダ(preflight.sh:21-23)とヘルプ(94)は「常に 0」と書いていますが、 未知のオプションは exit 2(146-150)、--help/--version は exit 0 です。 実装は妥当なので、記述側を直すのが筋です。

C-d [軽微] generate_report | tee "$OUTFILE" はサブシェル実行

preflight.sh:908。関数内で完結しており、リスク/WARN は ファイル経由で受け渡しているため実害は見当たりません(175-180, 202-204)。 ただし tee 不在時の分岐(910-911)では標準エラーが /dev/null に落ちます。

3.D verify.py

D-a [要実測・中] parse_sitemap_xml に XML 宣言付き文字列を渡した場合

verify.py:429 は ET.fromstring(text) に str を渡します。 実サイトの sitemap はほぼ必ず <?xml version="1.0" encoding="UTF-8"?> で始まります。 標準ライブラリの ElementTree はこれを許容すると理解していますが (encoding declaration で例外を出すのは lxml)、実行して確認できていません。 仮に例外になっても verify.py:431-436 の正規表現フォールバックが <loc> を拾うため、致命的な失敗にはならない設計です。

D-b [設計上の指摘] 判定ロジックが HTTP 取得と同一関数に密結合

セクション D-3 に記載のとおり、noindex / canonical / 旧ドメイン残存 / 混在コンテンツの判定はすべて check_url()(verify.py:537)内にあり、 HTTP 取得を伴わずに単体検証できる形になっていません。 QA 観点では、判定部を evaluate_page(text, headers, final_url, old_domain) の ような純粋関数に切り出すことを推奨します(実装変更のため未修正)。

D-c [仕様確認事項] 旧ドメイン「本文残存」件数の算出

verify.py:680-690。residual = body_hits - len(ref_hits) は 「HTML 全体の文字列出現数」から「属性由来のヒット数」を引く近似です。 相対 URL が旧ドメインへ解決される状況(--old-domain に検証対象自身を 指定した場合など)では ref_hits が body_hits を上回り、 residual が負になって WARN が出ません(> 0 でガード済みなので誤検知はしない)。 過検知より過小検知に倒す設計。事実として報告。

D-d [確認できた良い点(静的確認のみ)]

  • 標準ライブラリのみ。GET のみ(verify.py:261-271 で method="GET" 固定)。
  • リダイレクト自動追随を無効化し(_NoRedirect, 243-251)、 連鎖とループを自前で検出(follow, 339-385。for/else による too_many 判定はロジックとして正しい)。
  • 本文読み込みに 3MB 上限(MAX_BODY_BYTES, 60)。
  • --concurrency / --delay / --timeout で負荷制御可能。
  • domain_matches(410-414)は endswith("." + domain) なので notold.example.com を old.example.com の一致とみなさない(正しい)。
  • normalize_domain(391-400)はスキーム・パス・userinfo・ポート・ 末尾ドットを落とし小文字化する(search_replace_plan.sh の host_of が ポートを落とさないのと対照的で、こちらは実装が正しい)。

4. 「DB を書き換える呼び出しが発生したケース」の一覧

取得できませんでした。

偽 wp のログを取る手段(プロセス起動)が無いため、 --dry-run なしの search-replace が実際に何回・どのケースで発生したかは 一切測定していません。この表を埋められないことが、本レポート最大の欠落です。

静的読解に基づく期待値(未検証):

ケース非 dry-run の search-replace根拠となる行
B-1〜B-5(引数エラー / WP-CLI 不在)0 回178, 242-247, 279-299, 304-308(いずれも iterate_passes 到達前に exit)
B-6(既定 dry-run)0 回527-545 で exit 0(iterate_passes "live" は 570 行)
B-7〜B-10(--execute の安全条件未達)0 回389-412, 550-563(すべて iterate_passes 到達前に exit 2)
B-11(--execute --yes --backup <正常>)6 回(意図どおり)570
B-12(--include-bare-host 併用時)dry-run のみなら 0 回352-354 で 7 パス目を追加

5. 修正した不具合の一覧

なし。1 行も変更していません。

理由:

  1. 実行手段が無く、修正の効果も回帰の有無も検証できない。
  2. 目視で明確な構文エラー・クラッシュ経路は発見できなかった。
  3. 検出した事項(3.B-a, 3.B-b, 3.B-c, 3.C-c 等)は、 いずれも仕様判断か、テスト無しで触るべきでない安全クリティカル箇所。

安全ガードのスクリプトを未検証のまま書き換えるほうが、 指摘として残すよりリスクが高いと判断しました。


6. 未検証のまま残る範囲

以下はすべて未検証です。

  • 依頼された実行検証 A / B / C / D の全項目(構文チェックすら未実施)
  • 偽 wp スタブを用いた search_replace_plan.sh の安全ガード動作 (--dry-run の全パス付与、exit コード、バックアップ検証、 非対話時の実行拒否、DRY-RUN → EXECUTE の順序)
  • preflight.sh の完走・レポート出力・書き込み系サブコマンド非呼び出しの実測
  • verify.py の --help / 引数エラー / 純粋関数の入出力
  • 実 WordPress・実 DB を用いた検証(環境なし。依頼どおり用意していない)
  • 実サイトへの HTTP アクセスを伴う検証(依頼どおり禁止・未実施)
  • 外部ネットワーク経由の検証(wp core verify-checksums の実挙動を含む)
  • 各種シェル実装(dash / ash / busybox / bash)ごとの差異
  • Windows 上での verify.py の cp932 コンソール挙動(_setup_stdout)
  • shellcheck による静的解析

7. 推奨する次アクション

  1. Bash 実行が可能な環境で、本依頼の A〜D をそのまま再実行する。 特に B-6(全 search-replace への --dry-run 付与) と B-7〜B-11(EXECUTE の安全条件) は公開可否の必須条件。
  2. 再実行時、3.B-b(stdin 継承)を狙って stdin を読む偽 wp(例: cat > /dev/null を含む)でも試し、 パスの取りこぼしが起きないか確認する。
  3. 3.B-a(--include-bare-host による同一性ガード無効化)の仕様を確定する。
  4. 3.B-c(mktemp 未使用)を preflight.sh と同じ作法に揃える。
  5. ドキュメント不一致(3.B-d ポート、3.C-c exit code、3.C-b 外部通信)を修正する。

生成: PENGIN Tools QA, 2026-08-18. 実行検証未実施(実行手段なし)。