fix!: define deterministic operating hours for Location service - #687
Conversation
The current Location PR introduces weekly and exceptional operating hours, but leaves several wire and evaluation semantics ambiguous. In particular, closures rely on an artificial midnight interval, `open_now` depends on an implicit server clock, exception date bounds are unclear, and the specification does not define timezone, overnight, DST, overlap, or precedence behavior. Close those gaps with a UCP-native schedule model informed by Schema.org's OpeningHoursSpecification: https://schema.org/OpeningHoursSpecification Schema.org is design input only. UCP owns the field names, values, and evaluation rules defined here. Make weekly intervals explicit and reusable: "hours": [ { "day": "tuesday", "opens": "09:00", "closes": "12:00" }, { "day": "tuesday", "opens": "13:00", "closes": "21:00" } ] Rename `open` and `close` to `opens` and `closes`, and define `day` as a stable UCP weekday identifier rather than localized display text. Multiple entries for one day represent split shifts, and an interval whose closing time is earlier than its opening time continues into the next local date. Refactor the shared time interval schema so `opens` and `closes` are an optional but inseparable pair. Weekly hours require both fields, while exception hours may omit both to represent a full closure. Reject the ambiguous `00:00` to `00:00` pair and reserve `00:00` to `23:59` as the full-local-day sentinel. Replace the previous exception shape: { "from": "2026-11-26", "through": "2026-11-27", "label": "Thanksgiving", "open": "00:00", "close": "00:00" } with inclusive local-date bounds and an actual closure representation: { "title": "Thanksgiving", "valid_from": "2026-11-26", "valid_through": "2026-11-26" } Rename `from`, `through`, and `label` to `valid_from`, `valid_through`, and `title`. Treat `title` as optional presentation metadata that does not affect schedule evaluation. Allow timed exceptions with paired `opens` and `closes`, including multiple entries with identical bounds for split shifts. Define every returned schedule in the Location's Business-owned IANA timezone. Require `timezone` whenever regular or exception hours are present, and keep the canonical schedule independent of the requesting Platform or Buyer's timezone. Specify deterministic evaluation: - convert an exact instant into each Location's local date, weekday, and time - use half-open timed intervals, except for the reserved full-day sentinel - let overnight intervals carry into the following local date - replace regular hours with exception hours at local midnight - treat omitted weekdays as having no interval starting that day - treat absent schedules as unknown rather than closed - evaluate DST gaps and folds pointwise without shifting nonexistent times - reject equal time pairs and intersecting non-identical exception ranges as Business conformance errors where JSON Schema cannot express the constraint Remove the redundant `open_now` filter. It makes results depend on an implicit processing clock and creates undefined precedence when combined with `open_at`. Require one caller-supplied RFC 3339 instant instead: "filters": { "hours": { "open_at": "2026-05-18T17:00:00Z" } } Require `open_at` to include `Z` or a numeric offset. The offset identifies the instant only; the Business still evaluates that instant using each candidate Location's authoritative IANA timezone. Keep the nested hours filter open so extensions can add qualifiers without changing the standard predicate. Move complete Search and Lookup examples into the transport-neutral capability documents. Cover hours with serviceability and amenities, inventory with distance, split shifts, full closures, and partial Lookup success there. Reduce REST and MCP examples to equivalent binding envelopes that link to the same canonical payload examples. This keeps both transports on equal footing, avoids duplicating domain semantics, and prevents one binding's examples from becoming more complete or authoritative than the other. Preserve MCP's required `meta["ucp-agent"].profile` contract while separating protocol metadata from the Location request. This is a breaking correction to the Location PR's draft wire shape: - `open` becomes `opens` - `close` becomes `closes` - `from` becomes `valid_from` - `through` becomes `valid_through` - `label` becomes `title` - `open_now` is removed - full closures omit both time fields instead of using `00:00` to `00:00`
jingyli
left a comment
There was a problem hiding this comment.
Thanks @igrigorik for the enhancement on hours representation! Overall the proposed shape LGTM but added a couple of inline comments (would especially love to get some reaction on the request filter front).
| instant expressed with `Z` or a numeric offset, and can include | ||
| extension-defined fields. Before sending a Buyer-local wall time, a Platform | ||
| **MUST** resolve it to an exact instant for `open_at`. When asking which | ||
| Locations are open now, a Platform **MUST** supply the current instant. No |
There was a problem hiding this comment.
Just a naive thought: but I feel like replacing open_now boolean filter with this approach may not be ideal in the sense that current instant is extremely difficult to enforce in practice.
Platforms can issue the request at Z instant, but because there is always some intrinsic delay before business receives the request at Z' instant, it always look like platform has issued a search request with an instant in the past. I'd imagine there would be other legitimate reasons for businesses wanting to implement some logic to invalidate platform requests searching for stores that are "opened in the past", if this case is mixed in there, it introduces a rather grey area.
Instead, if we separate out the use case with open_now, then the boolean can clearly indicate that business should use Z' when processing the search request and can also easily invalidate all requests where open_at < Z'.
There was a problem hiding this comment.
Clarified in c23dc7e, ptal.
open_at identifies the instant relevant to the buyer—such as expected arrival or pickup time—rather than making a freshness claim tied to when the Business receives it. We don't need to chase false precision and request/response timing here.
There was a problem hiding this comment.
Thanks! The clarification works well for the scheduled/future-looking intent but would love to still dig a bit deeper into this thread as I personally think the correctness/precision remains important when it comes to hyper local goods that need an immediate representation that doesn't depend on the platform's clock. Related, this data contract will require 2 additional translations on both platform & business sides to express immediate - so it complicates implementation and sacrifices precision..
The things I feel are still unresolved:
- If the business receives a platform request where
open_atis in the past relative to the business's clock, it must decide whether to allow it or reject it now through leveraging a "tolerance window". - If platform's clock is misconfigured or agent hallucinates when it attempts to translate a user's "immediate" intention to an absolute local instant, then the response will be incorrect.
|
|
||
| ### Evaluation | ||
|
|
||
| Timed intervals ordinarily include the opening time and exclude the closing |
There was a problem hiding this comment.
Given this is documentation for location, I'm wondering if we should call out explicitly that the intervals used here are actually inclusive of both ends instead (also consistent of how we plan to represent the exception valid date ranges) of this standard half open representation?
The current way will result in a very unnatural way of representation opening hours. Example: a store that opens from 10am to 5pm. Under this rule, businesses would need to return [10:00, 17:01) vs. the more humanly understandable format of [10:00, 17:00].
There was a problem hiding this comment.
I don't think half-open is right contract here. 10:00–17:00 is open immediately before 17:00 and closed at 17:00, matching the ordinary meaning of "closes at 5" without requiring 17:01. A value of 17:02 means the Location closes exactly at 17:02; it is not rounded to 17:00.
There was a problem hiding this comment.
I actually think we are agreeing on the same thing conceptually 😃 but the wording of the sentence here remains the issue...
exclude the closing time seems to suggest that the closing time (value in closes) should be discarded and naively reads more like a [10:00, 17:01) and there is no further clarification sentence in this paragraph talking about why for location specifically we are leveraging a closed interval - the only exception builds on top of the half-open semantics for entire date.
Can I suggest rewording this sentence instead to call out that time intervals in Location capability deviates from the ordinary half-open timed intervals and instead enforces a closed interval where opening and closing times are both included?
Define `open_at` as the caller-selected instant relevant to the request, such as an expected arrival or pickup time. This avoids framing it as a request for the Business's receipt-time notion of "now": normal request latency does not change the question, and the Business evaluates the supplied instant against each Location's schedule. Describe operating hours more directly as local dates and clock times interpreted using the Location's IANA timezone. Clarify that temporary closures retain the regular `hours` schedule and override it with a date-bounded `exception_hours` entry that omits `opens` and `closes`. Mirror omitted-schedule semantics in the Location schema for implementers who read generated references: - an omitted day has no regular interval beginning that day - an interval from the preceding day may still carry into it - omission of the entire `hours` property means the schedule is unknown Make `time_interval` genuinely reusable by limiting it to generic `HH:MM` opening and closing fields. Location-specific recurrence and timezone interpretation remain with the containing daily, exception, and Location schemas. Remove the schema check that rejected only `00:00`–`00:00`. The actual authoring rule rejects every pair where `opens` equals `closes`, but standard JSON Schema cannot compare sibling values; enforcing one special case would misleadingly imply that other equal pairs are valid. Continue enforcing paired field presence and time formatting mechanically, while keeping unequal times as a normative Business conformance requirement and requiring Platforms not to infer openness from invalid schedule data.
|
|
||
| ### Evaluation | ||
|
|
||
| Timed intervals ordinarily include the opening time and exclude the closing |
There was a problem hiding this comment.
I actually think we are agreeing on the same thing conceptually 😃 but the wording of the sentence here remains the issue...
exclude the closing time seems to suggest that the closing time (value in closes) should be discarded and naively reads more like a [10:00, 17:01) and there is no further clarification sentence in this paragraph talking about why for location specifically we are leveraging a closed interval - the only exception builds on top of the half-open semantics for entire date.
Can I suggest rewording this sentence instead to call out that time intervals in Location capability deviates from the ordinary half-open timed intervals and instead enforces a closed interval where opening and closing times are both included?
| instant expressed with `Z` or a numeric offset, and can include | ||
| extension-defined fields. Before sending a Buyer-local wall time, a Platform | ||
| **MUST** resolve it to an exact instant for `open_at`. When asking which | ||
| Locations are open now, a Platform **MUST** supply the current instant. No |
There was a problem hiding this comment.
Thanks! The clarification works well for the scheduled/future-looking intent but would love to still dig a bit deeper into this thread as I personally think the correctness/precision remains important when it comes to hyper local goods that need an immediate representation that doesn't depend on the platform's clock. Related, this data contract will require 2 additional translations on both platform & business sides to express immediate - so it complicates implementation and sacrifices precision..
The things I feel are still unresolved:
- If the business receives a platform request where
open_atis in the past relative to the business's clock, it must decide whether to allow it or reject it now through leveraging a "tolerance window". - If platform's clock is misconfigured or agent hallucinates when it attempts to translate a user's "immediate" intention to an absolute local instant, then the response will be incorrect.
The TC discussion converged on keeping one `open_at` filter, but left open whether both the Platform and Business could apply timing tolerance when interpreting immediate intent. After further consideration, assign that flexibility to one side only. The Platform owns the interpretation of Buyer intent and selects the instant to query. It may use its current time, choose an expected arrival, pickup, or order-acceptance time, and round or adjust that choice to the granularity appropriate to the interaction. Once encoded, however, `open_at` identifies one specific RFC 3339 instant. Require the Business to evaluate that instant exactly as supplied using each Location's authoritative timezone. It must not round, shift, substitute request receipt time, or otherwise reinterpret the value. Allowing both parties to apply independent tolerance would make the evaluated question unknowable and could produce different matches for identical requests near an opening or closing boundary. Apply normal positive-match filter semantics: return a Location only when the Business can establish that it is open at `open_at`. Missing, invalid, out-of-range, or otherwise unusable schedule data is a non-match rather than a reason to guess or adjust the requested instant. Clarify that the numeric offset in `open_at` identifies the queried instant, not the Location's timezone. The Business converts that instant using the Location's authoritative IANA timezone before evaluating its local schedule. State closing-boundary behavior concretely: a `10:00`–`17:00` interval is open immediately before `17:00` and closed at `17:00`. This avoids ambiguity over whether `HH:MM` values represent exact boundaries or minute-sized buckets. Keep exception payloads useful for planning without accumulating stale history. Businesses should remove entries once they cannot affect any current or future instant and publish known future exceptions through the horizon for which their schedule is authoritative. Remove the request-language localization recommendation for exception `title`. The field remains optional presentation metadata, but this capability does not define a localization guarantee for it.
|
Thanks @igrigorik for the latest update! All my outstanding concerns are now addressed through 8bb2b74. Proceeding with the merge back into the feature branch. |
The current Location PR (#687) introduces weekly and exceptional operating hours,
but leaves several wire and evaluation semantics ambiguous. In particular,
closures rely on an artificial midnight interval,
open_nowdepends on animplicit server clock, exception date bounds are unclear, and the specification
does not define timezone, overnight, DST, overlap, or precedence behavior.
Close those gaps with a UCP-native schedule model informed by Schema.org's
OpeningHoursSpecification:
https://schema.org/OpeningHoursSpecification
Schema.org is design input only. UCP owns the field names, values, and
evaluation rules defined here.
Make weekly intervals explicit and reusable:
Rename
openandclosetoopensandcloses, and definedayas astable UCP weekday identifier rather than localized display text. Multiple
entries for one day represent split shifts, and an interval whose closing time
is earlier than its opening time continues into the next local date.
Refactor the shared time interval schema so
opensandclosesare anoptional but inseparable pair. Weekly hours require both fields, while
exception hours may omit both to represent a full closure. Reject the ambiguous
00:00to00:00pair and reserve00:00to23:59as the full-local-daysentinel.
Replace the previous exception shape:
with inclusive local-date bounds and an actual closure representation:
Rename
from,through, andlabeltovalid_from,valid_through, andtitle. Treattitleas optional presentation metadata that does not affectschedule evaluation. Allow timed exceptions with paired
opensandcloses,including multiple entries with identical bounds for split shifts.
Define every returned schedule in the Location's Business-owned IANA timezone.
Require
timezonewhenever regular or exception hours are present, and keepthe canonical schedule independent of the requesting Platform or Buyer's
timezone.
Specify deterministic evaluation:
Remove the redundant
open_nowfilter. It makes results depend on an implicitprocessing clock and creates undefined precedence when combined with
open_at. Require one caller-supplied RFC 3339 instant instead:Require
open_atto includeZor a numeric offset. The offset identifies theinstant only; the Business still evaluates that instant using each candidate
Location's authoritative IANA timezone. Keep the nested hours filter open so
extensions can add qualifiers without changing the standard predicate.
Move complete Search and Lookup examples into the transport-neutral capability
documents. Cover hours with serviceability and amenities, inventory with
distance, split shifts, full closures, and partial Lookup success there.
Reduce REST and MCP examples to equivalent binding envelopes that link to the
same canonical payload examples. This keeps both transports on equal footing,
avoids duplicating domain semantics, and prevents one binding's examples from
becoming more complete or authoritative than the other. Preserve MCP's
required
meta["ucp-agent"].profilecontract while separating protocolmetadata from the Location request.
This is a breaking correction to the Location PR's draft wire shape:
openbecomesopensclosebecomesclosesfrombecomesvalid_fromthroughbecomesvalid_throughlabelbecomestitleopen_nowis removed00:00to00:00