fix: derive the UCP response envelope and its registries from source - #38
Merged
damaz91 merged 1 commit intoAug 10, 2026
Conversation
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).
carolinerg1
requested review from
DanielFalconGuedes,
jingyli and
westeezy
and removed request for
jingyli
August 7, 2026 15:48
damaz91
approved these changes
Aug 10, 2026
damaz91
merged commit Aug 10, 2026
52cd356
into
Universal-Commerce-Protocol:main
15 of 16 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Observed
The generated
ucpresponse envelope does not matchucp.json#/$defs/base, so conformant responses lose or fail on several fields. Most visibly, the checkout response envelope omitspayment_handlers(the 04-08 checkout response binds toucp.json#/$defs/response_checkout_schema, whoseallOfaddsrequired: ["payment_handlers"]), which is the SDK root cause behind the sample-server gap in samples#168. Running the golden checkoutucpenvelope through the generatedUcpResponseSchemaalso false-rejected oncapabilities.*.name— a field the spec'scapability.json#/$defs/response_schemadoes not define.Root cause:
scripts/project-current-ucp-schemas.mjshand-wrote the compat response envelope and its registry item schemas, so they had drifted fromucp.json#/$defs/basein several independent ways (missingpayment_handlers/services/status, wrongly-requiredcapabilities, 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/baseand derives each registry item from its real#/$defs/response_schema(flattening the entity/baseallOfchain), 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 phantomname),services(optional, real service response shape — was stripped),payment_handlers(optional,{id, version}required +available_instruments/config/schema/spec— was absent). Capabilityextendsnow accepts itsstring | string[]source form.Scope / residuals (honest)
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).propertyNamesnot enforced; embedded per-transport serviceconfigcollapsed to a generic object; item-levelversionstrings not regex-constrained (the envelope's ownversionis).CapabilityResponseno longer exposes.name— no conformant runtime data ever carried it.Verification
ucpenvelope (capabilities + payment_handlers together, capability items withoutname, string and arrayextends) and assert acceptance with every registry retained; plus the required-field negatives. Each RED on the pre-change artifact, GREEN after, kill-tested.npm test: 41/41.npm run build+tsc --noEmitclean; the fix: enforce JSON Schema value constraints in generated zod schemas (#33) #34/feat: enforce uniqueItems and contains/minContains/maxContains in generated zod schemas #37 constraint-injector suites still green; pre-commit clean. The regenerated diff is only the envelope-derivation change (no product-type renames).