Repository navigation
Conversation
…talog.feed) Platforms have no standard way to acquire a Business's catalog in bulk for offline indexing, embedding, and ranking (RFC #550, DWG #682). Today's options are bespoke per-source APIs or crawling, neither of which carries the Business's own product and variant identifiers, so the resulting index cannot be resolved against Catalog Lookup or used for checkout. Design principle: build from UCP primitives. Discovery is a capability; market scope is the catalog `context` object; records are catalog Product objects; access control is UCP's existing Platform authentication; extensions compose as they do for Search and Lookup. Shape, top to bottom: - Profile. `dev.ucp.shopping.catalog.feed` declared beside search and lookup, carrying one `config.endpoint`: the URL that serves the feed list. No service entry, no operation, no MCP tool. The endpoint may be operated by the Business, its commerce platform, or a delegated feed publisher; the Business declares and revokes it through its profile and the operator applies the same request conventions and authentication mechanisms as the service endpoint. This lets feed managers publish without the commerce platform proxying, and lets feed authentication (public or permissionless) differ from checkout's. - Feed list. Fetched from `config.endpoint`, provisioned by caller identity, authoritative and complete for that caller. Each entry is one feed: `id`, `context`, `format`, `interval`, `generated_at`, and the URL of its current `manifest`. Different surfaces of one Platform are different identities, not request parameters, because feeds are materialized ahead of time. `format` on the entry lets a Platform pick feeds it can read without fetching manifests; a Business may list the same content in more than one format side by side. - Manifest. One per snapshot, at its own URL, immutable once published; identified and ordered by `generated_at`, retained until `expires_at`. Declares the capability and the extensions its records conform to (bounded by the profile), an open `format` vocabulary with `jsonl+gzip` and `parquet` (one Product per row, variants nested) registered, and a flat `files` list with RFC 9530 digests. Platforms MUST support every registered format. A design note explains why the list and the manifest are separate documents: control plane (per-caller, polled, small) versus data plane (shared, immutable, cacheable, written by the same system that writes the files). - Semantics. A feed is resolved for one `context`; the market triple is recommended, sub-national scope allowed, and a Platform must not consume a feed whose context carries a member it does not honor when serving. Absence from a complete snapshot means not offered in that context and carries no reason. Partial fetch or digest mismatch never triggers removals. `interval` is an advisory ISO 8601 duration; `Retry-After` is honored. For feeds served only to authenticated callers, manifest and file URLs carry an unguessable component. - Supplemental feeds. An entry with `extends` names a base feed in the same list. Its records are partial Products applied to the base record with the same `id`: each member carried replaces the base member whole, `null` removes it, and `variants` merges variant by variant by `id`. A supplement carries the `context` of the result it produces, which may differ from its base's, so one base can serve several markets through thin per-market supplements; the Business is responsible for the composed record being correct for that context. Supplements of one base apply in feed-list order, later wins. `id` is always the Business's; a third party's identifiers travel as extension fields. Worked examples: per-market prices, and a marketplace's trade-compliance fields delivered as a supplemental feed so the base feed stays portable. Deliberately absent: a `list_feeds` operation and MCP tool (the manifests do the work); per-file `count` (redundant with `digest`); caching MUSTs (left to implementers); a member-by-member object merge (whole replacement lets the schema catch a partial member such as a price without a currency). Draft for DWG discussion; not for merge until the decision log closes. Refs: #550, #682.
Factor the feed mechanism out of the shopping namespace so that a second domain can publish feeds without living under `dev.ucp.shopping`. The mechanism text is moved, not rewritten; what changes is which page it lives on and which nouns it uses. Shape: - `dev.ucp.common.feed` (source/schemas/common/feed.json, new) is the root capability: `config.endpoint`, the feed list, the feed entry, the manifest, and the file. Records are opaque to it; a manifest's `ucp.capabilities` declares what they are. Discovery, provisioning, snapshot semantics, immutability, digests, formats, access, and supplemental feeds are unchanged in substance and now domain-neutral. `context` stays here: it is a common type used across UCP, and the mechanism states its contract abstractly. - `dev.ucp.shopping.catalog.feed` becomes an extension of it. Its profile entry declares `extends`, and its schema composes onto the common manifest to add `filters` in the Catalog Search vocabulary. Its page keeps only what is shopping: records are catalog Products with the identifiers Catalog Lookup resolves, the market triple as the recommended `context`, `filters`, `variants` as the identified list for supplemental feeds, and the two worked examples. - `ucp.json` gains `response_feed_schema` for the two feed documents, which are neither catalog nor location responses. Decisions the split forced: - The extension composes onto the manifest only. Feed list entries are pure mechanism, because the list is one document shared by every record type a Business publishes, while a manifest is per feed and declares its own extensions. `filters` therefore lives on the manifest and belongs to the shopping extension. - `config.endpoint` is on the parent. A shopping publisher declares two profile entries, and negotiation prunes `catalog.feed` without its parent. That one extra entry is the whole blast radius. - No per-entry discriminator. A Platform learns a feed's record type from its manifest, fetched once on first sight. Manifest extension entries carry `extends` as profile entries do, since a manifest is a declaration rather than a record of negotiation: the entry extending `dev.ucp.common.feed` is the record type, a Platform skips a snapshot whose record type it does not recognize, ignores fields from unrecognized extensions of the record type, and rejects an entry it cannot classify. The feed list declares only `dev.ucp.common.feed`. Left open for discussion: who registers `format` values and governs `dev.ucp.common.feed`, and how a Product-field extension binds to feed records. Refs: #550, #682.
|
The split between feed list and manifest, and immutable snapshots at their URL, look right to me. Just two gaps I'd raise: 1. Terms of use for the feed itself. UCP already has the pieces. 2. A record count per file.
Happy to post schema and prose for either as review suggestions. Francesco Marinoni Moretto |
|
|
||
| `extends` tells a Platform what kind of entry it is looking at: | ||
|
|
||
| * The entry that extends `dev.ucp.common.feed` is the feed's **record type**: |
There was a problem hiding this comment.
On extension chaining: requiring record-level extensions to declare extends: "dev.ucp.shopping.catalog.feed" creates an artificial split with schemas like fulfillment and policy.return, which already canonically extend catalog.search or lookup.
I'd keep the manifest hierarchy scoped to the document: common.feed as the root, and catalog.feed as the record type extending it. Any extensions that decorate the Product record can keep their normal canonical parentage as advertised in the profile, without needing to synthesize a parallel inheritance tree just for feeds.
flowchart LR
subgraph Current ["Current PR #866 (Split Inheritance)"]
direction TB
C_Root["dev.ucp.common.feed"] --> C_Feed["dev.ucp.shopping.catalog.feed"]
C_Feed -->|"forced extends"| C_Ext1["dev.ucp.shopping.fulfillment"]
C_Feed -->|"forced extends"| C_Ext2["dev.ucp.shopping.policy.return"]
C_Search["dev.ucp.shopping.catalog.search"] -->|"canonical extends"| C_Ext1
end
subgraph Proposed ["Proposed (Decoupled Scope)"]
direction TB
P_Doc["dev.ucp.common.feed\n(Document Root)"] --> P_Feed["dev.ucp.shopping.catalog.feed\n(Record Type = Product)"]
P_Feed -.->|"records conform to"| P_Canon["Canonical Product + Extensions\n(fulfillment, policy.return, etc.)"]
end
There was a problem hiding this comment.
This doesn’t require a separate extension or duplicated field definitions. An extension can target both Catalog Feed and live Catalog capabilities, reusing shared $defs while defining the appropriate shape for each surface—just as Fulfillment does across Cart and Checkout.
Declaring the feed parent makes that applicability explicit. We shouldn’t infer it from Search/Lookup parent, because an extension may intentionally expose different or non-overlapping data on those surfaces
jamesandersen
left a comment
There was a problem hiding this comment.
Great progress here @igrigorik; this brings together many of the concepts we've been discussing in the DWG so far nicely.
|
|
||
| ## Extensibility and Supplemental Feeds | ||
|
|
||
| ### Extensions |
There was a problem hiding this comment.
This section defines the mechanism clearly, but I think it would land much better with a worked example. As a reader I can follow that an extension declared in the manifest's ucp.capabilities adds typed fields to records, but it took me a while to work out what a Business actually has to do end to end — and that path is the one most likely to be walked by people new to UCP.
Something like the following, for a Business adding five apparel attributes:
1. Publish an extension schema at an authority-bound URL on a domain the Business controls:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://merchant.example.com/ucp/schemas/apparel.json",
"name": "com.example.merchant.apparel",
"$defs": {
"dev.ucp.shopping.catalog.feed": {
"allOf": [
{ "$ref": "https://ucp.dev/{version}/schemas/shopping/product.json" },
{ "type": "object",
"properties": {
"fabric": { "type": "string" },
"care_instructions": { "type": "string" },
"fit": { "type": "string", "enum": ["slim", "regular", "relaxed"] },
"sustainability_rating": { "type": "integer", "minimum": 1, "maximum": 5 },
"country_of_manufacture": { "type": "string" }
} }
]
}
}
}
2. Declare it in the profile, alongside the feed capability and the record type:
"capabilities": {
"dev.ucp.common.feed": [
{ "version": "{version}", "spec": "...", "schema": "...",
"config": { "endpoint": "https://merchant.example.com/ucp/feeds" } }
],
"dev.ucp.shopping.catalog.feed": [
{ "version": "{version}", "spec": "...", "schema": "...",
"extends": "dev.ucp.common.feed" }
],
"com.example.merchant.apparel": [
{ "version": "{version}",
"spec": "https://merchant.example.com/docs/apparel",
"schema": "https://merchant.example.com/ucp/schemas/apparel.json",
"extends": "dev.ucp.shopping.catalog.feed" }
]
}
3. Declare it in each manifest whose records carry those fields:
"ucp": {
"version": "{version}",
"capabilities": {
"dev.ucp.common.feed": [{ "version": "{version}" }],
"dev.ucp.shopping.catalog.feed": [{ "version": "{version}", "extends": "dev.ucp.common.feed" }],
"com.example.merchant.apparel": [{ "version": "{version}", "extends": "dev.ucp.shopping.catalog.feed" }]
}
}
4. Emit the fields inline in the record:
{"id": "P100", "title": "Oxford Shirt", "price_range": {}, "variants": [], "fabric": "100% cotton", "care_instructions": "Machine wash 30", "fit":
"slim", "sustainability_rating": 4, "country_of_manufacture": "PT"}Writing it out surfaced one question I couldn't answer from the text. In the example above the extension extends dev.ucp.shopping.catalog.feed, so the five attributes exist only in the feed. If the Business wants the same attributes in live Catalog Search responses — which seems likely, given the records are described as "exactly as returned by Catalog Search" — does it need a second extension against dev.ucp.shopping.catalog.search, or is one extension expected to span both? Worth making explicit either way.
|
On alex-jansen's point about small in-place changes and JMathnov's question on file size: if a share λ of products changes on a given day and products are spread across files independently of how often they change, a file of n products is untouched with probability about (1 − λ)^n. At the roughly 1 to 3% of prices a day implied by published studies of online retailers (for example Cavallo 2018, NBER w25138), a file of a few hundred products almost always changes, so splitting a snapshot into smaller files and skipping unchanged ones saves little unless files hold only a handful of products, and grouping volatile products together helps only with small files. That suggests choosing file size for transfer and parallelism, and leaving small changes to deltas. Two thoughts for the delta phase, on the minimal delta mode bennyblum proposed on #550, itself building on BarryLzc's proposal here:
|
|
📋 Discussed in UCP Bulk Product Discovery Domain Working Group (DWG) Meeting - 2026-10-28 This pull request was referenced under the following agenda topic(s):
See the full meeting minutes for discussion notes and action items. |
|
📋 Discussed in UCP Shopping Tech Council (STC) Meeting - 2026-10-23 This pull request was referenced under the following agenda topic(s):
See the full meeting minutes for discussion notes and action items. |
Make existing custom record-type extensibility and feed-only publication explicit. Each record type defines its operation bindings. Replace the draft bare format tokens with namespaced identifiers using the shared reverse-domain type. Preserve the JSONL/gzip and Parquet contracts, mandatory core support, optional extension formats, and exact feed-list/manifest format matching.
Let domains define concrete record layouts, serialization, and compression while keeping Common Feed record-model agnostic. Move JSONL and Parquet formats to dev.ucp.shopping.catalog.jsonl and dev.ucp.shopping.catalog.parquet. Recommend both for Catalog consumers instead of requiring universal support from Common Feed Platforms. Preserve declared-owner requirements, full-identifier matching, and feed-list/manifest equality.
Keep descriptions focused on purpose and field meaning rather than duplicating the specification. Shorten the capability overview and both format descriptions; detailed requirements remain in the prose.
Add optional manifest links using the existing Link type so Businesses can reference terms governing Platform use of a snapshot's records. Clarify that supplemental elements update existing identified elements without inserting or deleting them, and that missing-resource recovery applies both before and after expires_at.
Require private manifests and files to be protected by resource-level authorization or unguessable URLs, rather than prescribing URL layout even when access is authorized.
Existing exporters often overwrite files at stable URLs. Permit that
publication model without requiring a new file URL for every change,
while preserving the complete-snapshot contract.
Keep each snapshot's manifest immutable at a distinct URL. File URLs
SHOULD be immutable, but mutable ones MUST have files[].digest binding
their expected bytes. Platforms MUST verify every provided digest,
including against cached bytes, before treating a snapshot as complete.
These MUSTs prevent URL reuse from silently mixing generations. For
example, a fetch spanning an update can produce:
Part 1 Part 2
Generation 1 A B
Generation 2 B C
Mixed fetch A C
B exists in both generations, but the mixed result omits it. Applying
removal semantics would incorrectly remove B. A successful HTTP fetch
does not establish snapshot consistency, and an unverified checksum
provides no protection.
Treat missing files and unverifiable or mismatched digests as an
incomplete snapshot: keep the prior copy and do not apply removals.
On digest failure, re-read the feed list and fetch the current snapshot,
respecting polling and retry rules.
Mutable URLs can make older snapshots unavailable once overwritten;
verification prevents accepting wrong bytes, not loss of availability.
Retention guidance therefore applies to the expected file bytes, not
merely a URL that still responds.
Keep response_feed_schema and response_ask_schema as separate, unchanged definitions when resolving the add/add conflict.
|
Thanks everyone for the great feedback and comments! I believe I've addressed and captured all the outcomes from the group and GH discussions. PTAL. |
|
One small, non-blocking clarification on Supplemental Feeds. The common spec already requires a supplement to carry members whose values depend on members it changes, and the catalog example applies this to Could If you'd rather keep this PR narrow, a follow-up issue works too. Francesco Marinoni Moretto |
A price change can leave an inherited unit_price stale even when the currency is unchanged. Platforms cannot repair it because the Business-supplied unit price is authoritative. Name unit_price alongside price_range in the Catalog supplement example: when a changed price invalidates an existing unit_price, the Business restates it under the existing dependent-member rule.
|
@frmoretto clarified in 7f1161e for that particular example. Ultimately, we cannot define or enumerate all the dependencies, and we do have prose that delegates this responsibility to the business that generates the feed. |
This proposal adds a way for a Business to publish its catalog in bulk — the same Product records Catalog Search returns — as files a Platform can index, embed, and rank offline, then call Catalog Search and Lookup at serving time for authoritative price and availability. The feed is point-in-time; the live operations stay authoritative.
Built from UCP primitives: discovery is a capability whose
confignames an endpoint; market scope is the catalogcontext; records are catalog Products; access is UCP's existing Platform authentication; extensions compose as they do for Search and Lookup. The 10k-foot view...Staged design is factored into a domain-neutral mechanism,
dev.ucp.common.feed(discovery, feed list, manifest, files, snapshot semantics, supplemental feeds), plusdev.ucp.shopping.catalog.feedas an extension that specifies the record type to catalog Products and addsfiltersdefined by shopping service.Checklist
!for breaking changes).