Skip to content

fix: derive the discovery profile ucp member from ucp.json business_schema - #73

Open
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/discovery-profile-registry-shape
Open

fix: derive the discovery profile ucp member from ucp.json business_schema#73
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/discovery-profile-registry-shape

Conversation

@vishkaty

@vishkaty vishkaty commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #72. Credit: #19 by @artemii-karkusha reported the divergence first.

What

UcpDiscoveryProfileSchema.ucp (UcpSchema) now models the registries the way ucp.json#/$defs/business_schema declares them: services, capabilities and payment_handlers are records keyed by reverse domain name whose values are entity arrays; services and payment_handlers are required, capabilities is optional; supported_versions, status and map_order are carried. map_order is emitted as a record of string arrays per ucp.json#/$defs/map_order; the envelope builder collapsed that $ref to a string, so UcpResponseSchema and UcpCheckoutResponseSchema gain the same one line change and the second business profile example in the overview (the map_order.payment_handlers block) now parses, after the elision stripping the specification tooling applies to that block. supported_versions was typed as a record of any and accepted a number where the overlay declares a profile URI; its values are now typed as strings. Before this change the hand written node carried the withdrawn 2026-01-11 shape (capabilities as a required flat array, services as single objects, no payment_handlers) and the package rejected the business profile example that the specification publishes.

Breaking (compile-time only): Ucp.capabilities changes from CapabilityDiscovery[] to Record<string, CapabilityResponse[]> and Ucp.services from Record<string, UcpService> to Record<string, ServiceResponse[]>. No exported name is added or removed, and no conformant runtime document was accepted by the old shape.

Root cause and fix

writeCompatibilityDiscoverySchemas in scripts/project-current-ucp-schemas.mjs wrote the ucp node as a literal. The response envelope in the same function is already derived from #/$defs/base through buildResponseEnvelopeSchema. The new buildBusinessProfileUcpSchema reuses that derivation and applies the business overlay (its required list and supported_versions). business_schema is the definition profile.json binds to what a business hosts at /.well-known/ucp; the platform profile example in the overview parses through the same type since platform_schema has the same required set. Registry items use the per entity response compat shapes exactly as the envelope does. No title is set, so the type keeps its property derived name and the UcpSchema export is unchanged.

Generated diff: the UcpSchema body, the map_order line on the two response envelopes, and the supported_versions value type; quicktype reorders a few unrelated declarations. The set of exported names is identical to main (checked by diffing the sorted export lists).

Tests (TDD)

New tests/discovery-profile-shape.test.js with tests/fixtures/business_profile_2026-08-25.json, the business profile example from docs/specification/overview/index.md at release/2026-08-25 (the ucp:example schema=profile def=business_schema block, version macro rendered):

  • the discovery profile type accepts the specification example
  • capabilities as a flat array is rejected, record of arrays accepted
  • a services entry as a single object is rejected
  • payment_handlers is carried and required, capabilities is optional, services is required
  • map_order is a record of key arrays, a bare string is rejected
  • supported_versions values are strings, a number is rejected, and the version key pattern holds

The two existing tests that used the empty legacy fixture (capabilities: [], services: {}) now use the specification shape.

Written first and watched fail before the fix existed. Measured on the final branch: running these test files against the models generated by unmodified main fails 7 (5 of the new tests plus the 2 updated existing ones); reverting the projector change and regenerating fails the same 7. Per mutant, each new test is killed by its own defect: modelling services as single objects reds the services test (the capabilities test reds under that mutant too, since both read one fixture); dropping payment_handlers reds exactly its own test; reverting only the map_order hunk reds exactly the map_order test; reverting only the supported_versions hunk reds exactly the supported_versions test. Restored: 155 of 155.

Class sweep

Hand written nodes in writeCompatibilityDiscoverySchemas, each checked against ucp.json at release/2026-08-25:

node spec source status
ucpDiscoveryProfile.ucp #/$defs/business_schema converted (this PR)
capabilityDiscovery (CapabilityDiscoverySchema) legacy 2026-01-11 entry with name no longer referenced by UcpSchema; still emitted as its own root so the export stays; candidate for deprecation in a follow up
ucpService (UcpServiceSchema) legacy 2026-01-11 single object with per transport members same as above
ucpDiscoveryProfile.payment.handlers no counterpart at 2026-08-25 (handlers live in ucp.payment_handlers) left as an optional legacy member; removing it would drop an export
signingKey (SigningKeySchema) keys[] JWK entries out of scope, untouched

Residual, stated, and each one over accepts rather than rejecting a conformant document. Registry items use the response compat shapes, so the business level entity requirements are not enforced: capability.json#/$defs/business_schema requires schema on a business capability entry, and service.json#/$defs/business_schema requires endpoint when the transport is rest, mcp or a2a. Probed on this branch, the profile example is still accepted with the capability schema deleted and with the rest service endpoint deleted. Enforcing either needs a derived business variant per entity, which would add exported types, so it is left for a follow up. supported_versions values are typed as strings; the uri format is not attached, because the constraint injector keys by the containing property set.

CI

The workflows here are held behind the approval gate, so I ran them on my fork for this exact commit (e099f7a, the same head this pull request proposes). Both concluded success:

Locally, on this branch:

  • npm test: 155 passed, 0 failed (main: 147 passed)
  • ./generate_models.sh <release/2026-08-25 checkout> twice: byte identical output, and identical to the committed src/spec_generated.ts
  • npm run build:noEmit and npm run build: exit 0
  • Conventional title: fix:

Cross implementation

python-sdk already types these registries as dict of lists (ucp.py, Base). The Node reference server in samples re-exports the package surface at 0.4.3 but does not use these discovery types, so no consumer change is needed.

@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Sep 11, 2026
…chema

UcpDiscoveryProfileSchema.ucp (UcpSchema) was a hand written projection
node carrying the withdrawn 2026-01-11 discovery shape: capabilities as a
flat array and required, services as a record of single objects, and no
payment_handlers. ucp.json#/$defs/base declares every registry as an
object keyed by reverse domain name whose values are entity arrays and
requires only version; #/$defs/business_schema additionally requires
services and payment_handlers and adds supported_versions. The published
package therefore rejected the business profile example that the
specification publishes (overview/index.md, def=business_schema) on both
capabilities and services, while UcpProfileDocumentSchema in the same
file, which is derived from the schema, accepted it.

Derive the node from business_schema the same way the response envelope
is derived, through buildResponseEnvelopeSchema plus the required list
and properties of the overlay. Registry items use the per entity response
compat shape, as the envelope does. No title is set so the type keeps its
property derived name and the UcpSchema export is unchanged.

Two properties were being widened past what the schema declares, and both
are corrected here. The envelope builder collapsed the map_order $ref to
a string, which rejected the second business profile example in the
overview (the block with map_order.payment_handlers); map_order is now a
record of string arrays per ucp.json#/$defs/map_order, so
UcpResponseSchema and UcpCheckoutResponseSchema change on that one line
as well. supported_versions was typed as a record of any and accepted a
number where the overlay declares a profile URI; its values are now
typed as strings.

Compile time breaking for consumers of the discovery type: Ucp.capabilities
changes from CapabilityDiscovery[] to Record<string, CapabilityResponse[]>
and Ucp.services from Record<string, UcpService> to
Record<string, ServiceResponse[]>. No exported name is added or removed.

Tests: tests/discovery-profile-shape.test.js parses the business profile
example from release/2026-08-25 (tests/fixtures/business_profile_2026-08-25.json)
through UcpDiscoveryProfileSchema, and pins the record shape of
capabilities and services, the presence of payment_handlers, the base
and business_schema required sets, the map_order shape, and the
supported_versions value type. The two existing tests that used the empty
legacy fixture now use the spec shape.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:needs-triage Signal that the PR is ready for human triage

Projects

None yet

3 participants