diff --git a/.claude/skills/eccube-controller/SKILL.md b/.claude/skills/eccube-controller/SKILL.md
index 2abe14fe13..631ffad255 100644
--- a/.claude/skills/eccube-controller/SKILL.md
+++ b/.claude/skills/eccube-controller/SKILL.md
@@ -1,6 +1,7 @@
---
name: eccube-controller
description: EC-CUBE 4.4 のコントローラを実装・改修するときの責務分離規約。「コントローラを作って」「アクションを追加して」「このコントローラを直して」「ルーティングを追加して」などと言われたとき、または src/Eccube/Controller・app/Customize/Controller 配下を作成・編集するときに使用する。Fat コントローラを避け業務ロジックを Service へ寄せるための規約。
+
---
# Controller 規約 — 責務分離と Fat 化防止(EC-CUBE 4.4)
@@ -135,6 +136,24 @@ vendor/bin/php-cs-fixer fix # PSR-12 整形・ライセン
- ❌ `#[Template]` 付きアクションの「エラー時に再描画される」を前提にレビュー判断 → ✅ `#[Template]` は配列を返したときのみ engage。Response/Redirect を返すパス(例: フォーム失敗で `redirectToRoute`)では描画されない
- ❌ 管理アクションを `%eccube_admin_route%` 配下以外に置く → ✅ admin ファイアウォール配下に置く
- ❌ 同一アクションで `executePurchaseFlow()` を複数回呼ぶとき 2 回目以降の `FlowResult` を無視する → ✅ 毎回 `hasError()`/`hasWarning()` の分岐を 1 回目と同じに揃える(共通化可)
+- ❌ 配列が来る可能性のあるリクエスト値を `getString()` でスカラーに強制する → ✅ `InputBag` は非スカラーで例外を投げるため強制できない。`all()` で受けて型を検査する
+- ❌ 削除アクションで外部キー違反だけを狙って個別の例外型を catch する → ✅ コアは `catch (\Exception)` で受けて `trans('admin.common.delete_error_foreign_key')` を返す形に統一している(`CategoryController` ほか 10 以上のコントローラ)。例外型を絞る実装は「動作としては正しいがコア非準拠」なので、揃えるかどうかは受入基準で判断する
+
+## 実行・確認方法
+
+```bash
+# ルーティングが意図どおり登録されたか(#[Route] の属性ミスはここで気づく)
+bin/console debug:router | grep product_detail
+bin/console cache:clear
+
+# 該当コントローラのテストだけを回す
+vendor/bin/phpunit tests/Eccube/Tests/Web/ProductControllerTest.php
+```
+
+- 実装後の整形・型・静的解析・テストは **AGENTS.md「開発コマンド」** に従って実行する
+ (PHP-CS-Fixer / PHPStan level 6 / PHPUnit)。
+- 認可の入り口は `security.yaml` だけでは追えない(`access_control` は `EccubeExtension` が動的注入する)。
+ 新規の管理アクションは Skill `eccube-security` の「アクセス制御モデル」を確認する。
---
diff --git a/.claude/skills/eccube-e2e/SKILL.md b/.claude/skills/eccube-e2e/SKILL.md
index 874382825c..56c51119d1 100644
--- a/.claude/skills/eccube-e2e/SKILL.md
+++ b/.claude/skills/eccube-e2e/SKILL.md
@@ -42,6 +42,7 @@ description: EC-CUBE 4.4 の E2E テスト(Playwright・`e2e/` 配下)を実
- ❌ retry でプラグイン/データが残留し「既にインストール済み」で再失敗 → ✅ `beforeEach`/`afterEach` で cleanup(無効化→削除→ディレクトリ削除。`plugin-misc.spec.ts` 修正例)。
- ❌ パスワードを見た目の文字数で作る → ✅ NFKC 正規化後で 15 文字以上か数える(min15。`[...str.normalize('NFKC')].length` で確認。#6488)。
- ❌ 新規 `admin-*`/`front-*` spec を作ったのに CI で実行されない → ✅ `.github/workflows/e2e-test.yml` の `suite:` 配列にファイル名(接尾辞 `.spec.ts` 抜き)を追加する。
+- ❌ 無関係な複数スイートが一斉に落ちたのを spec 側の不具合として個別に追う → ✅ 先に **globalSetup のログ**を見る。`setup-fixtures.php` は各ブロックに try-catch を持たない直列スクリプトなので、途中で Fatal になると**以降のフィクスチャが丸ごと未生成**になり、それに依存するスイートが連鎖的に落ちる。しかも `global-setup.ts` は失敗を catch して `Continuing without additional fixtures...` と警告するだけで**実行を止めない**ため、原因が spec 側にあるように見える。ログ末尾に `Fixtures setup complete.` が出ているかで切り分ける(コアの定数削除で `setup-fixtures.php` の置換が 1 箇所漏れ、admin-order / admin-refund-request / front-refund-request が同時に落ちた例がある)。
## 実行・確認方法
diff --git a/.claude/skills/eccube-entity/SKILL.md b/.claude/skills/eccube-entity/SKILL.md
index e639e2d380..6409ab3aac 100644
--- a/.claude/skills/eccube-entity/SKILL.md
+++ b/.claude/skills/eccube-entity/SKILL.md
@@ -1,6 +1,7 @@
---
name: eccube-entity
description: EC-CUBE 4.4 の Doctrine エンティティを実装・改修するときの規約。「エンティティを作って」「テーブルを追加して」「Entityにフィールドを足して」「リレーションを定義して」「マスタを追加して」などと言われたとき、または src/Eccube/Entity・app/Customize/Entity 配下を作成・編集するときに使用する。
+
---
# Entity 規約(EC-CUBE 4.4)
@@ -109,3 +110,25 @@ if (!class_exists(Example::class)) {
- ❌ 金額を float で四則演算(丸め誤差)→ ✅ `bcmath`(`bcadd` / `bcmul` / `bccomp`、スケール 2)で計算する
- ❌ `create_date` / `update_date` を自前の `#[ORM\PrePersist]`(+`#[ORM\HasLifecycleCallbacks]`)でセット → ✅ コアの `SaveEventSubscriber`(グローバル Doctrine prePersist/preUpdate)が `method_exists` で `setCreateDate`/`setUpdateDate`/`setCreator` を自動セットする(`src/Eccube/Doctrine/EventSubscriber/SaveEventSubscriber.php`)。setter さえ生やせばよく、自前 PrePersist は二重実装になるので書かない
- ❌ 他エンティティ(特にコアの `Product`/`Customer` 等、自分で制御できない親)への関連で親削除時の挙動を未決定 → ✅ FK は既定で削除を止める(RESTRICT 相当)。未指定だと**退会・商品削除が FK 違反で失敗**したり孤児化する。`onDelete`(`SET NULL`/`CASCADE`)を指定するか、Service・プラグイン disable 等で後始末する(コアは `onDelete` を限定使用し[95 JoinColumn 中 2 件]、多くは Service 側で関連を整理している)
+- ❌ `@deprecated` なゲッタを未使用と判断して削除する → ✅ CSV 出力項目(`dtb_csv`)のアクセサとして現役のことがあり、削除は仕様変更になる。先に CSV 定義と照合する
+- ❌ トレイト拡張・プロキシ絡みの不具合を「手元で再現しないから誤検知」と判断する → ✅ `TraitProxyAttributeDriver` は対象 FQCN が**まだ宣言されていないときだけ** `app/proxy/entity` の生成物を `require_once` する(同一 FQCN を二重宣言して `Cannot redeclare class` になるのを避けるため)。まっさらな状態では修正の有無にかかわらずトレイト由来の列が出るので、不具合はクラスとメタデータがロード済みのときだけ顕在化する。再現するには `getMetadataFor()` などで対象を先にロードした状態を作り、検証のたびに `app/proxy/entity` を消してから試す
+
+## 実行・確認方法
+
+```bash
+# 属性から導かれるスキーマ差分を SQL で確認(実行前に必ず目視する)
+bin/console doctrine:schema:update --dump-sql
+bin/console doctrine:schema:update --force
+
+# トレイトで拡張した場合はプロキシを再生成する(忘れると列が生えない)
+bin/console eccube:generate:proxies
+bin/console cache:clear
+```
+
+- 実装後の整形・型・静的解析・テストは **AGENTS.md「開発コマンド」** に従って実行する
+ (PHP-CS-Fixer / PHPStan level 6 / PHPUnit)。
+- カラム追加はマイグレーション不要(`schema:update` が反映する)。要否の判断は Skill `eccube-migration` を参照。
+
+---
+
+実装・改修後は、Skill `eccube-review-responsibility` で責務分離を点検すること。
diff --git a/.claude/skills/eccube-formtype/SKILL.md b/.claude/skills/eccube-formtype/SKILL.md
index b81b4337aa..74707af5bf 100644
--- a/.claude/skills/eccube-formtype/SKILL.md
+++ b/.claude/skills/eccube-formtype/SKILL.md
@@ -1,6 +1,7 @@
---
name: eccube-formtype
description: EC-CUBE 4.4 のフォーム(FormType)を実装・改修するときの規約。「フォームを作って」「FormTypeを追加して」「入力項目を足して」「バリデーションを設定して」「検索フォームを作って」「既存フォームに項目を追加して」などと言われたとき、または src/Eccube/Form・app/Customize/Form 配下を作成・編集するときに使用する。
+
---
# FormType 規約(EC-CUBE 4.4)
@@ -79,3 +80,22 @@ class ExampleType extends AbstractType
- ❌ 具象クラス依存 → ✅ コンストラクタ DI + 必要なサービスの注入
- ❌ 既存フォームに二重送信防止/楽観ロック用の unmapped hidden を足し、サーバー側で値未送信を即エラー扱い → ✅ 値が空/未送信なら判定をスキップ(プログラム的 POST・既存テスト・外部連携を壊さない後方互換を保つ)
- ❌ 共通 FormType(RepeatedPasswordType 等)を子で使い `options.constraints` を渡す(親が定義した制約が全置換され消える) → ✅ 親の制約一式も再掲して付与する
+
+## 実行・確認方法
+
+```bash
+# FormType の構成・オプション・拡張が効いているかを確認する(FQCN でも短縮名でもよい)
+bin/console debug:form BlockType
+bin/console cache:clear
+
+# フォームのテストだけを回す
+vendor/bin/phpunit tests/Eccube/Tests/Form/Type/Admin/BlockTypeTest.php
+```
+
+- 実装後の整形・型・静的解析・テストは **AGENTS.md「開発コマンド」** に従って実行する
+ (PHP-CS-Fixer / PHPStan level 6 / PHPUnit)。
+- `FormTypeExtension` で拡張した場合は、`debug:form` の出力に追加項目が現れることで登録を確認できる。
+
+---
+
+実装・改修後は、Skill `eccube-review-responsibility` で責務分離を点検すること。
diff --git a/.claude/skills/eccube-migration/SKILL.md b/.claude/skills/eccube-migration/SKILL.md
index ce6805119e..519754b1c5 100644
--- a/.claude/skills/eccube-migration/SKILL.md
+++ b/.claude/skills/eccube-migration/SKILL.md
@@ -1,6 +1,7 @@
---
name: eccube-migration
description: EC-CUBE 4.4 のデータベースマイグレーションを作成・編集するときの規約。「マイグレーションを作って」「マスタデータ/初期データを投入したい」「カラムの型を変えたい/リネームしたい」「スキーマを変えたい」などと言われたとき、または app/DoctrineMigrations 配下を作成・編集するときに使用する。注意: 単純なカラム追加は Entity 属性+schema:update で反映されるためマイグレーション不要(その判断にも本 Skill を参照)。
+
---
# マイグレーション規約(EC-CUBE 4.4)
@@ -32,6 +33,11 @@ bin/console doctrine:migrations:migrate --no-interaction
Entity 属性を足せば、新規は `schema:create`、既存は `schema:update` が拾う。
→ 例: PR #4912(Google アナリティクス機能)は `BaseInfo` にカラムを追加したが、
ALTER マイグレーションは作らず `schema:update` に委ねている。
+ > **ただし実際のコアには両方の前例がある**。`Version20260316234241` は `dtb_base_info` への
+ > カラム追加に対して `ALTER TABLE dtb_base_info ADD ...` を書いている。
+ > つまり「カラム追加にマイグレーションが付いている=規約違反」とは言い切れない。
+ > レビューで「マイグレーション欠落」「マイグレーション不要」を**断定しない**こと。
+ > 既定は不要(`schema:update` に委ねる)だが、既存行へ既定値を確実に入れたい等の理由があれば付けてよい。
- **マイグレーションを書くのは次の場合に限る**:
1. **マスタ/初期データの INSERT**(`mtb_*` のレコード、`dtb_block` / `dtb_mail_template` / `dtb_csv` 等への初期レコード投入)。
2. **`schema:update` が安全に扱えない構造変更**(カラムの**型変更・リネーム**、データ移行を伴う変更、
@@ -120,10 +126,32 @@ final class Version20240101000000 extends AbstractMigration
## よくある間違い
-- ❌ カラムを足したので `ALTER TABLE ... ADD COLUMN` のマイグレーションを書く
- → ✅ Entity 属性を足すだけ。新規は `schema:create`、既存は `schema:update --force` が反映する。
+- ❌ カラムを足したので反射的に `ALTER TABLE ... ADD COLUMN` のマイグレーションを書く
+ → ✅ 既定は Entity 属性を足すだけ(新規は `schema:create`、既存は `schema:update --force` が反映)。
+ 既存行への既定値投入など理由があれば書いてよい(上記「両方の前例がある」を参照)。
- ❌ `doctrine:migrations:diff` で Entity 差分から ALTER を自動生成する
→ ✅ `doctrine:migrations:generate` で空の雛形を作り、必要な SQL(INSERT・型変更等)だけ手で書く。
- ❌ マイグレーションでテーブルを"新規定義"してスキーマの源泉にする → ✅ 源泉は Entity 属性。
- ❌ INSERT・構造変更でガードなし → 再実行や環境差で失敗。✅ 存在チェックで冪等にする。
- ❌ `down()` 未実装 → ロールバック不能。✅ `up()`/`down()` を対で実装。
+
+## 実行・確認方法
+
+```bash
+# 属性から導ける差分(カラム追加・変更)は schema:update 側で反映される
+bin/console doctrine:schema:update --dump-sql
+
+# マイグレーション(INSERT・型変更等)を適用し、down() も往復で確かめる
+bin/console doctrine:migrations:migrate
+bin/console doctrine:migrations:migrate prev
+bin/console doctrine:migrations:migrate
+```
+
+- 実装後の整形・型・静的解析・テストは **AGENTS.md「開発コマンド」** に従って実行する
+ (PHP-CS-Fixer / PHPStan level 6 / PHPUnit)。
+- 冪等性は「同じマイグレーションを 2 回流しても失敗しない」ことで確認する。
+- 新規インストール経路(`doctrine:schema:create` + 初期データ)でも通ることを確認する。
+
+---
+
+実装・改修後は、Skill `eccube-review-responsibility` で責務分離を点検すること。
diff --git a/.claude/skills/eccube-plugin/SKILL.md b/.claude/skills/eccube-plugin/SKILL.md
index 4bd8faa657..9b8cc5a376 100644
--- a/.claude/skills/eccube-plugin/SKILL.md
+++ b/.claude/skills/eccube-plugin/SKILL.md
@@ -144,6 +144,7 @@ bin/console eccube:generate:proxies # app/proxy/entity/ を再生成
- ❌ トレイト追加後にプロキシ再生成を忘れる → ✅ `bin/console eccube:generate:proxies`
- ❌ プロジェクト固有の 1 回限りの改変をプラグイン化 → ✅ それは `app/Customize/`。着脱・再配布するものだけプラグイン
- ❌ `app/Customize`(`Eccube\` を直接拡張)と `app/Plugin`(`Plugin\{Code}\` 独立名前空間)の名前空間を混同 → ✅ 置き場所で名前空間を使い分ける
+- ❌ 「無効化した(または DB に登録していない)プラグインは読み込まれない」と考える → ✅ `Kernel::registerBundles()` は **DB の有効/無効を一切見ず**、`app/Plugin` 直下を `Finder` で列挙して各 `app/Plugin/<コード>/Resource/config/bundles.php` を `require` する。依存クラスが欠けたプラグインを置いただけで**カーネル起動そのものが失敗し、`bin/console` も PHPUnit も全滅する**。QA が一斉に起動段階で落ちたら、composer の依存を疑う前に `app/Plugin` の中身を退避してキャッシュを消して切り分ける
## 実行・確認方法
diff --git a/.claude/skills/eccube-purchase-flow/SKILL.md b/.claude/skills/eccube-purchase-flow/SKILL.md
index 2d70393b8e..5c45e9aa00 100644
--- a/.claude/skills/eccube-purchase-flow/SKILL.md
+++ b/.claude/skills/eccube-purchase-flow/SKILL.md
@@ -58,6 +58,9 @@ description: EC-CUBE 4.4 の受注処理(PurchaseFlow の Processor/Validator
メソッドを実装する**。PurchaseProcessor は `AbstractPurchaseProcessor` を継承すれば必要なメソッドだけ override 可。
- **`supports()` で早期 return**: フロー種別・`Order` か否か・店舗設定(`BaseInfo`)で適用可否を判定し、
対象外なら何もしない(`AddPointProcessor::supports()` が手本)。
+- **トランザクション境界はリクエスト全体**: `TransactionListener` が `kernel.request` で `beginTransaction()`、
+ `kernel.terminate` で `commit()`、例外時に `rollback()` する。Processor 内の `flush()` は SQL を発行するだけで
+ **コミットではない**ため、在庫引当・採番の可視性や競合を論じるときは「flush 済み=確定」と読み替えないこと。
- **金額計算は `bcmath`**(`bcadd` / `bcsub` / `bcmul` / `bccomp`)。float 演算で組まない。
合計・税・送料・値引きの集計は `PurchaseFlow::calculateAll()` が各段階後に行うので、Processor 側は
**明細(Item)を足し引きする**ことに集中する(合計の手計算は不要)。
@@ -237,6 +240,7 @@ class SaleLimitOneValidator extends ItemValidator
- ❌ PurchaseProcessor の `rollback()` を実装し忘れる → ✅ `prepare()` の逆操作(在庫戻し等)を必ず実装する
- ❌ 属性方式で実行順を制御しようとする → ✅ 順序が要るなら YAML タグの `priority`(降順)で指定する
- ❌ (A) YAML タグと (B) 属性を両方付ける → ✅ どちらか一方。コアは YAML、プラグイン/Customize は属性が定石
+- ❌ 送料無料を商品単位の性質として扱う(商品詳細に「この商品は送料無料」と出す、構造化データに `shippingRate: 0` を出す等)→ ✅ `DeliveryFeeFreePreprocessor` は `BaseInfo::getDeliveryFreeAmount()` を `$itemHolder->getTotal()` と、`getDeliveryFreeQuantity()` を `getQuantity()` と比較する **カート全体の合計に対する条件**。カートが確定していない商品ページでは判定できないので、出せない値は出さない
## 実行・確認方法
diff --git a/.claude/skills/eccube-repository/SKILL.md b/.claude/skills/eccube-repository/SKILL.md
index f04227ad7b..cdbdcf5ff7 100644
--- a/.claude/skills/eccube-repository/SKILL.md
+++ b/.claude/skills/eccube-repository/SKILL.md
@@ -1,6 +1,7 @@
---
name: eccube-repository
description: EC-CUBE 4.4 の Doctrine リポジトリを実装・改修するときの規約。「リポジトリを作って」「検索メソッドを追加して」「クエリを書いて」「一覧の絞り込みを実装して」などと言われたとき、または src/Eccube/Repository・app/Customize/Repository 配下を作成・編集するときに使用する。
+
---
# Repository 規約(EC-CUBE 4.4)
@@ -74,3 +75,21 @@ class ExampleRepository extends AbstractRepository
- ❌ 画面表示の一覧・関連取得を無制限に全件取得(件数が際限なく増え得る)→ ✅ ページング(Paginator 用に QueryBuilder を返す)か上限を設ける
- ❌ join 先への絞り込みを EXISTS 部分クエリへ移すとき、その別名に掛かっていた既存の制約を引き継がない → ✅ 同じ制約を EXISTS 内に再掲し、集計・出力側の母集団と一致させる
- ❌ 1 対多の範囲絞り込みで下限・上限を独立した EXISTS 2 本に分ける(別々の子行が満たせばヒットしてしまう)→ ✅ 同一の子行に両条件を要求するなら EXISTS 1 本にまとめる
+
+## 実行・確認方法
+
+```bash
+# 生成される DQL / SQL を確認する(EXISTS の制約漏れ・JOIN の重複はここで気づく)
+# → $qb->getQuery()->getDQL() / ->getSQL() をテストで出力して目視する
+vendor/bin/phpunit tests/Eccube/Tests/Repository/ProductRepositoryTest.php
+```
+
+- 実装後の整形・型・静的解析・テストは **AGENTS.md「開発コマンド」** に従って実行する
+ (PHP-CS-Fixer / PHPStan level 6 / PHPUnit)。
+- 件数・母集団が絡む変更は、**期待件数、または想定する母集団との整合性**をテストで固定する
+ (絞り込み条件を変えれば件数が変わるのは正常なので、「変更前後で件数が一致すること」を固定してはいけない)。
+- 一覧のページングは Paginator 側で適用されるため、Repository は QueryBuilder を返したままにする。
+
+---
+
+実装・改修後は、Skill `eccube-review-responsibility` で責務分離を点検すること。
diff --git a/.claude/skills/eccube-review-responsibility/SKILL.md b/.claude/skills/eccube-review-responsibility/SKILL.md
index a1c1641bde..45ba759dfa 100644
--- a/.claude/skills/eccube-review-responsibility/SKILL.md
+++ b/.claude/skills/eccube-review-responsibility/SKILL.md
@@ -5,6 +5,10 @@ description: EC-CUBE 4.4 で実装・改修したコードを実装直後に自
# 実装直後の自己レビュー(全層チェックリスト)
+**対象**: 直前の変更差分(`git diff` / `git diff --cached` で見える範囲)。
+特定のディレクトリに紐づかず、コントローラ / サービス / フォーム / テンプレート / エンティティ等の
+実装・改修が一区切りついた時点で全層を横断して使う。既存の未変更コードは対象外。
+
実装・改修が一区切りついたら、変更差分を次の観点で横断的に点検する。
**これは助言であり、必ずしも全件修正を要求しない**(既存コードの一括修正は求めない)。
@@ -65,3 +69,28 @@ description: EC-CUBE 4.4 で実装・改修したコードを実装直後に自
- **新規・改修したコードの指摘**を優先して提示する。
- 既存(未変更)コードの問題は、無理に直さず「将来のリファクタ候補」として軽く触れるに留める。
- 各指摘に「どの処理を、どこ(どの Service/Processor、どのエスケープ)へ」の具体案を添える。
+
+## 実行・確認方法
+
+```bash
+# 点検対象の差分を確定させる(レビュー範囲を「変更した箇所」に固定する)
+git diff --stat # 変更したファイルの一覧
+git diff # 変更行の実体(--stat だけでは何を直したか確定できない)
+git diff --cached --stat
+git diff --cached
+
+# 機械的に判定できるものは先にツールへ委ねる(実装中は変更した範囲に絞って回す)
+vendor/bin/php-cs-fixer fix src/Eccube/Controller/ProductController.php
+vendor/bin/phpstan analyse src/Eccube/Controller
+vendor/bin/phpunit tests/Eccube/Tests/Web/ProductControllerTest.php
+
+# PR を出す前に CI と同じ範囲へ広げる
+vendor/bin/phpstan analyse src # level 6
+```
+
+- ツールが拾えるもの(整形・型・非推奨 API)を人手で数えないこと。本 Skill の観点は
+ **ツールで検出できない責務分離・認可・エスケープ**に集中する。
+- **PHPStan を変更ファイルだけに絞ったまま push しない**。CI は `src` 全体を解析するため、
+ 呼び出し側との型の不整合など「変更ファイルの外に出るエラー」を見落とす。実装中は絞って速く回し、
+ PR を出す前に一度 `vendor/bin/phpstan analyse src` を通す。
+- 各レイヤの詳細な検証手順は、該当レイヤの Skill の「実行・確認方法」に従う。
diff --git a/.claude/skills/eccube-security/SKILL.md b/.claude/skills/eccube-security/SKILL.md
index 906dbe924f..88b7afe9e2 100644
--- a/.claude/skills/eccube-security/SKILL.md
+++ b/.claude/skills/eccube-security/SKILL.md
@@ -17,6 +17,29 @@ description: EC-CUBE 4.4 の認証・認可・CSRF などセキュリティを
EC-CUBE は **個別アクションの `#[IsGranted]` ではなく、ファイアウォール+ロール+Voter** で制御する。
設定は `app/config/eccube/packages/security.yaml`。
+> **`access_control` は `security.yaml` に無い。`src/Eccube/DependencyInjection/EccubeExtension.php`
+> の `configureFramework()` が `prependExtensionConfig('security', ...)` で動的に注入している**
+> (コード内コメント「security.ymlでは制御できないため, ここで定義する」)。生成されるのは次の 6 規則で、
+> `ECCUBE_FORCE_SSL` が有効なら全規則に `requires_channel: https` が足される。
+>
+> | path | roles |
+> |---|---|
+> | `^/%eccube_admin_route%/login` | `IS_AUTHENTICATED_ANONYMOUSLY` |
+> | `^/%eccube_admin_route%/` | `ROLE_ADMIN` |
+> | `^/mypage/login` | `IS_AUTHENTICATED_ANONYMOUSLY` |
+> | `^/mypage/withdraw_complete` | `IS_AUTHENTICATED_ANONYMOUSLY` |
+> | `^/mypage/change` | `IS_AUTHENTICATED_FULLY` |
+> | `^/mypage/` | `ROLE_USER` |
+>
+> **認可レビューで `security.yaml` だけを見ると、この 6 規則が見えず誤検出する。**
+> 例: `^/mypage/` 配下は `ROLE_USER` が前提なので `getUser()` は非 null だが、
+> これを知らないと「null 参照で 500 になる」と誤って指摘してしまう。
+> **ただし規則は上から順に最初に一致したものが適用される**ため、より上に並ぶ
+> `/mypage/login`・`/mypage/withdraw_complete`(`IS_AUTHENTICATED_ANONYMOUSLY`)は
+> 匿名で到達でき、`getUser()` が null になり得る。**`^/mypage/` 配下を一律に
+> 「認証済み」と扱わない**こと(`/mypage/change` は逆に `IS_AUTHENTICATED_FULLY` で更に厳しい)。
+> 認可の穴を探すときは必ず `EccubeExtension.php` 側も合わせて読む。
+
- **firewalls** は 3 つ:
- `admin`: `pattern: '^/%eccube_admin_route%/'` — `Member`(管理者) を認証。`enable_csrf: true`、login throttling 有り。
- `customer`: `pattern: '^/'`(サイト全体)— `Customer`(会員) を認証。remember_me 有り。
diff --git a/.claude/skills/eccube-service/SKILL.md b/.claude/skills/eccube-service/SKILL.md
index d38cd33c4e..cb5280ddd0 100644
--- a/.claude/skills/eccube-service/SKILL.md
+++ b/.claude/skills/eccube-service/SKILL.md
@@ -1,6 +1,7 @@
---
name: eccube-service
description: EC-CUBE 4.4 の Service を実装・改修するときの責務分離規約。「サービスを作って」「ロジックをサービスに切り出して」「このサービスを直して」「コントローラから業務処理を抽出して」などと言われたとき、または src/Eccube/Service・app/Customize/Service 配下を作成・編集するときに使用する。業務ロジックの受け皿を単一責任・HTTP非依存に保つための規約。
+
---
# Service 規約 — 業務ロジックの置き場所(EC-CUBE 4.4)
@@ -94,6 +95,24 @@ vendor/bin/php-cs-fixer fix # PSR-12 整形・ライセン
- ❌ 1 つの Service に無関係な処理を寄せ集める → ✅ 単一責任で分割
- ❌ `Request` を Service に渡す → ✅ 必要な値だけを引数で渡す
- ❌ ループ内で毎回 `flush()` → ✅ まとめて `flush()`(トランザクション境界を意識)
+- ❌ `flush()` を確定として扱う → ✅ `TransactionListener` が 1 リクエスト=1 トランザクションで包み、コミットは `kernel.terminate`。`flush` は SQL 発行のみ
+- ❌ `if` の下にあるロックを「条件付きだからほとんど通らない/デッドコード」と読む → ✅ 条件に使っているプロパティが**永続化されているか**を確かめる。`StockReduceProcessor` の悲観ロック(`EntityManager::lock(..., PESSIMISTIC_WRITE)`)は `if ($productStock->getProductClassId() === null)` の下にあるが、`ProductStock::$product_class_id` は `#[ORM\Column]` を持たない**非永続(transient)プロパティ**で、DB 列の `product_class_id` は `OneToOne` の `JoinColumn` 側にある。つまり DB から読み込んだ直後は常に `null` で、**ロックは毎回実行される**
+
+## 実行・確認方法
+
+```bash
+# サービスが登録され、依存が解決できているかを確認する
+# (ID の一部でも絞り込めるが、複数一致すると対話で選択を求められるので FQCN が確実)
+bin/console debug:container 'Eccube\Service\CartService'
+bin/console cache:clear
+
+# 該当サービスのテストだけを回す
+vendor/bin/phpunit tests/Eccube/Tests/Service/CartServiceTest.php
+```
+
+- 実装後の整形・型・静的解析・テストは **AGENTS.md「開発コマンド」** に従って実行する
+ (PHP-CS-Fixer / PHPStan level 6 / PHPUnit)。
+- デコレーション・差し替えをした場合は `debug:container` の出力で解決先が意図どおりか確認する。
---
diff --git a/.claude/skills/eccube-twig-template/SKILL.md b/.claude/skills/eccube-twig-template/SKILL.md
index c6fdeb5ab7..1efe3b4d89 100644
--- a/.claude/skills/eccube-twig-template/SKILL.md
+++ b/.claude/skills/eccube-twig-template/SKILL.md
@@ -98,6 +98,7 @@ public function onTemplateCart(TemplateEvent $event): void
- ❌ **管理画面テンプレートだから安全**と油断して `|raw` する → ✅ admin 配下も XSS シンク(過去の XSS 修正は管理画面テンプレートに多い)。DB/入力由来の値は admin でも必ずエスケープする
- ❌ テンプレートイベントにエンティティ永続化など業務処理を書く → ✅ 見た目調整のみ。業務は対応するコントローライベントへ
- ❌ inline `` や `"` で XSS・JSON 破壊になる。`json_encode($data, JSON_UNESCAPED_SLASHES|JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)` で `<`/`>`/`&` をエスケープする
+- ❌ 管理画面テンプレートの `$('#modal').on('show.bs.modal', ...)` を「Bootstrap 5 は jQuery 非対応だから動かない」と判断して書き換える → ✅ 本体は Bootstrap 5.3 と jQuery を併存させており、BS5 は jQuery があるときイベントを jQuery 側にも発火させる。コアの管理画面テンプレート 10 以上(`Content/layout.twig`・`Order/edit.twig` ほか)がこの書き方で動いている。モーダルの挙動を推測で判断せず、疑うなら実機か E2E で確認する
## 実行・確認方法
diff --git a/AGENTS.md b/AGENTS.md
index 588c7fc3fa..db593ac186 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -242,6 +242,7 @@ frontmatter の `description` がトリガ条件で、該当レイヤを触る
| CSV 入出力(CsvImport/Export・CSV 定義) | [`.claude/skills/eccube-csv/SKILL.md`](./.claude/skills/eccube-csv/SKILL.md) | `eccube-csv` |
| コンソールコマンド(Symfony Console・バッチ) | [`.claude/skills/eccube-command/SKILL.md`](./.claude/skills/eccube-command/SKILL.md) | `eccube-command` |
| 責務分離レビュー(実装直後の自己チェック・全層) | [`.claude/skills/eccube-review-responsibility/SKILL.md`](./.claude/skills/eccube-review-responsibility/SKILL.md) | `eccube-review-responsibility` |
+| コントリビューション(PR 作成・CI ゲートの再現) | [`.claude/skills/eccube-contributing/SKILL.md`](./.claude/skills/eccube-contributing/SKILL.md) | `eccube-contributing` |
> 規約は必要になった時点で `.claude/skills/eccube-/SKILL.md` を 1 ファイル追加して足す(`.codex`/`.agents` は symlink で自動共有)。
> 各ファイルは frontmatter(`name` / `description`)+本文の順で書き、本文は「対象/基本ルール/実装パターン/よくある間違い/実行・確認方法」の構成を推奨する(推測を載せず、必ず `src/Eccube/` の実コードで裏取りする)。