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-1 | preflight.sh の POSIX/bash 構文 | sh -n scripts/preflight.sh / bash -n ... | exit 0 | 実行できず | SKIP (実行不能: シェル起動手段なし) |
| A-2 | search_replace_plan.sh の構文 | sh -n scripts/search_replace_plan.sh / bash -n ... | exit 0 | 実行できず | SKIP (同上) |
| A-3 | verify.py のコンパイル | python -m py_compile scripts/verify.py | exit 0 | 実行できず | SKIP (同上) |
| A-4 | shellcheck | shellcheck 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 | --help | exit 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 | --help | exit 0 | 未実行 | SKIP |
C-2 についてはソース全文の grep による静的確認を行いました(セクション 3.C)。 静的には書き込み系サブコマンドは 1 つも使われていません。 ただし「実行時に呼ばれない」ことの実測ではありません。
D. verify.py
| # | 目的 | 期待 | 実際 | 判定 |
|---|---|---|---|---|
| D-1 | --help | exit 0 | 未実行 | SKIP (Python 起動不能) |
| D-2 | base_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_urlsnormalize_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 行も変更していません。
理由:
- 実行手段が無く、修正の効果も回帰の有無も検証できない。
- 目視で明確な構文エラー・クラッシュ経路は発見できなかった。
- 検出した事項(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. 推奨する次アクション
- Bash 実行が可能な環境で、本依頼の A〜D をそのまま再実行する。
特に B-6(全 search-replace への
--dry-run付与) と B-7〜B-11(EXECUTE の安全条件) は公開可否の必須条件。 - 再実行時、3.B-b(stdin 継承)を狙って
stdin を読む偽
wp(例:cat > /dev/nullを含む)でも試し、 パスの取りこぼしが起きないか確認する。 - 3.B-a(
--include-bare-hostによる同一性ガード無効化)の仕様を確定する。 - 3.B-c(
mktemp未使用)をpreflight.shと同じ作法に揃える。 - ドキュメント不一致(3.B-d ポート、3.C-c exit code、3.C-b 外部通信)を修正する。
生成: PENGIN Tools QA, 2026-08-18. 実行検証未実施(実行手段なし)。