Skip to content

fix: derive the UCP response envelope and its registries from source - #38

Merged
damaz91 merged 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix-response-payment-handlers
Aug 10, 2026
Merged

fix: derive the UCP response envelope and its registries from source#38
damaz91 merged 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix-response-payment-handlers

Conversation

@vishkaty

@vishkaty vishkaty commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Observed

The generated ucp response envelope does not match ucp.json#/$defs/base, so conformant responses lose or fail on several fields. Most visibly, the checkout response envelope omits payment_handlers (the 04-08 checkout response binds to ucp.json#/$defs/response_checkout_schema, whose allOf adds required: ["payment_handlers"]), which is the SDK root cause behind the sample-server gap in samples#168. Running the golden checkout ucp envelope through the generated UcpResponseSchema also false-rejected on capabilities.*.name — a field the spec's capability.json#/$defs/response_schema does not define.

Root cause: scripts/project-current-ucp-schemas.mjs hand-wrote the compat response envelope and its registry item schemas, so they had drifted from ucp.json#/$defs/base in several independent ways (missing payment_handlers/services/status, wrongly-required capabilities, and a legacy handler/capability item shape).

Change: derive the envelope from source

Instead of hand-writing it, the projection now derives the response envelope from the parsed source ucp.json#/$defs/base and derives each registry item from its real #/$defs/response_schema (flattening the entity/base allOf chain), so per-envelope specifics are the source schemas, not hand-written code, and a future base property cannot silently vanish (the projection throws on an unknown registry entity rather than dropping it).

Result — all five base properties now modeled at the correct shape and required-ness:

  • version (required, date-format), status (enum success/error, optional — was stripped), capabilities (optional — was wrongly required; item derived, no phantom name), services (optional, real service response shape — was stripped), payment_handlers (optional, {id, version} required + available_instruments/config/schema/spec — was absent). Capability extends now accepts its string | string[] source form.

Scope / residuals (honest)

  • The four response envelopes share one generated type, so checkout's required: ["payment_handlers"] cannot be enforced without a distinct type — payment_handlers is optional on the shared type; a checkout response missing it is not rejected. Noted as a follow-up (a distinct checkout envelope).
  • Over-accept only (never false-rejects a conformant response): registry-key propertyNames not enforced; embedded per-transport service config collapsed to a generic object; item-level version strings not regex-constrained (the envelope's own version is).
  • Breaking (compile-time only): CapabilityResponse no longer exposes .name — no conformant runtime data ever carried it.

Verification

The generated UCP response envelope (UcpResponseSchema, aliased by all four
response envelopes) was a hand-written compatibility schema that had drifted
from ucp.json#/$defs/base, and its registry item shapes were stale
hand-written approximations. A spec-conformant response was false-rejected or
silently stripped.

Observed vs expected
- Expected: ucp.json#/$defs/base (required: ["version"]) defines version,
  status, services, capabilities, and payment_handlers; each registry is
  keyed by reverse-domain name whose values are arrays of that entity's
  #/$defs/response_schema:
    * payment_handler.json#/$defs/response_schema: required {id, version};
      carries available_instruments, config, schema, spec.
    * service.json#/$defs/response_schema: required {transport, version}.
    * capability.json#/$defs/response_schema: required {version} ONLY, with
      an optional `extends` (string | string[]) and NO `name` property.
  response_checkout_schema additionally requires payment_handlers.
- Observed: payment_handlers/services/status were absent (stripped on parse);
  capabilities was wrongly required; PaymentHandlerResponse required the
  non-existent config_schema/instrument_schemas/name; and CapabilityResponse
  required a non-existent `name`, so EVERY conformant capabilities registry
  (including the conformance golden checkout ucp envelope) was rejected with
  path ["capabilities", <rdn>, 0, "name"] "Required". Confirmed by
  regenerating against the pinned 2026-04-08 schemas (SDK 0.4.x -> 2026-04-08
  per the README compat table).

Root cause & fix
The pipeline projects the source-layout spec into a legacy layout via
scripts/project-current-ucp-schemas.mjs, which HAND-WROTE the compat
discovery/ucp_response.json envelope and every registry item schema -- so any
base property or item field the author omitted or mis-specified diverged
silently. The envelope and all three registry item shapes are now DERIVED from
the parsed source: buildResponseEnvelopeSchema() enumerates
ucp.json#/$defs/base's own properties and required set (mapping each registry
to its per-entity response compat, failing loudly on an unknown entity), and
buildEntityResponseSchema() flattens payment_handler.json / service.json /
capability.json #/$defs/response_schema (through the entity/base allOf chain)
into the correct item shapes. Capability `extends` (string | string[]) is
emitted as a JSON Schema type-union so quicktype produces z.union without an
anyOf node (an anyOf node perturbs quicktype's naming of unrelated anonymous
types). A future base property or item field can no longer silently vanish.

payment_handlers is OPTIONAL on this shared envelope: one generated type is
aliased by all four response envelopes and order/cart/catalog legitimately
omit payment_handlers, so a checkout-only `required` would falsely reject
them. Enforcing the checkout-specific requirement needs a distinct type and is
left as a follow-up.

BREAKING (compile-time only): CapabilityResponse no longer exposes `.name`.
No conformant response ever carried it (it is not in capability.json), so
this is a correctness fix; consumers reading `.name` must drop it.

This is the SDK-level root cause behind the sample-server workaround filed as
samples#168 (which added payment_handlers to the sample checkout response).

Regenerated src/spec_generated.ts (the repo commits generated artifacts):
UcpResponseSchema models capabilities?/payment_handlers?/services?/status?/
version; PaymentHandlerResponseSchema/ServiceResponseSchema/
CapabilityResponseSchema match their real response shapes (pulling in
AvailablePaymentInstrumentSchema, TransportSchema, UcpResponseStatusSchema).
Added tests/response-payment-handlers.test.js asserting fidelity of the FULL
golden checkout envelope (all three registries together, retained), each
registry's required-ness, capability `extends` as string and string[], and a
sibling response without payment_handlers.

Known residuals (out of scope, over-accept only -- never false-reject a
conformant response): the shared envelope cannot enforce the checkout-only
`required: [payment_handlers]`; registry propertyNames (reverse_domain_name)
are not enforced on the record keys; the service response's per-transport
(embedded) config typing is collapsed to a generic object; and item-level
version strings are not regex-constrained (the envelope's own version is).
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Aug 6, 2026
@carolinerg1 carolinerg1 added status:under-review and removed status:needs-triage Signal that the PR is ready for human triage labels Aug 7, 2026
@carolinerg1
carolinerg1 requested review from DanielFalconGuedes, jingyli and westeezy and removed request for jingyli August 7, 2026 15:48
@damaz91
damaz91 merged commit 52cd356 into Universal-Commerce-Protocol:main Aug 10, 2026
15 of 16 checks passed
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.

3 participants