Skip to content

feat!: Introduce Location Search + Lookup capabilities - #589

Open
jingyli wants to merge 31 commits into
mainfrom
feat/location
Open

feat!: Introduce Location Search + Lookup capabilities#589
jingyli wants to merge 31 commits into
mainfrom
feat/location

Conversation

@jingyli

@jingyli jingyli commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Description

A mirror copy of #545 that supersedes it.

Defines standard interfaces for discovering, searching, and retrieving physical locations (e.g., retail stores, restaurants, warehouses, lodging properties).

It introduces two new capabilities under the dev.ucp.common namespace (consistent with Shopping's Catalog capability design):

  1. Location Search (dev.ucp.common.location.search): Discovery-focused endpoint for natural language query, geographic, and offerings-based filters.
  2. Location Lookup (dev.ucp.common.location.lookup): Resolution-focused endpoints supporting single & batch lookups.

Some key commerce flows it will be able to unlock:

  • Local Pickup Discovery: Finding locations like retail stores or restaurant branches
    nearby that support customer pickup and checking their operating hours & inventory availability
    before selection.
  • Fulfillment Area Verification: Checking if a specific location (e.g., utility depot, restaurant,
    or local service provider) has delivery coverage for a buyer's address.

Category (Required)

  • Core Protocol: Changes to the base communication layer, global context, or breaking refactors. (Requires Technical Council approval)
  • Governance/Contributing: Updates to GOVERNANCE.md, CONTRIBUTING.md, or CODEOWNERS. (Requires Governance Council approval)
  • Capability: New schemas (Discovery, Cart, etc.) or extensions. (Requires Maintainer approval)
  • Documentation: Updates to README, or documentations regarding schema or capabilities. (Requires Maintainer approval)
  • Infrastructure: CI/CD, Linters, or build scripts. (Requires DevOps Maintainer approval)
  • Maintenance: Version bumps, lockfile updates, or minor bug fixes. (Requires DevOps Maintainer approval)
  • SDK: Language-specific SDK updates and releases. (Requires DevOps Maintainer approval)
  • Samples / Conformance: Maintaining samples and the conformance suite. (Requires Maintainer approval)
  • UCP Schema: Changes to the ucp-schema tool (resolver, linter, validator). (Requires Maintainer approval)
  • Community Health (.github): Updates to templates, workflows, or org-level configs. (Requires DevOps Maintainer approval)

Related Issues

This is related to RFC #375's section 10.

Checklist

  • I have followed the Contributing Guide (including Conventional Commits title requirements and ! for breaking changes).
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • (For Core/Capability) I have included/updated the relevant JSON schemas.
  • I have regenerated Python Pydantic models by running generate_models.sh under python_sdk.

Screenshots / Logs (if applicable)

location_ucp_doc

@igrigorik igrigorik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, the shape and proposal make sense—ty for scaffolding this. A few design questions I'd like to work through...

1. Should common be the service, or the namespace containing a Location service?

The capability names are scoped to Location:

dev.ucp.common.location.search
dev.ucp.common.location.lookup

The profile examples, however, advertise the transport as:

{
  "services": {
    "dev.ucp.common": [{ "...": "..." }]
  }
}

I agree with dev.ucp.common.* as the namespace for cross-vertical primitives. However, I'm not sure about making dev.ucp.common one catch-all service: every future shared capability would then share an endpoint, transport schema, etc.

The alternative split is:

namespace:    dev.ucp.common.*
service:      dev.ucp.common.location
capabilities: dev.ucp.common.location.search
              dev.ucp.common.location.lookup

Do we think a catch-all .common service is preferred to scoped services?

2. Is geofence_radius sufficient to model the domain?

The current geo object combines a physical point with a circular geofence_radius, while geofence_point asks whether a requested point falls inside that circle. This covers a useful simple case, but in my experience service areas are often irregular polygons, disjoint regions, postal-code unions, etc.

Do we need to model (thin, hopefully) RFC 7946-based service-area shape supporting Polygon / MultiPolygon?

3. Can we adopt a Schema.org-inspired shape instead of inventing a bespoke one?

The latest changes make the custom model deterministic by defining 24:00, precedence between is_closed / is_24_hours / intervals, and a two-entry convention for overnight hours. That is an improvement, but it also highlights how much new protocol syntax and interpretation logic we are defining.

Schema.org's OpeningHoursSpecification already provides the core semantics we need:

  • dayOfWeek, opens, and closes;
  • multiple entries for split shifts;
  • closes < opens means the interval spans the next day;
  • no opens means closed;
  • validFrom / validThrough cover date-specific exceptions.

A Schema.org-inspired UCP shape could look like:

{
  "timezone": "America/New_York",
  "hours": [
    { "day_of_week": "monday", "opens": "09:00", "closes": "17:00" },
    { "day_of_week": "monday", "opens": "18:00", "closes": "22:00" },
    { "day_of_week": "friday", "opens": "22:00", "closes": "02:00" }
  ],
  "exception_hours": [
    {
      "valid_from": "2026-12-24",
      "valid_through": "2026-12-24",
      "opens": "09:00",
      "closes": "14:00"
    },
    {
      "valid_from": "2026-12-25",
      "valid_through": "2026-12-25"
    }
  ]
}

The contract is compact: each entry describes one open interval; repeated entries for the same day represent split shifts; a closes value earlier than opens spans midnight; and a date-bounded exception without opens means closed for that period. timezone supplies the interpretation context for local times. The example adapts Schema.org's semantics to UCP's snake_case naming and HH:MM convention rather than copying its JSON-LD representation verbatim.

Is there a concrete requirement that OpeningHoursSpecification cannot model? If not, I would align to it and remove the custom closed/24-hour flags, precedence rules, and overnight splitting convention.

4. Is offerings.inventory.quantity an inventory-disclosure query?

The store-finder use case—"which nearby location can fulfill the item I need?"—is valuable. A hard minimum quantity filter, however, lets a caller repeatedly probe thresholds and approximate a store's stock level even when the response never returns a count.

I suggest framing this as Buyer demand and coarse availability, not inventory disclosure:

  • reuse UCP's existing availability semantics rather than parallel inventory concept;
  • treat requested quantity as intent that a Business may coarsen;
  • change language to not imply that exact on-hand counts are returned;

5. What does offerings mean? 😅

Modelled request can filter on:

{
  "offerings": {
    "amenities": ["..."],
    "inventory": [{ "id": "..." }]
  }
}

The standardized Location entity, however, exposes neither amenities nor offerings. Although the filter contract implies that each returned Location satisfies the requested values, a Platform cannot render the matched facts, inspect additional amenities, or present structured item availability from the response without relying on vendor-defined fields. There is also a category question: amenities are relatively static Location characteristics, while item availability is dynamic and tied to another Catalog or menu identity. Grouping both under offerings may be convenient structurally, but does not necessarily make them one semantic concept.

My inclination would be to put static amenities on Location, model dynamic availability separately using the existing UCP availability vocabulary, and flatten the filter unless offerings gains a clearer cross-vertical contract.

6. What amenity vocabulary is interoperable across Businesses and verticals?

An open string is the right wire type for UCP, but the current proposal defines no well-known values. Should UCP publish a small open vocabulary for interoperable matching, and which initial values are important enough to standardize?

7. What are the capability-specific security and privacy requirements?

Location handles Buyer location hints, enumerates physical sites, describes service coverage, and can be used to probe per-location item availability. The generic Signals reference and current privacy note do not cover those capability-specific risks.

I think the specification needs explicit Security and Privacy Considerations covering:

  • coarse-by-default Buyer location and progressive disclosure;
  • purpose limitation and retention of location inputs;
  • store/site enumeration and rate limiting;
  • inventory and availability probing;
  • disclosure of internal, private, or non-buyer-visible locations;
  • disclosure of precise service-area geometry.

8. What is the bounded Location projection for Catalog and Checkout?

This PR points Checkout’s pickup destination directly at the rich common Location entity. That couples Checkout to the complete Location discovery model: geo, hours, service area, amenities, and every future Location extension would automatically accrete into the transactional response. I don't think that's right.

In Catalog Fulfillment (#507), we deliberately introduced a thin Location projection: a stable location ID plus method description. This avoided embedding an N-store matrix in product results and deferred richer Location facts to a separately negotiated capability. That boundary still makes sense, but I think there is a middle ground: split Location into a bounded base and an extended discovery entity.

Base Location
  id
  name
  address?
      │
      └── allOf → Extended Location
                    geo
                    hours
                    exception_hours
                    timezone
                    amenities
                    service_area

Catalog and Checkout would use the bounded, buyer-renderable base. Location Search/Lookup would return the extended entity when the service is negotiated. Platform requests would select a Location by stable id rather than asserting Business-owned name/address facts. This preserves the bounded Catalog model from #507, keeps Catalog and Checkout independently renderable, and prevents the full Location discovery model from accreting into every Checkout response.

Location search intentionally permits Business-defined filters, but the schema
relied on JSON Schema's implicit open-object default while the prose named
additionalProperties as the extension mechanism.

Declare the extension point explicitly so schema readers and generated
documentation can distinguish intentional extensibility from omission. Strict
resolution remains a caller-selected closed-world override.
Location documentation inherited Catalog-specific descriptions, rendering
contexts, and a severity policy that Location never defined. It also documented
a singular REST path and a filter name that do not exist in the binding/schema.

Use Location-specific llms.txt descriptions and render scopes, remove the
unsupported severity claim, and align the visible endpoint and filter names with
their canonical definitions.
@amithanda

Copy link
Copy Markdown
Contributor

+1 on scoping the service to dev.ucp.common.location. The thing that pushes me here is what the profile can still express once common has more than one domain in it, since the service key is what determines how many endpoints and how many version trains a business gets.

The setup. Suppose common grows the way the name invites, say Location plus Reviews plus Identity linking. Under a catch-all service the profile can only say:

"services": {
  "dev.ucp.common": [{
    "version": "2026-04-08",
    "transport": "rest",
    "schema": ".../services/common/rest.openapi.json",
    "endpoint": "https://business.example.com/ucp"
  }]
}

One endpoint, one transport document, and one version covering store lookup, review retrieval, and identity linking. That seems to make three fairly ordinary deployments unexpressible:

  1. Routing. Store locator data is usually served by the merchant's own site infrastructure, while reviews are very often served by a third-party review platform on that vendor's domain, and identity linking sits behind the IdP. With one endpoint per transport per service there is no way to say that. The workaround would be a per-capability endpoint override, which is scoped services under a different name.
  2. Versioning. The service entry carries its own version. A Reviews field change bumps the shared service version and emits a churn signal to every Location implementer even though nothing about Location moved. Domains that are operated by different parties rarely share a release cadence.
  3. Operational policy. A public store locator is anonymous, cacheable, and high volume. Identity linking handles tokens and wants stricter auth and tighter rate limits. One base URL for both forces a single shared policy, or path-based policy that the spec does not describe.

The scoped form expresses all three directly, including the case where two domains happen to share a host:

"services": {
  "dev.ucp.common.location": [{
    "version": "2026-04-08", "transport": "rest",
    "schema": ".../services/common/location/rest.openapi.json",
    "endpoint": "https://stores.business.example.com/ucp"
  }],
  "dev.ucp.common.reviews": [{
    "version": "2026-01-11", "transport": "rest",
    "schema": ".../services/common/reviews/rest.openapi.json",
    "endpoint": "https://ucp.reviews-vendor.example.com/business-123"
  }],
  "dev.ucp.common.identity": [{
    "version": "2026-04-08", "transport": "rest",
    "schema": ".../services/common/identity/rest.openapi.json",
    "endpoint": "https://business.example.com/ucp"
  }]
}

The asymmetry I think settles it: scoped services can emulate the catch-all, but the catch-all cannot emulate scoped. A business serving everything from one gateway just points every scoped entry at the same endpoint, as Identity does above, and pays a few redundant lines of generated JSON. A business that needs reviews served by a vendor and locations served in-house has no move available under the catch-all until the spec itself changes. Where one option is a strict superset of the other in expressive power and the only cost is profile verbosity, is verbosity not the cheaper thing to pay? Profile JSON is generated and fetched once, while endpoint topology is a deployment constraint that cannot be refactored away.

Would it be worth writing the rule down explicitly, something like: a service is the unit of deployment, not the unit of naming? A new service is warranted whenever a domain could plausibly be operated by a different party, on different infrastructure, or on a different release cadence than its siblings.

I like that framing because it explains the existing corpus rather than contradicting it. dev.ucp.shopping stays one service correctly, since cart, catalog, checkout, and orders share a transactional session and one commerce backend, so the cohesion is real. dev.ucp.common fails that test close to by construction, since "common" is defined as whatever is cross-vertical, which is nearly the opposite of a deployment boundary. On that reading the Catalog precedent this PR follows is not being overturned, just scoped: Catalog rides the shopping service because shopping is cohesive, not because "one service per namespace" is the rule. common looks like the first namespace where naming and deployment come apart, which is why it seems worth deciding deliberately rather than inheriting the Catalog shape by default.

@amithanda

Copy link
Copy Markdown
Contributor

For suggestion 2. above could we just drop the circle instead of upgrading it to polygons?

geofence_radius only appears in one place that matters, the Location response. Both filter inputs (distance.center and geofence_point) send just a point, as the rest.md examples show. And geofence_point is a predicate the Business evaluates server side, so it behaves identically whether coverage is a circle, a polygon, a postal code list, or a drive-time isochrone. The circle is only needed to publish the shape, never to ask the question.

That matters because publishing it as a circle is currently self-contradictory. search.md:96 binds the filter to the circle ("locations whose circular service area (defined by geofence_radius) contains the specified point"), so a Business whose real area is a polygon has to either approximate it (wrong answers near the boundary, across a river or a highway) or evaluate its true shape (in which case the published radius disagrees with the filter result). Which one is authoritative is undefined, as is the case where a location publishes no radius but a geofence_point filter arrives.

Proposal: drop geofence_radius and let coverage stay a server-evaluated predicate.

  • geo.json becomes a pure point, which is all three call sites actually need;
  • the filter takes an intent-based name such as serves or coverage, since the question is "which locations serve this?" rather than "whose circle contains this?";
  • the spec says Businesses MAY evaluate coverage with any internal representation and MUST treat their own evaluation as authoritative, so presence in the result set is the answer and no geometry is returned;
  • would it be worth accepting a postal address here too? index.md:27 frames the use case as coverage "for a buyer's address," but the filter takes only latitude and longitude, so postal-code-based Businesses have to reverse geocode back into the ZIP they already reason in.

Why this looks better than adding Polygon/MultiPolygon: it removes the contradiction rather than encoding it more precisely, it keeps the one representation every Business can implement, and it avoids publishing geometry that most would decline to share anyway.

"components": {
"parameters": {
"authorization": {
"name": "Authorization",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authorization cannot be modeled as an ordinary header parameter in OpenAPI 3.1; compliant tooling is required to ignore a header parameter with that name. Because this document also has no securitySchemes or root/operation security requirements, generated clients and gateways will interpret these operations as unauthenticated. UCP-Agent identifies a claimed profile but does not prove who sent the request. Please model the supported authentication alternatives through securitySchemes and operation-level security, explicitly represent anonymous public search if that is supported, and state that authentication and authorization occur before location-ID resolution or filtering.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great find and I think one that is also affecting our existing openapi services - e.g., https://github.com/Universal-Commerce-Protocol/ucp/blob/main/source/services/shopping/rest.openapi.json#L808! To keep the scope of the PR focused on the new capability, I'm going to keep this as-is here but have filed #668 to track a quick follow-up fix that would universally apply to all openapi service definitions we have.

}
},
"responses": {
"200": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both operations currently document only a 200 response even though the specification defines invalid-request, authentication, rate-limit, and server-error behavior. Autonomous clients need machine-readable distinctions between terminal errors, caller-correctable errors, and retryable failures; otherwise they may retry unauthorized requests, silently abandon valid searches, or amplify an outage. Please add the shared UCP error envelope for at least 400, 401, 403, 429, and 5xx, require Retry-After where appropriate, and align the REST and MCP behavior for an oversized lookup batch. The current documentation alternates between implementation-defined limits and a 400; please choose one normative status and error code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another great callout as I also recall seeing feedback from the community - #131 (section in Standardize Error Responses on explicitly mapping out protocol-level errors.

I'd like to keep this PR focused on the capability design and propose a follow-up to enhance all openapi definitions (e.g., https://github.com/Universal-Commerce-Protocol/ucp/blob/main/source/services/shopping/rest.openapi.json).

"format": "uuid",
"description": "Unique key for retry safety. Maps to HTTP Idempotency-Key header (optional for read-only operations)."
},
"signature": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces a detached JWS in the MCP JSON metadata, while UCP specifies RFC 9421 HTTP Message Signatures for HTTP transports, including MCP Streamable HTTP. These mechanisms are not wire-compatible and the new field leaves canonicalization, covered fields, key resolution, and replay parameters underspecified. Please remove the JSON-level detached JWS and use Signature-Input, Signature, and Content-Digest at the HTTP transport layer, consistent with the other UCP services. If a non-HTTP MCP transport is introduced later, its signing model should be specified independently rather than overloading this metadata field.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PTAL 17dac60.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17dac60 corrected the terminology but not the protocol layer: signature, signature_input, and content-digest remain JSON members of meta. RFC 9421 defines Signature and Signature-Input as HTTP fields, and UCP's existing MCP signing guidance places those fields plus Content-Digest in HTTP headers and explicitly avoids a duplicate JSON representation. Implementations following this OpenRPC document would sign a different wire contract than implementations following the core signing specification.

Comment thread source/schemas/common/location_search.json
Comment thread source/schemas/common/types/location_filter.json
Comment thread source/schemas/common/types/location_filter.json
Comment thread source/schemas/common/types/exception_hour.json
Comment thread source/schemas/common/types/location.json
Comment thread source/schemas/shopping/types/fulfillment_destination.json
@jingyli jingyli changed the title feat: Introduce Location Search + Lookup capabilities feat!: Introduce Location Search + Lookup capabilities Aug 1, 2026
@jingyli

jingyli commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @maximenajim for the feedback! Responses added and requesting for another passthrough.

@jingyli
jingyli requested a review from maximenajim August 1, 2026 03:51
},
{
"$ref": "retail_location.json"
"$ref": "../../common/types/location_base.json",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every location_base instance has id and name, while shipping_destination requires only id and permits unknown properties through its composed postal-address schema. A normal BOPIS destination such as {"id":"store-1","name":"Downtown","address":{"postal_code":"55403"}} therefore matches both branches and is rejected by oneOf; the current schema makes the bounded pickup projection unusable in Checkout.

Please select the destination schema from the containing fulfillment method—for example, type: "shipping" constrains destinations.items to shipping_destination and type: "pickup" constrains them to location_base—or add an explicit discriminator that makes the branches mutually exclusive. Add a conformance case with id, name, and address.

},
"ucp_request": "omit"
},
"hours": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The revised interval behavior is inspired by schema.org, but the wire representation still replaces its deployed vocabulary with hours, exception_hours, day, open, close, from, and through. Existing merchant JSON-LD uses openingHoursSpecification, specialOpeningHoursSpecification, dayOfWeek, opens, closes, validFrom, and validThrough; schema.org-aware generators and validators will not recognize the proposed fields, and through also uses a different exclusive-date convention.

@maximenajim

Copy link
Copy Markdown

I completed another pass. The proposal is looking good. I added a few more follow-up comments.

@igrigorik igrigorik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Jing — we're headed in the right direction. Lots of good feedback already, I'll focus on a couple of big questions/flags I'd like us to align and iterate on...

Let's reuse the existing Catalog and Shopping contracts

Location should inherit the established search and identifier-resolution semantics wherever they apply. Unqualified serves(target) should mean that any currently available method can serve the target. inventory[].id should remain opaque and Business-resolved, while its status predicate reuses Shopping's availability vocabulary. We should explicitly define omitted and conflicting inventory-status predicates. One forward-compatibility caveat: serves is currently closed and probably shouldn't be; adding a method qualifier later would require a schema/version change unless we preserve that extension point now.

Q: should amenities be a reverse-domain-name keyed map?

We currently model each amenity as a string, which is simple if the contract is only “this Location has this amenity.” Did we consider—or do we want—to model amenities as a reverse-domain-name keyed map instead?

I'd like to stress-test whether we'll eventually want properties attached to an amenity, such as a subtype, restrictions, availability, or other metadata. If amenities are deliberately Boolean membership identifiers, the current string array is fine. If we expect them to become extensible assertions, an RNDS map would give us namespace ownership and room for additional properties without a later array-to-map wire break.

Remaining hours schema and semantic issues together

The latest updates fixed timezone enforcement and some earlier exception-hour inconsistencies, but the current contract still has unresolved issues:

  • open_now and open_at may both be supplied, with implementation-defined precedence or OR behavior;
  • exception_hours uses 00:0000:00 for full-day closure, leaving no unambiguous 24-hour-open representation and no defined equal-time behavior for regular hours;
  • exclusive through differs from Schema.org's inclusive validThrough without an explicit conversion rule;
  • regular versus exception precedence, overlapping exceptions, split and overnight intervals, and closure behavior are not defined as one evaluation model;
  • the renamed UCP fields do not yet have a mechanical mapping to the corresponding Schema.org vocabulary.

Building on Maxime's existing thread, I think these should be resolved as one atomic schema, prose, and examples change rather than adjusting the pieces independently—potentially as a standalone stacked PR.

Separate the existing Fulfillment destination debt from the Location capability

This is a big one, because it points to existing spec debt. Maxime's oneOf comment exposes an existing Fulfillment problem: the shipping and Business Location destination branches overlap, the request projection does not reduce Location selection to a stable ID reference, and the surrounding contract does not guarantee that a discovered Location is accepted by the applicable Fulfillment method.

Replacing retail_location.json with location_base.json does not by itself solve those issues, and it also changes an existing published path. I recommend handling this as a separately reviewed breaking Fulfillment refactor—discriminator, bounded response projection, ID-based request selection, identity guarantee, and compatibility plan—that #589 can then build on.

@igrigorik

Copy link
Copy Markdown
Contributor

@jingyli opened #687 as a branch PR to address the hours feedback I flagged above, ptal. cc @gsmith85

richmolj added a commit that referenced this pull request Aug 5, 2026
Destination authorship is a function of the enclosing method's type:
shipping destinations are Platform-authored; pickup destinations are
Business-authored (the Platform selects one, never writes one); and
extension-defined method types specify their own destination shape and
writability.

- Destination `type` is required in responses, optional in requests.
  When a request destination omits it, the method's type implies the
  shape: untyped destinations under `shipping` validate as Shipping
  Destinations via new method-level conditioning (the conditioning of
  destinations[] on method.type the original schema lacked). A Platform
  MAY include `type` to disambiguate a reference when more than one
  kind can appear under a method (e.g. an id-only destination
  referencing a Business-managed mailing address vs an identity
  provider's customer address book).
- Removed the writable business-location destination (`{type, id}`
  request stub). `selected_destination_id` is the sole
  location-selection channel; it accepts any stable, Business-scoped
  Location ID the Business recognizes for the method, including IDs not
  yet enumerated in destinations[] (the #589 handoff). The Business
  returns the typed Location Summary in its response.
- Updated prose, field docs, and request-direction examples accordingly.

Validated: ucp-schema lint, validate_examples (296 pass),
test_validate_examples (50 pass), strict spec build, check_links.
richmolj added a commit that referenced this pull request Aug 5, 2026
…ly in requests

A fulfillment method's type selects the shape of its entire subtree,
destinations included: a shipping method has shipping-address
destinations, a pickup method has business-location destinations, and
extension-defined method types define their own. Polymorphism is
resolved at the parent, so request destinations need no per-object
discriminator.

- fulfillment_method branches per method type; the generic
  fulfillment_destination union is no longer referenced by schemas
  (kept for response documentation).
- Destination type is required in responses, optional in requests.
- destinations under pickup is response-only (ucp_request omit inside
  the pickup branch): under strict resolution the Platform cannot write
  business locations. selected_destination_id is the sole selection
  channel and accepts any Business-scoped Location ID the Business
  recognizes for the method, including IDs not yet enumerated (#589
  handoff).
- dependentRequired: a request that writes destinations[] must carry
  the method's type.
- Removed explicit additionalProperties:true from fulfillment_method
  (behavior-neutral in open validation; lets strict sealing work).
- Existing request wire shapes are unchanged; responses gain the
  required type field.

Assisted-By: devx/296664b9-53b6-409a-989a-ace9d3348247
igrigorik added a commit that referenced this pull request Aug 6, 2026
* fix!: discriminate destinations at the method level; type response-only in requests

A fulfillment method's type selects the shape of its entire subtree,
destinations included: a shipping method has shipping-address
destinations, a pickup method has business-location destinations, and
extension-defined method types define their own. Polymorphism is
resolved at the parent, so request destinations need no per-object
discriminator.

- fulfillment_method branches per method type; the generic
  fulfillment_destination union is no longer referenced by schemas
  (kept for response documentation).
- Destination type is required in responses, optional in requests.
- destinations under pickup is response-only (ucp_request omit inside
  the pickup branch): under strict resolution the Platform cannot write
  business locations. selected_destination_id is the sole selection
  channel and accepts any Business-scoped Location ID the Business
  recognizes for the method, including IDs not yet enumerated (#589
  handoff).
- dependentRequired: a request that writes destinations[] must carry
  the method's type.
- Removed explicit additionalProperties:true from fulfillment_method
  (behavior-neutral in open validation; lets strict sealing work).
- Existing request wire shapes are unchanged; responses gain the
  required type field.

Assisted-By: devx/296664b9-53b6-409a-989a-ace9d3348247

* clarify directional destination typing

   Fulfillment responses always self-describe with a required destination type,
   while Platform requests follow the enclosing method's authorship contract.

   Clarify that untyped destinations under the well-known shipping method default
   to Shipping Destination, pickup destinations are Business-authored and selected
   through selected_destination_id, and other method types define their own request
   shape and Platform writability.

   Remove the unsupported suggestion that an alternate destination type can be
   selected under core shipping. An ID-only saved or provider-held address remains
   a Shipping Destination; provider provenance or additional fields require a
   negotiated extension contract.

---------

Co-authored-by: Ilya Grigorik <ilya@grigorik.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants