エージェントコマース Product Feed: ACP / UCP Catalog 配信基盤の整備
- Dominant language
- PHP
- Stars
- 788
- Forks
- 719
- Avg merge
- 4d 4h
- Merged PRs (30d)
- 39
Description
## 概要 (Overview)
AI エージェント (ChatGPT、Gemini 他) が EC-CUBE 上の商品を発見・参照するための **Product Feed / Catalog 配信機能**を本体に実装する。エージェントがチェックアウトセッションを作成する際に line items として送る識別子 (`id` / `sku` / barcode 等) は、このフィード/カタログを介して事前に取得される。
**ACP と UCP では配信モデルが根本的に異なる**点に注意 (本 issue の最重要前提):
| | ACP Product Feed | UCP Catalog capability |
|---|---|---|
| 方向 | **加盟店 → Agent (OpenAI) への push** | **Agent → 加盟店への pull** |
| ホスト | **Agent 側がエンドポイントをホスト**、加盟店はクライアント | 加盟店がエンドポイントをホスト |
| 形式 | `metadata.json` + `products.jsonl` (全置換) / `POST /feeds`・`PATCH /feeds/{id}/products` | REST POST (`/catalog/search`・`/catalog/lookup`・`/catalog/product`) / GraphQL (eccube-api4 再利用) |
| 必須性 | ChatGPT Instant Checkout のカタログ供給に必須 | optional (realtime 取得) |
→ ACP は「加盟店が自前で配信エンドポイントを立てる」モデルでは**ない**。加盟店は `products.jsonl` を生成し、OpenAI がホストする Feed API へ **push (送出)** する。UCP Catalog は逆に加盟店が pull 用 API をホストする。両者を 1 つの「マッピング層 + 配信アダプタ」で扱えるよう共通化する。
## 参照仕様
- **ACP リポジトリ: https://github.com/agentic-commerce-protocol/agentic-commerce-protocol** (`2026-04-17`, Apache 2.0)
- Feed API (OpenAPI): `spec/2026-04-17/openapi/openapi.feed.yaml`
- Feed schema: `spec/2026-04-17/json-schema/schema.feed.json` (`Product` / `Variant` / `FeedMetadata` / `Barcode` / `Availability` / `Price`)
- **UCP リポジトリ: https://github.com/Universal-Commerce-Protocol/ucp** (`v2026-04-08`, Apache 2.0) — Catalog capability / discovery profile
- 共通基盤 #6777 の**薄いスライス**のみに依存: `MinorUnitConverter` / `BaseInfo` フラグ / `AgentCommerceMessageSigner` (UCP EC P-256 鍵、discovery 署名用。秘密鍵の保管は #6797 の `app/keystore/`、公開鍵 JWK は実行時導出) / `AgentCommerceScopeRegistry`。**CheckoutSession 中核 (#6777 後半) や checkout (#6776 / #6574) には依存しない**
- `eccube-api4` (`ec-cube/api42`) — UCP Catalog の GraphQL 再利用・認証時 (REST の認証必須モードや GraphQL を使う場合のみ)
## 背景
ACP / UCP 共に、エージェントがチェックアウトを開始するには**事前に商品を発見し識別子・価格・在庫を把握している**必要がある。本 issue は **ACP Product Feed (push) と UCP Catalog capability (pull) の両方**を対象とする。schema.org/Product JSON-LD は SEO 領域の別タスクでスコープ外。
CheckoutSession の line items 解決はこのフィード/カタログが前提となるため、論理順序上は本 issue が CheckoutSession 系 (#6776 / #6574) の前段。**本 issue は CheckoutSession に依存しないため、checkout (#6776 / #6574) より先行して開発・リリースできる** (依存は #6777 の薄いスライスのみ)。特に UCP Discovery (`/.well-known/ucp`) + UCP Catalog は国内で活用可能なエコシステムの入口として独立に価値があり、先行リリース対象とする。ACP push トランスポートは国内 GA 前のため最終段階に後回し (テスト計画参照)。
## 対象ブランチ
**`4.4` (Symfony 7 対応版)**
---
## 前提とする実行環境
EC-CUBE は **安価な共有レンタルサーバー**および **ec-cube.co (PaaS、CLI アクセス不可)** での動作が前提:
- クラウドストレージ (S3 / GCS 等) を標準機能で要求しない
- Redis / Memcached を標準機能で要求しない
- **ただし ACP feed の push は本質的に outbound 同期を要する** (下記 §1 参照)。標準は「管理画面ボタンによる手動同期」と「`bin/console` CLI コマンド」の 2 つを提供し (CLI 利用可否は環境依存だが標準で同梱)、cron / Symfony Scheduler / Messenger による定期同期は opt-in とする
---
## 実装スコープ
### 1. ACP Product Feed (加盟店 → OpenAI への push) ※実装・検証は最終段階
> **優先度**: ACP push は **ChatGPT Instant Checkout の国内 GA 前で活用できない**ため、実装・テストとも**最終段階に後回し**する (テスト計画 Layer 9)。ただし下記の `products.jsonl` / `metadata.json` **生成・形式**は UCP Catalog と共有するカタログデータ基盤であり、生成パイプラインとスキーマ契約 (Layer 1/2/2.5) は先行検証する。push **トランスポート (送出クライアント)** のみを後回しにする。
ACP の Feed API は **Agent (OpenAI) がホストするエンドポイントに加盟店が push する**モデル (`openapi.feed.yaml`、push model)。加盟店側に `manifest.json` / `shard` / `delta` のような pull 配信エンドポイントは存在しない。
#### 取込形式 (オフライン全置換)
- `metadata.json` — `FeedMetadata` shape (`id` / `target_country` / `updated_at`)
- `products.jsonl` — 1 行 1 `Product` オブジェクト。**全置換 (full replacement)**
#### API (Agent ホスト、加盟店はクライアント)
| メソッド | パス | 役割 |
|---|---|---|
| `POST` | `/feeds` | feed リソース作成 (`createFeed`) |
| `GET` | `/feeds/{id}` | feed メタデータ取得 (`getFeed`) |
| `GET` | `/feeds/{id}/products` | 現在の Agent ホスト商品集合を取得 (`getFeedProducts`) |
| `PATCH` | `/feeds/{id}/products` | `Product.id` 単位の部分 upsert (`upsertFeedProducts`、差分更新の唯一手段) |
#### 本体に実装するもの
- **`products.jsonl` / `metadata.json` 生成パイプライン** (`Product`/`ProductClass` → ACP `Product`/`Variant`)
- **OpenAI Feed API への push クライアント** (`POST /feeds` で初回登録、`PATCH /feeds/{id}/products` で差分 upsert、全置換はファイル取込)
- **認証**: OpenAI への **outbound Bearer 認証情報** (api_key)。これは #6777 の OAuth2 リソースサーバー (inbound 保護) とは方向が逆で対象外。認証情報は専用設定 (DB or `.env`) で管理
- **同期トリガ**: 管理画面ボタン (手動 push) と CLI コマンド (`bin/console eccube:acp-feed:push` 等) の両方を**標準提供**とし、cron / Scheduler / Messenger による定期同期は opt-in
- **差分検出**: `Product`/`ProductClass`/`Order` 更新を EventListener で捕捉し、次回 push 対象としてマーク (`PATCH` で upsert)
### 2. UCP Catalog (加盟店ホストの realtime API)
- REST 版 (v2026-04-08, **すべて POST の RPC 形式**・capability `dev.ucp.shopping.catalog.search` / `.lookup`):
- `POST /catalog/search` (operationId `search_catalog`): `{query?, filters?, pagination?, context?, signals?}` → `{ucp, products[], pagination?, messages?}`
- `POST /catalog/lookup` (operationId `lookup_catalog`): `{ids[], filters?}` → `{ucp, products[], messages?}`
- `POST /catalog/product` (operationId `get_product`): `{id, selected?, preferences?}` → `{ucp, product, messages?}`
- 加盟店ホスト。**Catalog query は read-only のため RFC 9421 署名は OPTIONAL**。認証必須モード (`ucp:catalog` scope) は inbound OAuth2 = eccube-api4 依存
- ⚠ 旧記載の `GET /catalog/products` は v2026-04-08 では不正 (一次仕様 `source/services/shopping/rest.openapi.json` で確認)
- GraphQL 版: **eccube-api4 の既存 GraphQL スキーマに UCP Catalog 用クエリ/タイプを追加** (再利用優先検証・api4 依存)
- どちらを公開するかは `BaseInfo` で切替可能
### 3. データマッピング層
ACP の addressable unit は **`Variant`** (= EC-CUBE `ProductClass`)、その親が **`Product`** (= EC-CUBE `Product`)。共通の DTO/マッパーで ACP `Product`/`Variant` と UCP Catalog の双方へ変換する:
#### Product レベル (EC-CUBE `Product`)
| EC-CUBE | ACP `Product` / UCP | 備考 |
|---|---|---|
| `Product.id` | `id` | 親商品識別子 |
| `Product.name` | `title` | |
| `Product.description_detail` | `description` (`text` / `html` / `markdown`) | HTML タグ除去 or 許可リスト適用 |
| `Product` 詳細ページ URL | `url` | `RequestContext` から生成 |
| `ProductImage` | `media[]` (`type: image`) | |
#### Variant レベル (EC-CUBE `ProductClass` = SKU 単位)
| EC-CUBE | ACP `Variant` / UCP | 備考 |
|---|---|---|
| `ProductClass.id` / `ProductClass.code` | `id` / `sku` | 一意識別子 (店舗商品コード) |
| `ProductClass.price02` | `price` | minor unit、共通基盤 `MinorUnitConverter` |
| (定価があれば) | `list_price` | |
| `ProductClass.stock` / `stock_unlimited` | `availability` | ACP 既知値: `in_stock` / `limited_stock` / `backorder` / `preorder` / `out_of_stock` / `discontinued` |
| `ClassCategory` (規格1/規格2) | `variant_options` | 色・サイズ等 |
| **GTIN / JAN** | `barcodes[]` (`type: GTIN\|UPC\|EAN`, `value`) | **EC-CUBE 標準に GTIN フィールドは無い** (`ProductClass.code` は店舗コードで GTIN ではない)。**`app/Customize/` の拡張ポイント経由で注入**。標準では出力しない |
| `BaseInfo.shop_name` 等 | `seller` | |
> `Product.code` という標準フィールドは存在しない (`Product` の `getCodeMin/Max` は集約値)。GTIN は上記の通り Customize 拡張前提。
### 4. 共通基盤 (#6777) / 本 issue で新設するもの
- `AgentCatalogItemDto` — プロトコル非依存の商品 DTO
- `ProductReferenceResolverInterface` — `sku` / `product_class_id` / barcode から `ProductClass` を解決
- `CatalogMapper` — `Product`/`ProductClass` → ACP `Product`/`Variant` / UCP Catalog の変換パイプライン
- `BaseInfo` 追加フィールド (方針整理 2026-06-09):
- ~~`acp_feed_enabled`~~ — **廃止**。ACP feed push は認証情報 (base URL + API key) の有無で実質ガードするため専用フラグを持たない。
- ~~`ucp_catalog_api_enabled`~~ — **廃止**。UCP Catalog は公開商品データのため**常時公開**(フラグでゲートしない)。
- `ucp_catalog_requires_auth` (boolean, default false) — UCP Catalog の OAuth 必須モード (api4 着手時に実装。#6777 で定義)
- ※ checkout 有効化フラグ `acp_checkout_enabled` / `ucp_checkout_enabled` は #6777 で定義 (本 issue の discovery/catalog/feed はいずれも checkout フラグの対象外)。
### 5. Discovery (`.well-known`)
ACP と UCP は discovery の方式・パスが**非対称**だが、**配信基盤は共通化する** (well-known ルーティング / `api_base_url`・`endpoint` の `RequestContext` 動的生成 / capability・transport の宣言モデル / RFC 8615 設置形態判定・管理画面警告)。`DiscoveryDocumentBuilder` 的なプロトコル非依存層を置き、**UCP profile を最初の具象**、ACP `acp.json` を**スロットイン可能**に設計する (これが ACP discovery を Layer 3 までに「抽象として」並行整備する主目的=#6777 の共通基盤強化)。
- **UCP**: `GET /.well-known/ucp` (ucp.dev 標準・確定)。profile schema に厳密準拠 (`ucp` オブジェクト + `signing_keys`、reverse-domain キーの `services`/`capabilities`、EC 公開鍵 JWK、HTTPS 必須・3xx 禁止・`Cache-Control: public, max-age ≥ 60s`)。Catalog capability は profile の `capabilities` / `services` に宣言。**先行リリース対象。**
- **ACP**: discovery は二層。
- **商品 discovery = 本 issue の Product Feed を OpenAI へ push** (`schema.feed.json`)。merchant ホストの商品 well-known は無く、これが主たる discovery surface。
- **capability discovery = `/.well-known/acp.json`** (`rfc.discovery.md`、**Status: Proposal / unreleased**、リリース版 2026-04-17 未収録)。中身は `protocol`/`api_base_url`/`transports`/`capabilities` (= checkout の `services`/`extensions`) のみ。**鍵・商品カタログ・`merchant_id` は MUST NOT**。
- **⚠ `acp.json` ≠ Product Feed の広告**: `rfc.discovery.md` の **Non-Goal に「Product or catalog discovery は out of scope」**と明記。`acp.json` が宣言するのは **checkout capability** であり、これを実装しても「ACP Product Feed 対応」を謳う surface にはならない。ACP の商品 discovery は **feed の push そのもの** (本 issue Layer 1/2/2.5 の `products.jsonl` 生成 + Layer 9 の push)。
- **`acp.json` の実出力 (公開) はゲートする**: (1) RFC が **released** 化、かつ (2) ACP **checkout (#6776) が実装済 + `acp_checkout_enabled=true`** を満たすまで**出力しない** (builder は先行整備するが、動かない checkout capability を広告しない / unreleased スキーマへの作り込みを避ける)。それまでは forward-looking・フラグ裏。
- **共通の設置制約 (RFC 8615)**: well-known はオリジンルート固定・1 オリジン 1 枚。サブディレクトリ / 1 オリジン複数ショップの制約・配信方針は **共通基盤 #6777 「10. 設置形態と discovery 配信」** に準拠。`api_base_url`/`endpoint` は `RequestContext` から動的生成。
### 6. 在庫・価格の更新反映 (EventListener 駆動)
- `Product` / `ProductClass` 更新時、`Order` 完了時の在庫変動を Doctrine EventListener で捕捉
- **ACP feed**: 変更分を「次回 push 対象」としてマークし、`PATCH /feeds/{id}/products` で upsert (全置換は定期 or 手動)
- **UCP Catalog (pull)**: 加盟店ホストのキャッシュ (FilesystemAdapter、`var/cache/catalog/`) を無効化
- 在庫の即時反映が必要な場合は `stock_updated_at` カラム追加を検討 (`update_date` では在庫変動を捕捉しきれない)
### 7. UCP Catalog 配信のパフォーマンス (共有レンサバ前提)
UCP Catalog は加盟店ホストの pull API なので、リクエスト時 lazy 生成 + FilesystemAdapter キャッシュ + Symfony Lock (スタンピード防止) + gzip + `iterableResult()`/`StreamedResponse` で大規模カタログに対応:
| 項目 | 目標値 | 手段 |
|---|---|---|
| 一覧/個別取得 (キャッシュ Hit) | p95 ≤ 500 ms | FilesystemAdapter から gzip 配信 |
| 個別商品取得 (UCP SLO) | p50 ≤ 1 s / p95 ≤ 10 s | `POST /catalog/product` |
| キャッシュ Miss (初回生成) | p95 ≤ 30 s 以内 | `iterableResult()` + `StreamedResponse` (PHP timeout 内) |
| メモリ上限 | ≤ 256 MB / プロセス | stream、配列蓄積しない |
> ACP feed (push) 側の性能目標は「`products.jsonl` 生成時間」と「push 送出のスループット」であり、pull の ETag/304 とは別物。生成は `iterableResult()` でストリーム生成し、巨大カタログでもメモリを蓄積しない。
### 8. 管理画面 / CLI
- **管理画面と CLI の 2 系統を標準提供**する (どちらか一方に依存しない運用を保証)。共有レンサバ等で管理画面操作を好む運用、cron や CI/デプロイから自動化したい運用の双方をカバーする
- 管理画面 (`/admin/content/agent_commerce`・コンテンツ管理メニュー): ACP feed の「今すぐ push」「全置換 push」、UCP Catalog の「キャッシュクリア」「キャッシュ生成」ボタン (CLI アクセス不可な ec-cube.co 等でも操作可能)
- CLI (標準同梱): `bin/console eccube:acp-feed:push` (差分 upsert) / `--full` (全置換)、`eccube:ucp-catalog:cache:warmup` / `:clear`。cron / Scheduler から呼び出すことで定期同期 (opt-in) の実行口も兼ねる
### 9. 配送情報
- ACP `Variant` / UCP Catalog には代表値のみ。確定送料は CheckoutSession の `fulfillment_options[]` (#6776 / #6574) で計算。
---
## スコープ外
- schema.org/Product (JSON-LD) — SEO 領域の別タスク
- Product Feed 管理画面の高度なチューニング UI — 別 issue
- MCP transport での Catalog 公開 — EC-CUBE MCP Server の別タスク
- 多言語フィード — 別 issue (拡張ポイントは確保)
---
## テスト計画
**優先度の方針**: UCP Catalog (pull) と Discovery は**日本国内で活用可能**なため先行検証する。**ACP push トランスポートは ChatGPT Instant Checkout の国内 GA 前で活用できないため、テスト・実装とも最終段階 (Layer 9) に集約**する。ただし `products.jsonl` / `metadata.json` の**生成・形式正しさ**は push 可否と無関係なカタログデータ基盤 (UCP と共有) なので、Layer 1 / 2 / 2.5 で先行検証する。
### Layer 0: 仕様適合性 (Spec Conformance / 要件トレーサビリティ)
`specifications/` 配下の一次仕様 (ACP `2026-04-17` / UCP `v2026-04-08`) の各**規範要件 (MUST / MUST NOT / SHALL / REQUIRED)** を検証テストケースに紐付け、**「どの仕様要件を満たし、実装済みか」を追跡可能**にするためのレイヤー。Layer 1〜9 の機能テストとは独立した観点で、横断的に要件の充足状況をスナップショットする。
**方針:**
- **1 テストケース = 1 規範要件**。テストメソッドの docblock に**仕様書の GitHub URL** (該当行アンカー付き) を記載する。
- **assertion の第3引数 (失敗メッセージ) に仕様要件文をそのまま記載**する (例: `'MUST NOT include authentication tokens or keys'`)。テスト失敗時に**どの MUST 違反か**がメッセージから直接読めるようにし、レビュー時も「コード ↔ 仕様」を 1:1 で確認できるようにする。
- **未実装の要件は `$this->markTestIncomplete()` で `incomplete` とする**。CI 上で「未充足要件」として可視化し、実装完了時に incomplete を外す運用とする。
- **要件カタログ (全 MUST 一覧) の網羅的な管理は別 issue** とし、本 issue にはテスト計画と**サンプル数本のみ**を示す。本テスト群はそのカタログに対する**実装状況の生きたスナップショット**として機能する。
- URL は版固定の参照とする (ACP はパスに版 `2026-04-17` を含む / UCP docs は `v2026-04-08` 時点)。行番号は仕様更新で前後しうるため、参照は**要件文の文面**を正とする。
> **対象スコープ**: 本 issue は Feed / Catalog / Discovery が対象。checkout / order の規範要件 (`order-rest.md` の「`MUST` check the `messages` array in responses before accessing order data」等) は #6776 / #6574 のトレーサビリティに委ね、ここでは Catalog / Discovery の要件に集中する。
#### サンプルテスト
> 以下はパターンを示す**サンプル**。実装済みの要件はそのまま assert し、未実装は `markTestIncomplete()` とする。
**UCP Catalog REST 適合性 (実装対象):**
```php
// tests/Eccube/Tests/Service/AgentCommerce/Conformance/UcpCatalogConformanceTest.php
/**
* UCP Catalog REST transport conformance.
*
* @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/catalog/rest.md#conformance
* (v2026-04-08, "A conforming REST transport implementation MUST")
*/
class UcpCatalogConformanceTest extends AbstractWebTestCase
{
public function testProductsReturnValidPriceObjects(): void
{
$client = $this->createClientWithScope('ucp:catalog');
$client->request('POST', '/catalog/search', [], [], ['CONTENT_TYPE' => 'application/json'], '{}');
$body = json_decode($client->getResponse()->getContent(), true);
foreach ($body['products'] as $product) {
foreach ($product['variants'] as $variant) {
// rest.md#conformance item 2 — 第3引数に要件文を記載
self::assertArrayHasKey('amount', $variant['price'],
'MUST: Return products with valid `Price` objects (amount + currency).');
self::assertArrayHasKey('currency', $variant['price'],
'MUST: Return products with valid `Price` objects (amount + currency).');
}
}
}
public function testSearchResponseHasUcpWrapper(): void
{
$client = $this->createClientWithScope('ucp:catalog');
$client->request('POST', '/catalog/search', [], [], ['CONTENT_TYPE' => 'application/json'], '{}');
$body = json_decode($client->getResponse()->getContent(), true);
// search_response は ucp / products を必須とする
self::assertArrayHasKey('ucp', $body,
'MUST: catalog search/lookup responses MUST include the `ucp` response wrapper.');
self::assertArrayHasKey('products', $body,
'MUST: catalog search responses MUST include a `products` array.');
}
public function testLookupDeduplicatesDuplicateIdentifiers(): void
{
// MUST: Duplicate identifiers in the request MUST be deduplicated.
// @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/catalog/lookup.md#L56
$this->markTestIncomplete('未実装: /catalog/lookup (Lookup capability) 未実装');
}
public function testOversizedBatchReturnsRequestTooLarge(): void
{
// MUST: Return HTTP 400 with `request_too_large` error for requests exceeding batch size limits.
// rest.md#conformance item 5
$this->markTestIncomplete('未実装: バッチ上限超過時の request_too_large エラー');
}
}
```
**UCP Discovery profile 適合性:**
```php
// tests/Eccube/Tests/Service/AgentCommerce/Conformance/UcpDiscoveryConformanceTest.php
public function testWellKnownServedOverHttpsWithCacheControl(): void
{
// MUST: served over HTTPS; 3xx redirects prohibited.
// SHOULD/MUST: Cache-Control: public, max-age >= 60
// @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/docs/specification/overview.md
$this->markTestIncomplete('未実装: /.well-known/ucp 配信 (Phase 1a / トラック A で実装予定)');
}
public function testSigningKeysContainOnlyPublicJwkParameters(): void
{
// MUST NOT: private key parameters ("d") MUST NOT appear in signing_keys[].
// EC public key JWK のみ (kty:"EC", crv:P-256|P-384, x, y)
// @see https://github.com/Universal-Commerce-Protocol/ucp/blob/main/source/schemas/profile.json
$this->markTestIncomplete('未実装: discovery 署名鍵公開 (AgentCommerceMessageSigner)');
}
```
**ACP Feed schema 適合性 (生成データ — push 可否と無関係):**
```php
// tests/Eccube/Tests/Service/AgentCommerce/Conformance/AcpFeedConformanceTest.php
public function testGeneratedProductHasRequiredFields(): void
{
// MUST (schema): Product は "id" と "variants" が required。
// @see https://github.com/agentic-commerce-protocol/agentic-commerce-protocol/blob/main/spec/2026-04-17/json-schema/schema.feed.json#L439
$product = $this->catalogMapper->toAcpProduct($this->createProduct());
// schema.feed.json#L439 — required: ["id", "variants"]
self::assertArrayHasKey('id', $product,
'MUST (schema): Product requires "id". schema.feed.json required: ["id", "variants"]');
self::assertNotEmpty($product['variants'],
'MUST (schema): Product requires non-empty "variants". required: ["id", "variants"]');
foreach ($product['variants'] as $variant) {
// schema.feed.json#L309 — Variant required: ["id", "title"]
self::assertArrayHasKey('id', $variant,
'MUST (schema): Variant requires "id". required: ["id", "title"]');
self::assertArrayHasKey('title', $variant,
'MUST (schema): Variant requires "title". required: ["id", "title"]');
// schema.feed.json#L33 — Price required: ["amount", "currency"]
self::assertArrayHasKey('amount', $variant['price'],
'MUST (schema): Price requires "amount". required: ["amount", "currency"]');
self::assertArrayHasKey('currency', $variant['price'],
'MUST (schema): Price requires "currency". required: ["amount", "currency"]');
}
}
public function testMetadataMatchesFeedMetadataShape(): void
{
// MUST (schema): FeedMetadata は "id" が required。
// @see https://github.com/agentic-commerce-protocol/agentic-commerce-protocol/blob/main/spec/2026-04-17/json-schema/schema.feed.json#L491
$meta = $this->catalogMapper->buildFeedMetadata();
self::assertArrayHasKey('id', $meta,
'MUST (schema): FeedMetadata requires "id". schema.feed.json required: ["id"]');
}
```
**ACP `acp.json` discovery 適合性 (出力ゲート / Non-Goal 検証):**
```php
// tests/Eccube/Tests/Service/AgentCommerce/Conformance/AcpDiscoveryConformanceTest.php
public function testAcpDiscoveryDoesNotLeakKeysCatalogOrMerchantId(): void
{
// MUST NOT include: Merchant identifiers / Payment handler details /
// Buyer information / Authentication tokens or keys.
// Non-Goal: "Product or catalog discovery ... is out of scope".
// @see https://github.com/agentic-commerce-protocol/agentic-commerce-protocol/blob/main/rfcs/rfc.discovery.md#L286
// @see https://github.com/agentic-commerce-protocol/agentic-commerce-protocol/blob/main/rfcs/rfc.discovery.md#L50
$doc = $this->discoveryBuilder->buildAcpDocument();
self::assertArrayNotHasKey('merchant_id', $doc,
'MUST NOT: Merchant identifiers or configuration');
self::assertArrayNotHasKey('signing_keys', $doc,
'MUST NOT: Authentication tokens or keys');
self::assertArrayNotHasKey('products', $doc,
'Non-Goal: Product or catalog discovery is out of scope');
self::assertArrayNotHasKey('catalog', $doc,
'Non-Goal: Product or catalog discovery is out of scope');
}
public function testAcpDiscoveryNotEmittedUntilReleasedAndCheckoutEnabled(): void
{
// 仕様 Status: Proposal / unreleased。実出力は RFC released 化 +
// checkout (#6776) 実装 + acp_checkout_enabled=true をゲート (本 issue §5)。
// @see https://github.com/agentic-commerce-protocol/agentic-commerce-protocol/blob/main/rfcs/rfc.discovery.md
$this->markTestIncomplete('未実装/ゲート中: acp.json 実出力は released + checkout 実装まで未エミット');
}
```
### 先行検証 (国内で活用可能・UCP Catalog / Discovery 中心)
#### Layer 1: マッピング層単体 (PHPUnit、外部依存なし)
- `Product`/`ProductClass` → ACP `Product`/`Variant` / UCP Catalog DTO
- `MinorUnitConverter` 経由の価格変換 (JPY ゼロデシマル、bcmath)
- HTML タグ除去・許可リスト (description の XSS 防止)
- 在庫切れ / 販売停止 / 削除商品の `availability` 判定
- `variant_options` による規格マッピング、`barcodes[]` は **Customize 拡張時のみ**出力
- **UCP 固有属性** (`native_commerce` / `consumer_notice` 等) の判定 (どの属性から導くかは設計レビュー、Customize 拡張可)
#### Layer 2: スキーマ契約テスト (生成データの形式正しさ — push 可否と無関係)
- ACP feed schema を `tests/fixtures/acp/`、UCP Catalog schema を `tests/fixtures/ucp/` に vendored、`2026-04-17` / `v2026-04-08` pin
- `justinrainbow/json-schema` で `products.jsonl` の各 `Product`・`metadata.json` (`FeedMetadata`)・UCP Catalog レスポンスを validate
- Spectral CLI で OpenAPI lint
#### Layer 2.5: DB 突合テスト (最重要)
feed/catalog が DB の真の状態を反映しているか:
- 全 `ProductClass` が出力に存在 (件数一致 + SKU 集合一致)
- `price02` ↔ 出力 `price`、`stock`/`stock_unlimited` ↔ `availability` 一致
- `Product.Status: NotShow` が出力から消える / `out_of_stock` 扱い
- 価格改定・在庫変動 → UCP はキャッシュ無効化 → 反映確認 (ACP push 反映は Layer 9)
- 商品削除 → UCP は次回取得で消える
#### Layer 3: UCP Catalog エンドポイント (PHPUnit)
- `POST /catalog/search` / `/catalog/lookup` / `/catalog/product`、未存在 ID の扱い、`Content-Encoding: gzip`、リクエストボディ hash によるキャッシュ
- `ucp:catalog` scope 照合、認証必須/不要モード (**認証必須モードは inbound OAuth2 = eccube-api4 依存**)
- GraphQL 版: eccube-api4 連携、N+1 回避 (SQL ログで実 SQL 件数確認・**api4 依存**)
#### Layer 3': Discovery (共通抽象 + UCP profile)
- **共通抽象**: `DiscoveryDocumentBuilder` がプロトコル非依存に動作 (well-known ルーティング、`api_base_url`/`endpoint` の `RequestContext` 動的生成、設置形態判定)
- **UCP profile 妥当性**: `signing_keys[]` が EC 公開鍵 JWK (秘密鍵パラメータ非混入)、`services`/`capabilities` の reverse-domain キー、`Cache-Control: public, max-age ≥ 60s`、HTTPS・3xx 禁止、Catalog capability 宣言、`services[].endpoint` が動的生成 (設置パスをハードコードしない)
- **ACP `acp.json` builder (出力はゲート)**: builder/モデルの単体テストは行うが、**実出力は released 化 + checkout (#6776) 実装 + `acp_checkout_enabled` をゲート**。`acp.json` に**商品/カタログ・鍵・`merchant_id` が混入しない** (Non-Goal) ことを検証。未充足時は出力されない (404/未エミット) ことを確認
#### Layer 4: パフォーマンス計測 (UCP Catalog 中心、nightly + 手動 PoC)
- 10 万 SKU (CI nightly) + 100 万 SKU (手動 trigger / 共有レンサバ実機 PoC)
- UCP: キャッシュ Miss 生成 ≤ 30 s、Hit ≤ 500 ms、メモリ ≤ 256 MB (`iterableResult()` stream)
- (`products.jsonl` 生成時間/メモリの計測は Layer 9 で ACP push と併せて実施)
#### Layer 4.5: スタンピード防止 (UCP Catalog)
- 同一リソースに 10 並列 → 再生成 1 回に集約 (Symfony Lock)
#### Layer 5: キャッシュ/差分整合 (UCP 中心)
- 在庫更新・`Order` 完了・商品削除/復活 → UCP キャッシュ無効化が正しく動く (ACP の「次回 push 対象マーク」検証は Layer 9)
#### Layer 6: Google Merchant Center Diagnostics (日本でも利用可能)
- Merchant Center テストアカウント (#6574 と共用) に Shopping Feed を登録し、Diagnostics → IDP で errors / warnings / notifications を確認
- `native_commerce` / `consumer_notice` 属性の受理確認
- 手動 trigger (`workflow_dispatch`)
#### Layer 7: 商品詳細ページとの整合性 (PHPUnit + Crawler)
- `url` / `media` URL の到達性 (200)、詳細ページ価格 ↔ 出力 `price` 一致、`NotShow` 商品が出力から消える
#### Layer 8: モックエージェント E2E (Codeception、UCP)
- UCP: Catalog 取得 → 商品識別子で CheckoutSession 作成 → 確定
### 最終段階 (ACP push — 国内 GA 前提、活用可能になるまで後回し)
#### Layer 9: ACP push トランスポート
- **push クライアント (モック feed service に対して)**: `POST /feeds` / `GET /feeds/{id}` / `GET /feeds/{id}/products` / `PATCH /feeds/{id}/products`、全置換 (`products.jsonl` 取込) と差分 upsert (`PATCH`)、outbound Bearer、リトライ/冪等性
- `products.jsonl` 生成時間 / メモリの計測 (大規模カタログ)
- ACP の差分検出 (`Product`/`ProductClass`/`Order` 更新 → 次回 push 対象マーク → `PATCH` upsert)
- ACP モックエージェント E2E: `products.jsonl` 生成 → モック feed service へ push → CheckoutSession 作成 → 確定
- **ACP 実機 push (OpenAI Feed API) は GA 後の別タスク** (`workflow_dispatch`)
### CI 統合
- Layer 0 (仕様適合性): PR ごと。`incomplete` 件数を集計し「未充足要件」として可視化 (件数の増加は要 review)
- Layer 1〜3': PR ごと (Layer 2.5 は smoke 1000 SKU + nightly 10 万)
- Layer 4: nightly (10 万) + 手動 (100 万)
- Layer 4.5 / 5 / 7 / 8: PR ごと (7/8 は smoke + nightly 全件)
- Layer 6: 手動 trigger
- **Layer 9 (ACP push): 国内 GA まで後回し。実装後は nightly / 手動 trigger** (PR 毎の常時実行にはしない)
## ロールバック方針
- discovery / UCP Catalog は公開商品データのみのため**常時公開**(専用の無効化フラグは持たない)。無効化が必要な運用では Web サーバ/ルーティング側で遮断するか、`app/Customize` で対応する。
- ACP feed push は**認証情報 (`ECCUBE_AGENT_COMMERCE_ACP_FEED_BASE_URL` / `_API_KEY`) を外せば停止**する (専用フラグは持たない)。OpenAI 側の feed は別途削除/失効。
- checkout (#6776 / #6574) は `BaseInfo.acp_checkout_enabled` / `ucp_checkout_enabled` を `false` で無効化 (本 issue の範囲外)。
---
## 進め方
**本 issue は CheckoutSession 非依存のため先行トラックで進める** (依存は #6777 の薄いスライスのみ):
1. #6777 の薄いスライス (`MinorUnitConverter` / `BaseInfo` フラグ / `AgentCommerceMessageSigner`+UCP EC 鍵 / `AgentCommerceScopeRegistry`) が landing したら着手。**CheckoutSession 中核や checkout (#6776 / #6574) の完成を待たない**
2. **先行リリース対象 (国内で活用可能・UCP 中心)**: データマッピング → **discovery 共通抽象** (プロトコル非依存、UCP profile を最初の具象、ACP `acp.json` builder もここで先行整備=#6777 共通基盤の強化) → UCP Discovery (`/.well-known/ucp`) + UCP Catalog (REST / GraphQL) を実装し、Layer 1〜8 で検証して早期リリース。**`acp.json` の実出力は released 化 + checkout (#6776) 実装をゲート**し、それまでは builder のみ (フラグ裏・未エミット)
3. **最終段階 (国内 GA 前提)**: ACP push トランスポート (Layer 9) を後回しで実装。`products.jsonl` / `metadata.json` の生成・形式は先行段階 (Layer 1/2/2.5) で検証済みのため、push クライアントの追加に集中できる
4. ACP モックエージェント E2E (Layer 9) は、checkout (#6776) が揃った段階で feed 生成 → push → CheckoutSession 作成まで通す
---
## 追記 (2026-06-11): 参照実装 SwagUcp からの discovery 知見
Shopware の UCP 参照実装 **SwagUcp**(`agentic-commerce-lab/SwagUcp`)の discovery 実装を精読。本 issue で landing 済みの `/.well-known/ucp`(PR #6815)に対し、将来の機能強化として以下のパターンを記録する。
- **capability negotiation**: プラットフォームの profile と business の capability の **交集合**を取り、`extends`(依存)を持つ capability は親が交集合に無ければ除去(推移的に安定するまで pruning)。バージョン互換は date 比較(`platform <= business`)。
- **バージョン別プロファイル形状**: UCP の version(`YYYY-MM-DD`)ごとに `capabilities` を配列 vs name-map、`payment_handlers` の配置を切り替える互換レイヤ(SwagUcp `UcpCompatibilityService`)。
- これらは discovery を「静的文書」から「リクエストに応じた capability 交渉」へ拡張する際の参考。現状の常時公開 discovery + Catalog capability 宣言(実装済)と両立する後続強化と位置づける。
注: signing_keys[](EC P-256 JWK)・reverse-domain payment_handlers・`Cache-Control` 等の基本構造は SwagUcp と一致を確認済み。
Contributor guide
Assessment
This issue has not been assessed yet.