fix: enforce capability extends constraints on the discovery projection - #63
Conversation
6686e14 to
b00608c
Compare
|
Rebased onto main and regenerated, thank you for the nudge. Worth flagging that the regeneration did not go through unchanged. This PR Re-measured on the pushed head: CapabilityDiscoverySchema.extends emits the Checked against the running reference as well: the Python flower shop serves There is a second force push on this branch. The first one carried a stale I have also corrected the PR body, which still quoted the 2026-04-08 pattern, |
Fixes Universal-Commerce-Protocol#62 CapabilityDiscoverySchema.extends now enforces the same constraint CapabilityResponseSchema.extends already enforces since Universal-Commerce-Protocol#55: a string matching the reverse domain pattern that common/types/reverse_domain_name.json declares, or a non empty array of such strings. Before this change it accepted any string and rejected every array, including conformant ones. capability.json declares extends once, on $defs/base, and both the discovery projection and the response projection inherit it through allOf. The discovery side was hand authored with a bare { type: "string" } instead, so the two disagreed about the same declaration. Rebased onto main after Universal-Commerce-Protocol#61 and regenerated. The clone of the shared node carries refs written relative to schemas/, which do not resolve from the discovery/ output directory, so it is passed through a rewrite that repoints them the way the response projection already repoints its item ref. Keeping the node otherwise intact matters: the pattern and minItems stay attached, so quicktype emits the real constraint rather than a bare union that would then depend on the injector matching it by property set, which it does not, because the discovery and response objects differ in one property name.
b00608c to
2010aa2
Compare
Fixes #62
What
CapabilityDiscoverySchema.extends(the discovery profile projection ofcapability.json) now enforces the same constraintCapabilityResponseSchema.extendsalready enforces since #55: a string matching the reverse domain pattern
^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$, or a non empty array of such strings.Before this change it accepted any string and rejected every array, including
conformant ones.
Root cause
capability.jsondeclaresextendsonce, on$defs/base, as aoneOfof aconstrained string or a constrained array. Two things in this repo consume that
oneOf:capabilityResponse, inwriteCompatibilityDiscoverySchemas(), is derived fromthe source through
buildEntityResponseSchema(), which flattens$defs/response_schemaand runs each property through
toCompatLeaf(). That path keeps the union shape, soquicktype emits a
z.union(...)for it, and the injector fix: enforce capability extends constraints #55 extended(
describeStringArrayUnionConstraint) can find and reattach the pattern andminItems.capabilityDiscovery, in the same function, is a hand authored object for$defs/platform_schema(the discovery profile shape). Itsextendsproperty was abare
{ type: "string" }literal, never wired to the sourceoneOf. quicktype hadno union to render, so there was nothing for the injector to recover, regardless of
fix: enforce capability extends constraints #55.
The two projections exist because
platform_schemaandresponse_schemadiffer inrequired fields (platform requires
spec/schema; response does not, and also has noname), so they cannot share one output type. #55 closed the gap on the responseside; this PR closes the same gap on the discovery side, using the property the fixed
side already draws from:
This mirrors how the adjacent
schema,spec, andversionproperties in the sameobject are already cloned from the source schemas rather than hand typed, so the fix
follows the shape of an existing helper in the file, rewriteItemRefForDiscovery,
though it does add a small one of its own.
Testing
All commands run on the exact branch, from a clean clone of
Universal-Commerce-Protocol/js-sdkatdbd0dbec22b699fa6e8171f09c72f7ccf01b0b64:npm run generateagainst a checkout of the pinnedrelease/2026-08-25spec:regenerated
src/spec_generated.ts. Ran twice; byte identical both times (therepo
model-driftCI job diffs this file against a fresh regeneration, so thisPR ships that check already passing).
npm run pretest && npm test: 147 pass, 0 fail (144 pre-existing plus 3 new;one of the three, the empty array case, already passed before the fix for an
unrelated reason and is kept as a named regression guard).
npm run build:noEmit: clean.npm run build: clean (cjs, esm, types).two new constraint tests red; restoring returns the suite to 147 passing.
pre-commit runon the changed files: every hook passes.Sweep of the defect class
Swept the projection script for the same class, a hand authored literal shadowing
a constrained source declaration (the constrained leaves elsewhere in the
discovery objects are already cloned from source):
extendsnamecapability.jsonhas nonameproperty; the registry keys capabilities viapropertyNamesagainstreverse_domain_name.json, so the discoverynamefield has no source counterpart to derivesigningKeyrelease/2026-08-25profile.jsondeclares$defs/jwk_public_keyandbase.properties.keys, so this literal does shadow a real source declaration. It was out of scope when this PR was written against the 2026-04-08 pin, where no such schema shipped. Left for a separate change rather than widened hereap2_mandatefallbacks, inwriteCompatibilityAp2Schema()$defsexist (ap2_with_checkout_mandate, and the projected checkout def), so the fallbacks never engageCoexistence
#61 has merged, and the rebase onto it was not trivial. The projection change is
unchanged in substance, but regenerating on top of #61 failed outright: the
shared extends node carries a ref written relative to schemas/, which does not
resolve from the discovery/ output directory. The node is now passed through a
rewrite that repoints its refs the way the response projection already repoints
its item ref, which keeps the pattern and minItems attached rather than relying
on the constraint injector, whose index is keyed by the containing object
property set and so does not carry from the response object to this one.