qwen_scenario_analysis_review_v2

Qwen シナリオ検証レポートのレビュー(改訂版)

対象: https://grav.machines.jp/ja/rewald_build_top_dir/rewald-llm-scenario-index-analysis レビュー日: 2026-04-05(初版) / 2026-04-05(改訂) / 2026-04-05(ソースコード照合・修正実施) レビュアー: Claude Opus 4.6 Qwen に渡されたソース: https://api01.rewards.machines.jp/llms-scenario-index.txt およびそこからリンクされる llms-scenario-{a,b,c,d,e}.txt 初版で誤って照合に使用したファイル: docs/templates/scenario-{a,b,c,d,e}-*.html(簡易テンプレート。llms 版とは内容が異なる)


改訂の経緯

  1. 初版: ローカルの簡易テンプレート(docs/templates/scenario-*.html)と照合し、Qwen の記述の多くを「捏造」と判定。
  2. 第2版: Qwen に渡された llms-scenario-*.txt(API 配信の完全実装ガイド)と再照合。初版の「捏造11件」のうち9件は誤判定と判明。
  3. 第3版(本版): 実際のサーバーソースコード(templates/lib/point-all.twig, src/)を基準に llms-scenario-*.txt を精査。Qwen が「合格」とした箇所に実際のセキュリティ問題を発見し、llms ファイルを修正。

レポート概要

Qwen がシナリオ A〜E を仮想空間でシミュレーションし、各シナリオの正常系・異常系を検証した結果報告。全シナリオで「合格」判定が出ている。

シナリオ一覧

シナリオ 内容 Qwen判定
A ポイント付与のみ(決済なし) 全項目合格
B 商品購入 + PayPal JS SDK(同一ページ完結型) 全項目合格
C 商品購入 + Stripe Checkout(リダイレクト型) Nonce のみ「要確認」、他合格
D ポイント購入 + PayPal NCP(チャージ型) 全項目合格
E ポイント購入 + Stripe Checkout(チャージ型) 全項目合格

シナリオ別 照合結果

シナリオ A: Qwen の記述は正確 — 問題なし

照合元: llms-scenario-a.txt

llms-scenario-a.txt には point_setting, point_own, point_trans_open, point_trans_close の4ショートコードが使われ、「ポイントを付与」ボタン(grant-button)、closeTriggered フラグ、pointInstance.close() が実装されている。

Qwen の記述 llms-scenario-a.txt 判定
「ポイントを付与」ボタンがある grant-button ID のボタンが存在 正確
triggerClose() が発火してポイント付与 pointInstance.close() を await で呼出 正確
closeTriggered フラグで二重付与防止 var closeTriggered = false; で定義、クリック時にチェック 正確
レスポンスで残高表示 displayBalance()response.result.point を参照 正確

評価: Qwen の記述は llms-scenario-a.txt の内容と一致。全項目合格の判定は妥当。PSP なしのシナリオなので pointInstance.close()(POST)で PSP 検証が行われないことは問題ない。

注: ローカルの docs/templates/scenario-a-mypage.htmlpoint_own のみの簡易版であり、llms-scenario-a.txt とは別物。


シナリオ B: Qwen の記述は正確 — ただし PSP 検証バイパスの問題あり → 修正済み

照合元: llms-scenario-b.txt

Qwen の記述 llms-scenario-b.txt 判定
closeTriggered フラグで二重消費防止 var closeTriggered = false; + triggerClose() 内でガード 正確
ゼロ円時に PayPal ボタン非表示 → 「ポイントで支払う」ボタン切替 amount <= 0 の分岐で専用ボタン生成 正確
custom_idpointsystem_nonce を含める custom_id: point.pointsystem_nonce 正確
onCancel でポイント消費しない close を呼ばず、キャンセル表示のみ 正確
response.result.total_price 等を参照 response.result.* 配下で統一 正確

Qwen が見落とした問題(ソースコード照合で発見):

onApprove 内で triggerClose()pointInstance.close()(POST)を呼んでいたが、POST ハンドラ(pointTransCloseRestApi)は verify_callback_name = null を設定するため、functions.php で登録した PayPal 検証コールバックが一切呼ばれない。ブラウザコンソールから triggerClose() を直接実行すれば、PayPal 決済なしにポイント確定が可能だった。

根拠: templates/lib/point-all.twig:1896$atts['verify_callback_name'] = null;

ハンドラ メソッド PSP 検証
pointTransCloseRestApi(POST) pointInstance.close() が使用 null(検証なし)
pointTransCloseRestApiPSP(GET) orderId/customId 付きリクエスト あり

修正内容: onApprove を GET ベースの triggerClosePSP(orderId) に変更。サーバー側で PayPal API 照会が行われるようになった。

変更ファイル: public/llms-scenario-b.txt
追加 FINGERPRINT: B2-PSP-001

シナリオ C: 大部分正確 — PSP 検証未実施 + デッドコード → 修正済み

照合元: llms-scenario-c.txt

Qwen の記述 llms-scenario-c.txt 判定
ボタンクリックで close 実行後、Stripe にリダイレクト pointInstance.close() → Stripe セッション作成(FINGERPRINT: C2-FLOW-001) 正確
rollbackPoints() 関数で Stripe 失敗時にロールバック FINGERPRINT: C2-ROLLBACK-001 で定義。catch 内で await rollbackPoints() 正確
ゼロ円時に Stripe スキップして完了ページへ直接遷移 FINGERPRINT: C2-ZERO-001: if (amount <= 0)window.location.href 正確
成功ページで point_trans_detail 実行 FINGERPRINT: C3-DETAIL-001 正確
キャンセルページで point_trans_cancel 実行 FINGERPRINT: C4-CANCEL-001 正確
close = 「仮確保・ロック」 point_trans_close は確定処理であり「仮確保」という用語は不正確 用語が不正確
セッションタイムアウト(30分)で自動ロールバック llms-scenario-c.txt にも記載なし Qwen の推測(未確認)

Qwen が見落とした問題(ソースコード照合で発見):

  1. PSP 検証未実施: シナリオ B と同じ根本原因。pointInstance.close()(POST)では verify_callback_name = null。functions.php で登録した Stripe 検証コールバックが呼ばれなかった。成功ページで Stripe の payment_status を検証する仕組みがなかった。

  2. デッドコード: カートページ line 173 の var nonce = sessionStorage.getItem('pointsystem_nonce'); が定義されたまま一度も参照されていなかった。

修正内容:

  • functions.php に verify-stripe-session REST エンドポイントを追加(FINGERPRINT: C1-VERIFY-001)
  • 成功ページで Stripe payment_status をサーバー側検証するコードを追加(FINGERPRINT: C3-VERIFY-001)
  • 未使用の nonce 変数を削除
変更ファイル: public/llms-scenario-c.txt
追加 FINGERPRINT: C1-VERIFY-001, C3-VERIFY-001

シナリオ D: Qwen の記述は正確 — 可読性改善のため変数名を修正

照合元: llms-scenario-d.txt

Qwen の記述 llms-scenario-d.txt 判定
非表示 span から nonce 取得 document.getElementById('purchase_nonce').textContent.trim() 正確
custom_id: nonce を PayPal に渡す custom_id: nonce 正確
REST API で point_purchase_close を POST /wp-json/pointsystem/v1/point_purchase_close/ に POST 正確
response.result.result.point で二重ネスト参照 修正前は result.result.point(変数名と JSON キーの衝突)→ 修正後は response.result.point 正確(可読性改善済み)
purchaseClosed フラグ(購入ページ) var purchaseClosed = false; で定義 正確
window.__pointsystem_purchase_closed(完了ページ) 完了ページでグローバルフラグとして実装 正確
リロード時の二重加算防止 __pointsystem_purchase_closed チェックでスキップ 正確

評価: 全項目合格の判定は妥当。pointPurchaseCloseRestApi は PSP 検証コールバックが登録されていれば自動的に呼び出す設計(templates/lib/point-all.twig:1960)のため、シナリオ B/C のような PSP バイパス問題はない。

修正内容: 完了ページの .then(function(result) {...}) の変数名を resultresponse に変更。result.result.pointresponse.result.point となり、他シナリオ(A〜C)の response.result.point と統一。

変更ファイル: public/llms-scenario-d.txt
変更 FINGERPRINT: D3-RESP-001(変数名変更)

背景: 実際の API レスポンスは {"result": {"point": 500}} の単一ネスト。修正前は JS 変数名 result と JSON キー result が衝突して result.result.point となっていたが、動作としては正しかった。可読性と全シナリオの一貫性のために変数名を response に統一した。


シナリオ E: 大部分正確 — リロード二重加算防止が欠落 → 修正済み

照合元: llms-scenario-e.txt

Qwen の記述 llms-scenario-e.txt 判定
非表示 span から nonce 取得 document.getElementById('purchase_nonce').textContent.trim() 正確
nonce と amount を success_url に含める encodeURIComponent(nonce) + encodeURIComponent(amount) 正確
完了ページで URL パラメータ取得 → REST API 呼出 URLSearchParamspoint_purchase_close に POST 正確
response.result.result.point で二重ネスト参照 修正前は result.result.point(変数名と JSON キーの衝突)→ 修正後は response.result.point 正確(可読性改善済み)
window.__pointsystem_purchase_closed でリロード防止 修正前の llms-scenario-e.txt には存在しなかった Qwen がシナリオ D から類推

Qwen が見落とした問題(ソースコード照合で発見):

シナリオ D の完了ページには __pointsystem_purchase_closed フラグによるリロード防止があるが、同じアーキテクチャのシナリオ E には欠落していた。F5 リロードで REST API が再呼出され、二重加算のリスクがあった。

Qwen はこのフラグを「実装済み」と評価したが、実際には D からの類推であり、E のドキュメントには存在しなかった。結果的に Qwen の指摘は「あるべき姿」として正しかったが、「実装済み」という評価は事実と異なった。

修正内容:

  • シナリオ D と同じ __pointsystem_purchase_closed パターンを追加
  • 完了ページの変数名を resultresponse に変更(D と同様、可読性改善)
  • 購入ページの checkout session 作成 fetch に .catch() を追加(エラー時にユーザーへ通知)
  • 完了ページの point_purchase_close fetch に .catch() を追加(ネットワークエラー表示)
変更ファイル: public/llms-scenario-e.txt
追加 FINGERPRINT: E3-GUARD-001
変更 FINGERPRINT: E3-RESP-001(変数名変更)
変更 FINGERPRINT: E2-CHECKOUT-001(.catch 追加)、E3-CLOSE-001(.catch 追加)

llms ファイル修正サマリー

# ファイル 問題 修正内容 FINGERPRINT
1 llms-scenario-b.txt PSP 検証バイパス(PayPal 未検証で close 可能) onApprove を GET ベースの triggerClosePSP(orderId) に変更 B2-PSP-001
2 llms-scenario-c.txt Stripe 決済ステータス未検証 functions.php に verify-stripe-session エンドポイント追加 C1-VERIFY-001
3 llms-scenario-c.txt 成功ページで Stripe 検証なし 成功ページに payment_status 検証コードを追加 C3-VERIFY-001
4 llms-scenario-c.txt 未使用の nonce 変数(デッドコード) var nonce = sessionStorage.getItem(...) を削除
5 llms-scenario-d.txt result.result.point が紛らわしい 変数名 resultresponse に変更 D3-RESP-001
6 llms-scenario-e.txt リロード時の二重加算防止なし __pointsystem_purchase_closed フラグを追加 E3-GUARD-001
7 llms-scenario-e.txt result.result.point が紛らわしい 変数名 resultresponse に変更 E3-RESP-001
8 llms-scenario-e.txt 購入ページの fetch に .catch() なし .catch() でエラー通知を追加 E2-CHECKOUT-001
9 llms-scenario-e.txt 完了ページの fetch に .catch() なし .catch() でネットワークエラー表示を追加 E3-CLOSE-001

Qwen レポートに残る不正確な記述(未修正)

# 内容 該当シナリオ 詳細
1 close = 「仮確保・ロック」という用語 C point_trans_close は確定処理。「仮確保」はシステムに存在しない概念
2 セッションタイムアウト(30分)で自動ロールバック C 下記分析の通り、本システムでは導入困難

セッションタイムアウト自動ロールバックが導入困難な理由(シナリオ C)

Qwen は「セッションタイムアウト(30分)で自動ロールバック」と記述したが、これは一般的なトランザクション管理の知識からの類推であり、本システムのアーキテクチャでは単純に導入できない。

理由1: 「仮確保」状態が存在しない

point_trans_open  → nonce 発行(ポイント残高に変化なし)
point_trans_close → ポイント確定(残高が即座に変動)  ← 確定処理
point_trans_cancel → 確定の取消

close の時点で確定済みであり、「仮確保中」という中間状態がない。タイムアウトで取り消す対象は「確定済みトランザクション」になるため、単なるクリーンアップではなく確定の巻き戻しになる。

理由2: ポイント残高の競合

① 残高 1000pt
② close() → 600pt 利用確定 → 残高 400pt
③ Stripe リダイレクト中にブラウザを閉じる
④ その間にユーザーが別デバイスで 300pt を使用 → 残高 100pt
⑤ 30分後に自動ロールバック → 600pt を戻す → 残高 700pt

⑤の 700pt は正しいか? ④で使った 300pt は close 後の残高(400pt)から使われたもの。自動ロールバックにより使えるはずのなかったポイントが事後的に正当化される

さらに悪いケース:

④' 残高 400pt をすべて別の購入で使用 → 残高 0pt
⑤' 自動ロールバック → 600pt 戻る → 残高 600pt
⑥' ユーザーが 600pt で再度購入 → ロールバック分を二重利用

理由3: Stripe 決済成功との区別ができない

サーバー側からは「Stripe 決済が成功したが success ページを訪問しなかった」と「Stripe 決済が失敗/キャンセルされた」の区別が困難。自動ロールバックすると:

  • Stripe 決済が実際には成功していた場合 → 商品も届き、ポイントも戻る(マーチャント損失)
  • Stripe 決済が失敗していた場合 → ポイントが戻る(正しい動作)

現実的な対処

  • 今回追加した Stripe payment_status 検証(C3-VERIFY-001)で成功ページの偽造を防止
  • キャンセルページ(C4-CANCEL-001)で明示的キャンセルをカバー
  • ブラウザ閉じのエッジケースはカスタマーサポート対応(手動で point_trans_cancel を実行)

ローカルテンプレートと llms 版の乖離

機能 ローカルテンプレート llms 版
closeTriggered フラグ なし あり(A, B, C)
rollbackPoints() 関数 なし あり(C)
ゼロ円分岐処理 なし あり(B, C)
purchaseClosed / __pointsystem_purchase_closed なし あり(D, E に追加
レスポンス参照パス response.result.point response.result.point に統一済み(修正前は D, E で result.result.point
PSP 検証方式(B) GET + orderId で検証あり GET + orderId に修正済み
Stripe 検証(C) なし verify-stripe-session を追加済み

総合評価(最終版)

Qwen レポートの信頼性: 高い(ただし PSP 検証の盲点あり)

強み:

  • 各シナリオの正常系フロー理解は llms-scenario-*.txt の内容と正確に一致
  • 異常系テスト(二重実行、キャンセル、ゼロ円)の分析は実装に基づいている
  • 検証項目の設計(二重実行、ロールバック、通貨、Nonce)は体系的かつ網羅的
  • PayPal / Stripe の連携パターンの違いを正しく区別

弱み:

  • point_trans_close を「仮確保・ロック」と表現(用語の不正確さ)
  • セッションタイムアウト自動ロールバックの存在を推測で記述
  • PSP 検証が実際に呼ばれるかどうかの検証が欠如(最も重要な見落とし)
    • POST vs GET でハンドラが異なり PSP 検証の有無が変わることに気づいていない
    • 「Nonce/セキュリティ: 合格」と判定したが、サーバー側の決済検証がバイパスされていた
  • シナリオ E のリロード防止をシナリオ D から類推して「実装済み」と判定(実際は未実装)

レビュープロセスの教訓

  1. 照合元の一致: LLM レビュー時は、対象 LLM に渡されたソースと同一のものを照合元にする
  2. サーバー側の検証: フロントエンドコードだけでなく、サーバー側ハンドラの実装(POST vs GET の挙動差異)まで追跡する必要がある
  3. 「合格」判定の検証: 仮想シミュレーションの「合格」は、コードの存在確認であり実行確認ではない。実際のリクエストフローでどのコードパスが通るかの検証が不可欠